GitLab项目导入RCE分析(CVE-2022-2185)
作者:Nguyễn Tiến Giang (Jang) · 2022年7月21日 · 11分钟阅读
目录
- 环境搭建与调试
- CVE-2022-2185分析
- 相关信息
- GitLab中的Worker旁注
- 案例1
- 案例2
- 批量导入中的Pipeline旁注
- 案例1 + 2 = 3
- 概念验证视频
本月初,GitLab发布了14至15版本的安全补丁。公告中提到一个CVSS 9.9分的认证后RCE漏洞。
该漏洞存在于GitLab的项目导入功能中,由@vakzz发现。有趣的是,我在作者HackerOne资料中发现他四个月前也曾发现导入项目功能中的另一个漏洞。
看到赏金后我心动不已,于是开始学习Rails并调试此漏洞!(没想到3万美元并不容易赚到( ° ͜ʖ ͡°))
注意:本文可能比往常更长。您可闲暇时阅读,若只想看PoC,可直接跳至末尾观看视频!
环境搭建与调试
这部分相对繁琐复杂,需要研究人员耐心!最初我参考了Sun*朋友的帖子搭建环境,但运行后速度慢且不稳定,于是决定自行安装。
我的环境使用Ubuntu Desktop 18.04虚拟机。
首先设置GitLab的GDK套件:
|
|
等待15-30分钟完成GDK设置。然后检出有漏洞的GitLab版本:
|
|
检出后编辑以下文件:
config/gitlab.yml,找到gitlab主机配置行,改为虚拟机IP以便外部浏览:
|
|
找到webpack相关行,将enabled设为false:
|
|
编辑后进入GitLab/文件夹,输入以下命令编译服务器资源:
|
|
config/puma.rb,找到声明workers的行并注释:
|
|
编辑完配置文件后,输入以下命令启动相关服务:
|
|
IDE方面,我使用RubyMine(Jetbrains产品)。用RubyMine可浏览打开gitlab文件夹,IDE会自动检测安装相关组件。通过Run > Edit Configurations添加调试配置。
添加Rails配置如下:
然后添加sidekiq配置:
使用从GitLab仓库克隆的源代码时,调试配置已就绪。
从此可开始调试,尽管sidekiq worker可能运行不稳定。目前我仍不知为何有时会错过任务。
CVE-2022-2185分析
我依据@vakzz早前报告的漏洞分析此漏洞。尽管两个漏洞仅在入口点相同而技术完全不同,建议先阅读旧漏洞报告,其中某些技术可能相关。
相关信息
修复版本为15.1.1和14.10.5。我选择v15.1.1开始研究。
阅读gitlab-v15.1.1的提交时,幸运的是提交不多,但我发现以下重要提交:
提交5d58c705有有趣名称,听起来与此漏洞相关:
security-update-bulk-imports-project-pipeline-15 -1
此提交中的显著变化: 在lib/gitlab/import_export/decompressed_archive_size_validator.rb中:
validate_archive_path
方法检查@archive_path是否为符号链接、非字符串和非文件的情况:
|
|
调用validate_archive_path后,该方法继续调用Open3.popen3(command, pgroup: true)
运行命令。命令声明如下:
|
|
此方法直接将字符串@archive_path追加到命令gzip -dc
中,所以我猜测命令注入漏洞发生在此处!
DecompressedArchiveSizeValidator
类在两个地方使用:
- file_importer.rb
- file_decompression_service.rb
GitLab中的Worker旁注
GitLab工作机制是Web界面仅处理常规任务。较重任务使用sidekiq作为workers执行作业,这些作业从Web控制器推送。
这也是设置调试环境时必须为sidekiq添加调试配置的原因。
案例1
首先查看file_importer.rb分支。从Import::GitlabProjectsController开始,它创建并调用Projects::GitlabProjectsImportService.new(current_user, project_params).execute
来创建作业。
第17、18和19行已被注释以便调试。我不明白为何GDK调试环境有问题。所有上传文件都报告无效!!此问题在产品版本中不会发生。
用于创建项目的project_params已被限制,仅允许传递参数:name、path、namespace_id、file。
堆栈跟踪到此位置:
从GitlabProjectsImportService.execute继续调用prepare_import_params来编辑、添加和删除其他重要参数(1)。
然后,GitlabProjectsImportService.execute再次调用Projects::CreateService.execute,使用从GitlabProjectsController传递的参数创建Project。在Projects::CreateService.execute中,如果导入的项目不是模板,该方法将继续使用传递的参数初始化Project对象。
创建项目后,该方法继续进入调用validate_import_source_enabled!验证import_type的分支。
有两个分支满足条件:第一个条件中,import_type属于以下类型之一:
|
|
第二种情况,import_type必须存在于Gitlab::CurrentSettings.import_sources列表中。
创建Project对象并进行一些杂项修改后,此方法调用Projects::CreateService.import_schedule为worker添加导入计划:
|
|
要添加到导入计划中,此项目必须具有gitlab_project导入类型。
添加到计划后,worker将接收作业并执行以下操作:
堆栈跟踪到DecompressedArchiveSizeValidator.execute:
但是,根据此分支,我们无法控制@archive_path。
当worker执行作业时,@archive_file从Project.import_source获取。但此属性默认未设置且值为null!
此值在Gitlab::ImportExport::FileImporter.new中仍为null。
仅在调用Gitlab::ImportExport::FileImporter.copy_archive时,此值才会设置:
@archive_file_name基于Project的full_path生成,因此无法操纵此值。根据此分支,我们无法进行命令注入¯(ツ)/¯
案例2
file_importer.rb分支被确认不可利用,因此我转向第二个分支分析。第二个分支是file_decompression_service.rb。
此分支相当难分析,需要更多发现才能获得正确payload访问。
首先,必须进入GitLab的导入组功能,填写GitLab URL和访问令牌等信息。
正确填写后,我们将进入导入页面。只需单击Import按钮开始导入:
回到Burpsuite请求历史记录,我们有如下请求:
在源代码中搜索group_entity关键字,我发现除了group_entity之外,还有project_entity:
我在Web或任何文档中找不到此功能。很可能,这是GitLab的隐藏开发功能!
此批量导入功能由Import::BulkImportsController处理。
执行create_bulk_import后,BulkImportsController.execute方法继续调用BulkImportWorker.perform_async,方法内容如下:
注意调用BulkImports::CreatePipelineTrackersService.new(entity.execute!)
。此方法考虑哪些Pipeline适合与传递的参数执行:
例如,对于project_entity,我们有一些如下Pipeline:
批量导入中的Pipeline旁注
此概念是批量导入独有的。这些Pipeline的可执行文件是lib/bulk_imports/pipeline/runner.rb。
Pipeline将声明和重写方法,如extract、transform、load和after_run。
runner将按顺序浏览和执行这些方法:提取数据、转换数据、加载数据、after_run。
并按照stage.rb文件中声明的顺序依次执行Pipeline。
回到批量导入项目,ProjectPipeline pipeline将是第一个执行的pipeline。
ProjectPipeline内容:
在ProjectPipeline.load中,调用Projects::CreateService.execute
,参数params = data。如旁注所述,data是由Transformers修改的数据。
ProjectPipeline的extractors和transformers是:
- extractor ::BulkImports::Common::Extractors::GraphqlExtractor, query: Graphql::GetProjectQuery
- transformer ::BulkImports::Common::Transformers::ProhibitedAttributesTransformer
- transformer ::BulkImports::Projects::Transformers::ProjectAttributesTransformer
根据Pipeline流程:
- GraphqlExtractor.extract将通过graphql从目标获取数据
- ProhibitedAttributesTransformer和ProjectAttributesTransformer将修改接收到的数据
对于GraphqlExtractor,在GitLab的修复提交中,Graphql::GetProjectQuery修复如下:
此处可清楚看到要检索的变量已大部分减少。
以下是GraphqlExtractor检索的数据示例:
对于ProhibitedAttributesTransformer,此transformer的主要功能是删除一些敏感属性:
对于ProjectAttributesTransformer.execute:
此方法获取data,执行一些额外步骤设置必要属性如import_type、name、path,然后调用data.transform_keys!(&:to_sym)
执行转换。所有Hash的键刚刚传入Symbol形式。
// 在Ruby中有Symbol与String概念。粗略地说,Symbol前面会有冒号“:”
以下是执行data.transform_keys!(&:to_sym)后的示例:
请记住,data完全可控,因为它从GraphQL检索,GraphQL从我们的网站获取。
回到案例1中提到的导入,我们完全可以记录project.import_source,从中控制@archive_file和RCE(〜 ̄▽ ̄)〜
在ProjectAttributesTransformer的修复提交中,此transformer没有接受检索到的数据,而是创建了一个新Hash,仅添加一些必要的键/值并返回干净的Hash,意味着未添加其他属性:
当前,提取和转换后的data将传入Projects::CreateService.execute。
不幸的是,从ProjectPipeline创建的项目只能具有import_type = gitlab_project_migration。
但是,在import_schedule中,此项目将被条件!@project.gitlab_project_migration?
拒绝:
|
|
尽管可以控制Project对象的属性,但最重要的属性已被覆盖,无法重写(实际上可以,但我将在另一篇文章中讨论)。
案例1 + 2 = 3
然后……我在那里卡了近两周,研究ruby的调试功能。有时碰到断点,有时没有。有时RubyMine突然崩溃,这让我相当头疼。
直到最后几天,我找到了另一种更快的调试方法,无需开启GitLab服务器,即使用GitLab的RSpec调试。此方法的便利是避免等待sidekiq worker。
从此,我开始用project_pipeline_spec.rb调试ProjectPipeline,修改一些与project_data相关的数据然后运行:
多亏如此,我的调试非常快,并取得了一些新结果。
仔细重读Projects::CreateService.execute分支后,我意识到错过了模板处理分支:
此分支调用Projects::CreateFromTemplateService.execute
与params,即从ProjectPipeline获取的data。
此方法主要检查template_name的存在,然后调用GitlabProjectsImportService.execute与params进行进一步处理:
如第1部分讨论,GitlabProjectsImportService.execute然后将调用prepare_import_params处理params:
这里,如果template_file存在,程序将覆盖param import_type为gitlab_project。
处理param后,GitlabProjectsImportService.execute将调用Projects::CreateService.execute使用修改后的params重新创建项目。
因此,import_type已更改为gitlab_project,仍重用旧Pipeline params => RCE ( ͡° ͜ʖ ͡°)( ͡° ͜ʖ ͡°)( ͡° ͜ʖ ͡°)
有个注意点:注入的命令不会立即执行!
在Gitlab::ImportExport::FileImporter.import中,将调用wait_for_archived_file方法等待@archive_file存在,然后进入较低处理分支(我们注入命令的分支)。
wait_for_archived_file方法内容: 使用MAX_RETRIES = 8,此程序将循环8次等待文件存在,每次睡眠2^i,我应用公式计算幂级数和。大一学生可验证此点,我们知道如果文件不存在,必须等待2^8 -1 = 255秒:
如果文件不存在,此方法还继续在底部调用yield,这意味着wait_for_archived_file之后的语句仍正常调用,例如:
此时,关于此漏洞的一切都很清楚,尽管阅读Ruby/Rails的过程相当痛苦,但也带来了很多知识和一些有趣的事情。
概念验证视频
演示时间!
如果视频加载失败,这里是备份。
感谢阅读!
© 2025 STAR Labs
Powered by Hugo & PaperMod