漏洞概述
GitLab CE/EE的8.9、8.10、8.11、8.12以及8.13版本中存在任意文件读取漏洞,攻击者或可利用这个漏洞来获取应用程序中敏感文件的访问权。在获取到这些机密数据之后,攻击者将可以通过执行恶意命令来访问应用程序服务器。
8.9、8.10、8.11和8.12版本中漏洞的CVSS(通用漏洞评分系统)评分为8.4分(CVSS:3.0/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:H)。而8.13版本中相同漏洞的CVSS评分为9.0分,因为在该版本中攻击者完全可以在无需获得管理员权限的情况下利用这个漏洞(CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H)。在所有版本的攻击场景中,GitLab实例需要导入并启用GitLab的输出文件功能。
漏洞分析
GitLab的输出上传功能中存在一个漏洞,这个漏洞将允许攻击者读取GitLab实例中的任意文件,这个漏洞主要是由JSON.parse中的错误操作而导致的,因为JSON.parse中有可能会包含或引用GitLab导出文件的符号链接。当我准备开始对这个功能进行分析之前,我创建了一个演示仓库,并且还通过项目的管理员面板导出了这个GitLab实例。当我们创建了一个新的项目之后,我们就可以直接导入这些GitLab文件了。演示站点为https://gitlab.com/projects/new(点击”GitLab导出”)。通常情况下,一个简单的GitLab导出文件一般包含下列文件:
- export $ ls -lash
- total 48
- 8 -rw-r--r--@ 1 jobert staff 5B Oct 25 19:52 VERSION
- 8 -rw-r--r--@ 1 jobert staff 341B Oct 25 19:53 project.bundle
- 8 lrwxr-xr-x 1 jobert staff 11B Oct 25 20:43 project.json
当我们再次加载之前导出的GitLab文件时,将会发生以下几件事情。首先,系统会等待文件写入磁盘(针对大型仓库而言);其次,系统会根据VERSION文件来检测导入项目的版本;最后,GitLab会根据project.json文件来创建一个新的Project实例。
这里的第一步其实并不重要,所以我们现在直接来看一看第二步中系统所执行的相关代码(Gitlab::ImportExport::VersionChecker,第12-18行):
- def check!
- version = File.open(version_file, &:readline)
- verify_version!(version)
- rescue => e
- shared.error(e)
- false
- end
请各位注意第13行的代码,它将会打开文件并调用readline方法,而这个方法将会返回稳健的第一行数据。第16行代码会捕获系统运行过程中的所有异常,并将异常信息压入errors栈。所有的这些错误都将被发送至前端。接下来,让我们看一看该文件中的第27-31行代码:
- if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
- raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
- else
- true
- end
这也就意味着,如果文件版本不正确的话,系统会返回一个异常,异常信息中会包含这份GitLab导出文件的版本信息。我们解压GitLab的导出文件,用一个符号链接替换其中的VERSION文件,然后再重新进行压缩。tar文件的结构如下所示:
- export $ ls -lash
- 8 lrwxr-xr-x 1 jobert staff 11B Oct 25 20:43 VERSION -> /etc/passwd
- 8 -rw-r--r--@ 1 jobert staff 341B Oct 25 19:53 project.bundle
- 8 lrwxr-xr-x 1 jobert staff 11B Oct 25 20:43 project.json
在创建好了新的GitLab导出文件之后(在导出目录中执行tar -czvf test.tar.gz),我们就可以加载这份新的GitLab文件了。加载成功之后,因为文件存在版本错误,所以系统将会抛出一个异常,而GitLab实例将会返回第一行错误信息:
但是,这种方法只能读取某个文件的第一行数据。这的确很有意思,但这肯定不是我们想要的,我们想要读取文件的完整内容。于是我们继续分析,看看是否能找到读取完整文件的方法。正如我之前所提到的,导入过程中的第三步就是创建一个新的Project实例。此时,下列代码将会被执行(Gitlab::ImportExport::ProjectTreeRestorer,第11-22行):
- def restore
- json = IO.read(@path)
- tree_hash = ActiveSupport::JSON.decode(json)
- project_members = tree_hash.delete('project_members')
- ActiveRecord::Base.no_touching do
- create_relations
- end
- rescue => e
- shared.error(e)
- false
- end
这段代码采用的结构与负责进行版本检测的代码结构非常相似,第13-18行代码可以捕获异常,然后将错误信息压入errors栈。ActiveSupport会使用JSON.parse来解码JSON数据,如果解码失败的话,系统会将待解码的字符串包含在错误信息中一起返回。这也就意味着,如果我们可以让解码器抛出一个异常的话,我们就可以读取稳健的内容了。其实也并不难,先来看看下面给出的这个文件结构:
- export $ ls -lash
- 8 -rw-r--r--@ 1 jobert staff 11B Oct 25 20:43 VERSION
- 8 -rw-r--r--@ 1 jobert staff 341B Oct 25 19:53 project.bundle
- 8 lrwxr-xr-x 1 jobert staff 11B Oct 25 20:43 project.json -> /etc/passwd
在这个例子中,project.json文件是一个指向/etc/passwd的符号链接。第14行代码可以调用IO.read方法来读取文件内容。很明显,/etc/passwd文件中并不包含有效的JSON数据。因此,系统肯定会抛出一个异常,而异常信息中将会包含/etc/passwd文件的内容。使用tar来对文件进行压缩,然后准备上传[演示文件下载-test.tar.gz(F130233)]。文件导入成功之后,我们就可以从错误信息中获取链接文件的内容了:
需要声明的是,这并不是我自己的/etc/passwd文件。下面给出的是gitlab.com中/etc/passwd文件的最后五行数据:
- alejandro:x:1117:1117::/home/alejandro:/bin/bash
- prometheus:x:999:999::/opt/prometheus:/bin/false
- gitlab-monitor:x:998:998::/opt/gitlab-monitor:/bin/false
- postgres:x:116:121:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
- brian:x:1118:1118::/home/brian:/bin/bash
因此,攻击者同样可以利用这种方法来读取GitLab中Rails项目的机密文件。需要注意的是,这个问题也将导致RCE漏洞。除此之外,攻击者甚至还可以通过这个漏洞拿到GitLab的shell,并访问所有的代码仓库。
附件下载
1. F130233: test.tar.gz
2. F130234: Screen_Shot_2016-10-25_at_20.55.36.png
3. F130235: Screen_Shot_2016-10-25_at_19.28.51.png
漏洞修复情况
下面给出的是我们所采用的漏洞修复代码,感兴趣的用户可以自己动手实现一下:
- diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
- index 113895b..ffd1711 100644
- --- a/lib/gitlab/import_export/file_importer.rb
- +++ b/lib/gitlab/import_export/file_importer.rb
- @@ -43,6 +43,14 @@ module Gitlab
- raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result
- + remove_symlinks!
- + end
- +
- + def remove_symlinks!
- + Dir["#{@shared.export_path}/**/*"].each do |path|
- + FileUtils.rm(path) if File.lstat(path).symlink?
- + end
- +
- true
- end
- end
- diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
- index 7cdba88..c551321 100644
- --- a/lib/gitlab/import_export/project_tree_restorer.rb
- +++ b/lib/gitlab/import_export/project_tree_restorer.rb
- @@ -9,8 +9,14 @@ module Gitlab
- end
- def restore
- - json = IO.read(@path)
- - [@tree_hash](/tree_hash) = ActiveSupport::JSON.decode(json)
- + begin
- + json = IO.read(@path)
- + [@tree_hash](/tree_hash) = ActiveSupport::JSON.decode(json)
- + rescue => e
- + Rails.logger.error("Import/Export error: #{e.message}")
- + raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
- + end
- +
- [@project_members](/project_members) = [@tree_hash](/tree_hash).delete('project_members')
- ActiveRecord::Base.no_touching do
- diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
- index fc08082..bd3c3ee 100644
- --- a/lib/gitlab/import_export/version_checker.rb
- +++ b/lib/gitlab/import_export/version_checker.rb
- @@ -24,12 +24,19 @@ module Gitlab
- end
- def verify_version!(version)
- - if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
- + if different_version?(version)
- raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
- else
- true
- end
- end
- +
- + def different_version?(version)
- + Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
- + rescue => e
- + Rails.logger.error("Import/Export error: #{e.message}")
- + raise Gitlab::ImportExport::Error.new('Incorrect VERSION format')
- + end
- end
- end
- end
我们已经发布了针对该漏洞的修复补丁和安全公告,感兴趣的用户可以访问并了解详情。地址:https://about.gitlab.com/2016/11/02/cve-2016-9086-patches/
我们强烈建议使用了上述版本GitLab的用户尽快安装更新补丁。请注意,GitLab 8.9.x版本目前还没有可用的更新补丁。使用了8.9.0-8.9.11版本的用户虽然没有可用的更新补丁,但是可以通过下面给出的解决方案来缓解这个漏洞的影响。
漏洞缓解方案
禁用项目的导入/导出功能
使用管理员账号登录GitLab,然后执行下列操作:
1. 选择“Admin Area”;
2. 点击“Settings”
3. 在“Import Sources”面板中禁用“GitLab export”选项
4. 点击“保存”
验证操作是否成功:
1. 使用浏览器以普通用户身份登录GitLab;
2. 点击“Projects”;
3. 点击“New Project”
4. 输入项目名称;
5. 确保界面中没有显示“GitLab export”选项