密碼裡面不能有 SELECT/INSERT/UPDATE/DELETE/DROP

這是在 Hacker News 上看到的:「Password may not contain: select, insert, update, delete, drop (uni-lj.si)」,原網站在「Password reset - ID portal」,熱鬧的地方在於原作者 (或是外包商?) 也在 Hacker News 上面回應...

禁止密碼裡面有某些字元還蠻常見的,但這次看到的很有趣 (然後被貼到 Hacker News 上):

Your password must also not contain the following character combinations: script, select, insert, update, delete, drop, --, ', /*, */.

從網域及英文版的介面可以查到這是盧比安納大學的系統,作者 (或是外包) 在 id=39079030 提到了這是上面的要求:

Oooh! I put that string there! It was a request by management, and I still don't know why. This site doesn't store any passwords, it's basically just a nice interface to external account management.

I heard a rumour that some legacy apps have weird validation on their login fields, so students wouldn't be able to log in with passwords containing certain strings. But I don't actually know of any examples.

就... 很好玩?

jQuery 3.5.0 的修正,補 XSS 漏洞

這次 jQuery 3.5.0 修正了一個安全性漏洞:「jQuery 3.5.0 Released!」,不過實測了一下,不完全算是 jQuery 的自己出的問題,比較像是幫使用者擦屁股。

jQuery 的說明如果看不懂,可以交叉參考「XSS Game」這篇的說明,搜尋 htmlPrefilter 應該就可以看到了。

這次修正的函數是被 html() 用到的 htmlPrefilter(),這個函數會在 3.5.0 之前會把 <tag/> 這樣的元素轉成 <tag></tag>,而 3.5.0 之後會保留本來的形式。

利用這個特性,就可以用這樣的字串來打穿 WAF:

<style><style/><script>alert(1)<\/script>

原因是 WAF 在看到時會以為 <script><style> 內部的 data 而認為不是 XSS 攻擊而穿過 WAF 的檢查,但實際上被 jQuery 展開後變成:

<style><style></style><script>alert(1)<\/script>

而最前面的 <style><style></style> 被當作是一包,後面的 <script> 就成功被拉出來執行了。

這是相同程式碼使用 jQuery 3.4.1 與 jQuery 3.5.0 的差異,我把 alert(1) 改成 document.write(1),比較容易在 JSFiddle 上看出差異:

不過回過來分析,會發現一開始用 html() 才是問題的起點,要修正問題應該要從這邊下手,而不是用 WAF 擋...

相對路徑的攻擊方式 (Relative Path Overwite,RPO)

在「Large-scale analysis of style injection by relative path overwrite」這邊看到的,記得這個方式不是新方法,不過還是有人會中...

這種攻擊是組合技,基礎是引用 css 或是 js 時使用相對路徑 (像是 static/style.css 這樣的引用法),再加上 https://www.example.com/a.php 這樣的頁面通常也可以吃 https://www.example.com/a.php/,甚至是後面再加東西... 在某些情境下組不出來,但精心策劃後就有機會在頁面上弄出奇怪的 xss 或是其他攻擊了。而論文內列出了常見的的組合:

然後拿 Alexa 的排名來看,其實還是有些站台可以打:

防禦的方式也不算太難,absolute path 是個還不錯的方式:

One option is to use only absolute URLs, taking away the relative path expansion.

base tag 也是個方式 (不過在 IE 上還是有問題):

Alternatively you can specify a base tag, though Internet Explorer did not appear to implement the tag correctly (i.e., was still vulnerable) at the time of the evaluation.

另外作者也提到了 document type 的方式 (看起來是建議用 html5 的 <!DOCTYPE html>),然後 IE 另外做些處理避免失效:

One of the best mitigations is to avoid exploitation by declaring a modern document type that causes rendering in standards compliant mode. This defeats the attack in all browsers apart from IE. For IE it is also necessary to prevent the page being loaded in a frame by using X-Frame-Options , using X-Content-Type-Options to disable ‘content type sniffing,’ and X-UA-Compatible to turn off IE’s compatibility view.

不過大型站台本來就因為業務需求,會把 asset domain 切開 (然後透過 CDN 加速),而且會設計系統讓 programmer 很容易使用這樣的架構,反而因此比較不會用到 relative path,中這個攻擊的機會就低多了...

利用上傳的檔案跳過 CSP 限制

CSP 可以做到一些簡單的保護機制,但在設計不良的情況下還是有辦法繞過。

這次是上傳合法的 JPEG 檔案,但當作 javascript 檔案繞過去:「Bypassing CSP using polyglot JPEGs」。

開頭的「FF D8 FF E0」可以在「List of file signatures」這邊看到是「JPEG raw or in the JFIF or Exif file format」,而這四個字元在 javascript 不會出問題。接下來的「2F 2A」表示 JPEG header 長度,剛好就是「/*」,把後面的東西給包起來,後面再用類似的方式一直組合就打穿了...

這種攻擊要跳過的是「用 CSP 的 self 限制不能引用外部網站 javascript」的限制,但還是有些前提:

  • 允許使用者傳到同一個 domain 上面。
  • 網站上有 XSS 漏洞。

其中第一個問題常見的解法是另外開一個 domain 來放使用者上傳的檔案 (最好是連 top domain 都不一樣,完全隔開),才可以透過 CSP 降低風險...

Flash 的 crossdomain.xml 架構問題

在「"Lax" Crossdomain Policy Puts Yahoo Mail At Risk」這篇裡面看到不安全的 Flash 造成的問題:「Seizing Control of Yahoo! Mail Cross-Origin... Again」。

找有問題的 swf 檔案 (hosting 在 crossdomain.xml 允許的網段下),然後利用 injection 或是根本就沒檢查權限來打趴... 把 swf 當跳板用就是了 :p

文章後面那個 Disclosure Timeline 看起來頗心酸 :o

XSScrapy:自動化 XSS 攻擊

也是不知道在哪邊看到的,反正容易安裝就裝起來玩玩看:「XSScrapy: fast, thorough XSS vulnerability spider」。XSScrapy 是一套自動化攻擊軟體,是目前這類測試軟體裡用起來最簡單的版本。

直接 clone 下來就可以執行了,如果有遇到 dependency 的問題,可以透過 pip 安裝:

pip install -r requirements.txt

執行的方法就這樣:

./xsscrapy.py -u http://example.com

上面這個方法是在訪客模式下慢慢跑,你也可以給他一組帳號密碼讓他填 form,他會試著登入後不斷打下去:

./xsscrapy.py -u http://example.com/login_page -l loginname -p pa$$word

也可以用 HTTP Basic Authentication:

./xsscrapy.py -u http://example.com/login_page -l loginname -p pa$$word --basic

然後發現的問題會寫到 XSS-vulnerable.txt 裡面:

XSS vulnerabilities are reported in XSS-vulnerable.txt

用法就這樣而已... XD

在 HTML 內嵌 JSON object 時要注意的事情...

有時候我們會因為效能問題,在 HTML 內嵌入 JSON object,而不是再多一個 HTTP request 取得。

但「嵌入」的行為如果沒有處理好,就產生非常多 XSS attack vector 可以玩。

首先最常犯的錯誤是使用錯誤的 escape function:

<!DOCTYPE HTML>
<html>
<body>
<script>
var a = "<?= addslashes($str) ?>";
</script>
</body>
</html>

這樣可以用 </script><script>alert(1);// 攻擊 $str。因為 addslashes() 並不會過濾到這個字串,而產生這樣的 HTML:

<!DOCTYPE HTML>
<html>
<body>
<script>
var a = "</script><script>alert(1);//";
</script>
</body>
</html>

而這個字串會造成 DOM parser 解讀上產生不是我們預期的行為:

可以看到在字串裡面的 </script> 被拆開了。

這是因為瀏覽器會先拆解產生 DOM tree,再把 <script></script> 內的程式碼交給 JavaScript engine 處理。所以在一開始產生 DOM tree 的時候,是看不懂 JavaScript 程式邏輯的...

正確的方法是用 json_encode() 處理,因為 PHPjson_encode() 預設會把 / (slash) 變成 \/ (這是 JSON spec 裡合法的轉換):

<!DOCTYPE HTML>
<html>
<body>
<script>
var a = <?= json_encode($str) ?>;
</script>
</body>
</html>

這會產生出:

<!DOCTYPE HTML>
<html>
<body>
<script>
var a = "<\/script><script>alert(1);//";
</script>
</body>
</html>

但上面這段 HTML 與 PHP code 仍然有問題,如果 $str<!--<script 時,你會發現 DOM 又爛掉了:

<!DOCTYPE HTML>
<html>
<body>
<script>
var a = "<!--<script>";
</script>
</body>
</html>

escape.alf.nu 的 Level 15 就是利用這個問題,再加上其他的漏洞而完成 XSS 攻擊。

為了這個問題去 StackOverflow 上問:「Why does <!--<script> cause a DOM tree break on the browser?」,才又發現上面這段 code 並不是合法的 HTML5 (先不管 head & title 的部份,補上後仍然不是合法的 HTML5)。

原因在於 DOM parser 對 <script></script> 的特殊處理:「4.3.1.2 Restrictions for contents of script elements」。(話說這段 ABNF 差點讓我翻桌...)

解法是在 <script></script> 的開頭與結尾加上 HTML 註解:(這剛好是 HTML 4.01 建議的方法)

<!DOCTYPE HTML>
<html>
<body>
<script>
<!--
var a = "<!--<script>";
-->
</script>
</body>
</html>

那段 ABNF 的目的是希望可以盡可能往後找到 --></script> 結尾的地方。

當然你也可以用 json_encode()JSON_HEX_TAG<> 硬轉成 \u003c\u003e 避開這個問題,但這使得呼叫 json_encode() 時要多一個參數 (而非預設參數),用起來比較卡...

這個問題會變得這麼討厭,是因為 DOM parser 與 JavaScript 語法之間有各自的處理方式,然後又有些 pattern 是之前的 spec 遺留下來的包袱 (像是 HTML 4.01 在「18.3.2 Hiding script data from user agents」裡有提到用 <!----> 包裝 <script></script>),變成在設計 HTML5 時都要考慮進去相容...

之前會習慣用 <!--//--> 包裝 <script></script> 倒不是這個原因,而是因為不這樣做的話,jQuery 在 IE 使用 html() 時遇到有 <script></script> 的字串會爛掉,所以後來寫的時候變成習慣了...

反而因為這個習慣而避開了這個問題...

超難搞啊...

Filter Input & Escape Output...

維基百科上有一篇「Secure input and output handling」說明要怎麼處理 input 與 output (對資安方面的說明)。標題的 Filter Input 與 Escape Output 是 Gasol 之前提到後才知道的名詞,以前只知道要 validate & escape,沒想過一個比較好記的念法...

這邊都以 PHP 為主,其他程式語言也應該會有對應的方式...

Input 有很多管道,有可能是使用者或是 3rd party 廠商透過 Form 傳進來的資料 (在 PHP 裡可能是 $_GET 或是 $_POST),也有可能是 cookie 的資料 (因為使用者可以修改,所以視為不安全的資料)。從檔案讀資料進來 (可能是普通的文字檔,或是 XML,也可能是圖片) 也算是 input。

Output 也有很多管道,像是 HTML、JSON,或是組 SQL statement 時使用變數。

Filter Input

在處理 input data 時,一般常常忘記的是「先強制轉成 Non-null UTF-8 string」(現在一般都是用 UTF-8,所以這邊就只講 UTF-8)。

這是因為很多 PHP function 對非 Non-null UTF-8 string 有非定義行為 (undefined behavior,不保證效果與輸出結果的正確性),加上一般常見的產品需求可以用 Non-null UTF-8 string 滿足,所以在 PHP 內拿到資料時可以先 filter 過。

Update:下面說的步驟錯了,請參考「關於 Non-null string 的處理...」這篇的說明。

有兩個步驟要做,第一個是確保他是 Non-null,直接把 \0 以及以後的東西幹掉:

$str_out = preg_replace('/\0.*/g', '', $str_in);
$str = preg_replace('/\0.*/g', '', $str); // 直接取代原來字串

第二個是確保他是 UTF-8 string。這點可以直接用 iconv() 轉,而不用自己寫 regex 處理了:

$str_out = iconv('UTF-8', 'UTF-8', $str_in);
$str = iconv('UTF-8', 'UTF-8', $str);

iconv() 從 UTF-8 轉到 UTF-8 是一個特別的用法,我是在 Cal Henderson 的「Building Scalable Web Sites: Building, Scaling, and Optimizing the Next Generation of Web Applications」上看到的 (有中譯版)。

如果傳進來的值本來就假設是整數,那麼就用 intval() 轉一次。如果假設是非負整數 (包含 0),那麼就用 abs()intval()。如果是文字類的 (像是 e-mail),可以再用其他的 regex 檢查。

一般來說,白名單會比黑名單好。不過這也是一般性,很多時候還是很囧的...

Escape Output

Escape 指的是 Escape character (轉義字元)。不同的情況下會有不同的 escape character,所以保護的方式也不一樣。

以 HTML 來說,想要顯示小於符號 <,實際上要用 &lt; 表示。而在 PHP 裡面常用的 htmlspecialchars() 定義了「只 escape 五個符號」,剛好可以拿來用:

<div><?= htmlspecialchars($str) ?></div>

也有人推薦 htmlentities(),不過因為轉的比較多 (所以要考慮的行為比較多),我比較不喜歡...

對於 XML,在 XML 的規範裡規定一定要把 <& 換成 &lt;&amp; (Character Data and Markup),但 > 則可以 (非必要) 換成 &gt;。也允許把雙引號 " 換成 &quot;,以及單引號 ' 換成 &apos;

所以 XML 剛好可以直接用 htmlspecialchars() (確認完全符合 spec 要求),反過來 htmlentities() 就不保證了。

再來是 MySQL 的 escape 與 charset 有關,所以要用 mysql_real_escape_string() 或是 PDO::quote(),但更好的方法應該是使用 prepare & execute (binding variables)...

而 shell 的 escape 則應該用 escapeshellarg() 再帶入 string 裡。

每一種 output 所需要的 escape function 不同,都不能直接拿 addslashes() 來用... (這個 function 的設計就很 PHP...)

純粹 JSON 的話,用 json_encode() 就可以幫你處理好。問題是在 HTML + JSON 時會讓你處理到抱頭痛哭... 這就是另外的故事了 +_+

結論?

Filter Input + Escape Output 只是最基本的一步,不過在大多數的狀況下,這個方法就可以擋下不少問題了,算是很實用的 policy...