深入剖析XSS和HTML、URL、JavaScript編碼(一)

寫這篇文章的由來是讀了《深入理解瀏覽器解析機制和XSS向量編碼》這篇文章,覺得寫得很好,我看了很多遍,但是感覺還是有些混亂,所以就想自己調研並寫一篇更加明白的將XSSHTML、URL、JavaScript編碼說清楚的文章。一方面留給自己,另一方面拋磚引玉,引出潛在的大神出來糾正錯誤,討論更好的理解方法。因爲該話題涉及的篇幅較長,打算拆分成若干篇幅來討論,主要分爲HTML解析、URL解析、JavaScript解析、通過編碼防禦XSS等幾個部分。

XSS的危害不必多說,其中防禦XSS的一個很重要的措施便是輸出編碼,那麼如何編碼才能保證不會出現XSS,其實和上下文(context)有着緊密的關係。我理解的上下文就是環境的概念,處在什麼環境會發生解碼,什麼環境腳本纔會執行,之前我一直不是很清楚,後面查了大量資料,在這個專題中就來仔細談一談。

HTML解析

XSS在HTML中的執行環境

爲了搞清楚XSS和HTML解析的關係,我查看了部分HTML5標準文檔,並畫了一些圖輔助理解。首先我們談一下有哪些機制可以使攻擊者提供的可執行代碼在文檔的上下文中運行,HTML5的Scripting部分提到,包括但不限於以下:

  1. <script>標籤
  2. javascript: URLs
  3. 事件處理Event handlers
  4. 自我腳本技術SVG

所以要想執行XSS腳本,必須滿足以上一種執行環境,這篇文章我們只要談第一種,<script>標籤的情況。

HTML的元素

HTML元素(element)在語法上由標籤(tag)表示的,通過標籤才能識別出元素的種類。其實標籤和後面要談的狀態聯繫緊密,所以先看看HTML中通過標籤是如何將元素分類的。下圖是HTML5中的分類,主要爲6種元素,空元素、原始文本元素,RCDATA元素、外部元素、基本元素和模板元素,如下圖:

圖片描述

上圖中的第三種元素叫做RCDATA,它的定義如下:

The term RCDATA elements refers to elements within which character references are supported, but all other content is treated as raw text instead of markup.

意思是支持字符引用(character reference,下面我們談字符引用),其他類型的內容被視爲文本的元素,RCDATA包括<title>和<textarea>內的元素,根據字面意思理解,支持字符引用應該就是可以轉義編碼的意思。

第二種元素叫做原始文本元素(Raw text element),也就是純文本的元素,這意味着其他內容(如註釋,字符引用和其他元素)無法在HTML語法中表示,也就是不能轉義編碼後的文本。原始文本元素包括<script>和<style>內的元素,看下面的代碼

<script>
function isLeap(year) {
    return year % 4 == 0 &amp;&amp; year % 100 != 0 || year % 400 == 0;
}
console.log(isLeap(2019))
</script>

這個是執行不了的,$amp;在<script>標籤中不能被轉義爲&,<script>標籤內的是純文本元素。現在我們還不清楚,轉義了是否就可以執行,其實轉義和執行是兩碼事,後面我們會仔細剖析,我們先看看字符引用的定義。

字符引用(Character Reference)

字符引用主要是通過轉義(escape)來表示文本無法正常表達的字符,例如分數 ½ 的轉義是&#x00BD;字符引用必須以U+0026(&)開頭;在防範XSS中是爲了轉義一些危險的字符,字符引用有以下三種表達類型:

  1. 命名字符引用(Named character references),以&開頭,加上name,參考這裏,例如&lt;,以U+003B(;)結束,這個也叫作HTML實體(HTML Entity);
  2. 10進制數字字符引用(Decimal numeric character reference),格式爲&#後面跟一個或更多0-9的字符,以U+003B(;)結束;
  3. 16進制數字字符引用(Hexadecimal numeric character reference),格式爲&#x(或者&#X)後面跟一個或更多0-9A-Fa-f的字符,以U+003B(;)結束;

字符引用和HTML元素

上面那個圖其實談到一些字符引用和HTML元素的關係,這裏我們再詳細探討一下字符引用在哪些元素中可以被轉義。上面我們已經談過了原始文本是無法轉義字符引用的,RCDATA是可以轉義字符引用的,如下:

<textarea>&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;</textarea>

可以在瀏覽器中的調試元素中看到渲染之後,RCDATA已經變爲了<script>alert(1)</script>,但是轉義之後變爲原始文本元素,無法執行。外部元素和模板元素我們以後再談,然後談基本元素。基本元素定義如下,來自HTML5參考draft版

Normal elements can contain text, character references, other elements, and comments, but the text must not contain the less-than sign character (<) or an ambiguous ampersand.

從定義可以看出基本元素中不能包含<和歧義的&,可以包含文本、字符引用、註釋等其他元素,字面上理解這裏的字符引用是可以被轉義的,如下:

<h1>&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;</h1>

這個也是可以轉義出來的,上面討論了這麼多,發現RCDATA基本元素都是可以轉義字符引用的,那麼滿足什麼條件纔會切換到執行腳本的環境呢,這個需要了解HTML的解析。

HTML解析與XSS執行

XSS的執行環境其實和一個叫做狀態機的概念息息相關的,狀態機必須以data state開始,我們看看HTML是如何分詞的,引用自HTML5標準:

Most states consume a single character, which may have various side-effects, and either switches the state machine to a new state to reconsume the current input character, or switches it to a new state to consume the next character, or stays in the same state to consume the next character. Some states have more complicated behavior and can consume several characters before switching to another state.

這段話的意思是狀態機消耗一個字符會產生多種可能的影響:

  1. 切換到一個新的狀態,重新消耗現在的字符;
  2. 切換到一個新的狀態,消耗下一個字符;
  3. 維持狀態不變,消耗下一個字符;
  4. 切換到一個新的狀態,消耗多個字符;

我們主要用到的是其中的1、2,絕大多數是第2種情況,情況其實這種狀態的改變和字符的消耗就是在解析輸入的字符流,輸出構建Document tree,正好就是下圖中的tokenizer的過程:
parsing-model-overview.png
可以看到輸入字符流不僅來自網絡的返回,還來自腳本的執行,也就是說腳本是可以改變DOM樹的。HTML解析比較抽象,下面這個圖可能看起來更簡單一點,我把它貼出來了,來自這裏
browser_parse_html.png
其實我的理解是,HTML解析器一邊解析比特流(Bytes->Characters->tokens),一邊開始構建DOM tree,把node壓棧,<html>是第一個被壓棧的node,直到解析完畢纔會彈出。在解析的過程中會出現釋放token的情況,某個token被釋放之後,必須立即被tree構建狀態處理,因爲tree構建會影響解析的狀態,舉個例子:如果token是<script>被釋放(emit)的話,tree構建狀態會立馬將環境切換到一種叫做script data state的狀態,這個就是腳本執行狀態,後面會仔細說。

狀態(state)

之前談到轉義和執行關係不大,而執行和解析狀態有着直接關係,我們先看看狀態。其實HTML狀態機中的狀態數目非常多,我這裏只列舉一些,包括Data state、RCDATA state(RCDATA狀態)、RAWTEXT state、Script data state(腳本數據狀態)、PLAINTEXT state、Tag open state、End tag open state、Tag name state等,有大概80個,詳細狀態查看這裏。關於轉義,就涉及字符引用,那麼看看字符引用是怎麼被轉義的,而且搞清楚字符引用和執行到底有沒有關係。關於狀態,牆裂建議看一看HTML5標準中的tokenization這一節,否則理解起來有些困難。

字符引用狀態

根據HTML5標準,我們分析一下轉義中的狀態轉移,假設的例子是

<...>
&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;
<...>

例子是<script>alert(1)</script>轉義後的結果,放置在html中,從data state開始輸入stream處理,如下圖

clipboard.png

初始狀態是data state(數據狀態),第一個字符是&,消耗&進入character reference state(字符引用狀態)並且設置好返回狀態爲data state用作後面使用;然後根據上面的代碼是#,消耗#,進入numeric character reference state(數字字符引用狀態),同理,消耗x,進入hexadecimal character reference start state(16進制字符引用開始狀態),消耗3C,進入hexadecimal character reference state(16進制字符引用狀態),這裏不是很重要,略去了複雜度,其實是將字符3C轉爲十進制的數字後放入一個叫做character reference code中去;然後消耗;,進入numeric character reference end state(數字字符引用結束狀態),後面檢查,character reference code放入buffer,釋放(emit)buffer中的字符token,並返回最先設置的返回狀態,即data state。重複後面的字符引用&#x73;&#x63;...&#x3E;直到結束,都沒有將script作爲一個整體token釋放,只是將字符引用一個一個解析後一個token一個token釋放。這就是爲什麼轉義了之後,沒有執行環境的原因。

簡單總結一下,當<script>沒有做編碼時,會將script整體作爲一個token釋放,然後就會進入script data state狀態;但是如果編碼後,就會將script看成文本,拆成一個一個字符釋放(轉義後爲s作爲token釋放,又進入字符引用狀態,繼續轉義後爲c作爲token釋放,......,最後是t),所以到最後也沒進入script data state狀態,也就是沒有執行環境。

我們終於知道了<script>標籤轉義後是進入不了執行環境的,那麼如何進入<script>執行環境呢?我們可以分析一下,如果需要script data state,就需要<script>標籤,進而需要Tag open state狀態,HTML5唯一進入Tag open state狀態的是Data state消耗<字符,所以條件就是Data state加沒有轉義的<script>標籤。邏輯如下圖:
圖片描述
當然除了script data state,通過釋放標籤還會進入其他狀態,如下:

  1. 進入RCDATA state,通過釋放<title>、<textarea>標籤;
  2. 進入RAWTEXT state,通過釋放<style>、<xml>、<iframe>、<noembed>、<noframes>標籤;
  3. 進入script data state,通過釋放<script>標籤;
  4. 如果scripting flag啓用,<noscript>會切換到RAWTEXT state狀態 ,否則會進入data state
  5. 進入PLAINTEXT state,通過釋放<plaintext>標籤;
  6. 其他標籤的釋放進入data state

這個和上面的HTML元素有些類似,可能HTML元素的分類是借鑑了這裏吧。

進入<script>執行環境

上面說了HTML5唯一進入<script>執行環境的是Data state加<script>標籤,所以要考慮所有進入Dara state狀態的方式。我這裏大致總結了一下,進入Data state的方式有以下情況:

  1. Tag open state,參考HTML5文檔,除了! / ?和字母,anything else都被視爲parse error,釋放<,進入data state,所以下面這個是可以執行的,感興趣的可以試一下<12561,.';\][22<script>alert(1)</script>;
  2. End tag open state,消耗>會被視爲parse error,進入data state,所以</><script>alert(1)</script>也是可以執行的;
  3. Tag name state,消耗>,釋放標籤token(當然這裏的標籤是前面剛討論的進入data state,不是<script>這類的特殊標籤),進入data state;

還有很多,我畫了一個圖,描繪了主要的進入data state的狀態:
圖片描述
所以遵循HTML狀態的轉移過程,無論多麼複雜和奇怪的內容也可以判斷出後面的<script>腳本塊是否可以執行。理解了這個我們再返回來看看哪些標籤可以轉義字符引用,也就是進入字符引用狀態(character reference state):

  1. data state:消耗&,設置return state爲data state,然後切換到character reference state;
  2. RCDATA state:消耗&,設置return state爲RCDATA state,然後切換到character reference state;
  3. Attribute value (double-quoted) state:消耗&,設置return state爲attribute value (double-quoted) state,然後切換到character reference state;
  4. Attribute value (single-quoted) state:消耗&,設置return state爲attribute value (single-quoted) state,然後切換到character reference state;
  5. Attribute value (unquoted) state:消耗&,設置return state爲attribute value (unquoted) state,然後切換到character reference state;

所以能轉義字符引用的就三種data stateRCDATA state還有attribute value state(單引號、雙引號或無引號)。我們下面測試一下,data state就不說了,方式太多了:

<!--1. RCDATA state的<textarea>標籤,編碼部分是<script>alert(1)</script>-->
<textarea>&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;</textarea>
<!--2. 屬性狀態(無引號),編碼部分爲><script>alert(1)</script>-->
<div id=1 name=&#62;&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;>1</div>
<!--3. 屬性狀態(雙引號),編碼部分爲"><script>alert(1)</script>-->
<div id=2 name="&#34;&#62;&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;">2</div>

測試結果是上面3個都可以轉義字符引用,但是都是一個一個字符轉義後釋放,沒有將script整體作爲一個token釋放,這裏就不多說了。其實今天我們只談論了一種<script>腳本執行環境的情況,但是背後卻有這麼多的backgroud做支撐,所以XSS的姿勢不是隨便蒙的,理解了原理纔是真的瞭解爲什麼XSS在這裏執行了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章