(譯者注:由於某些詞匯翻譯成中文后很生硬,因此把相應的英文標注在其后以便理解。這篇文章講的內容很基礎,同時也很重要,希望對大家有所幫助。)
這篇文章將要深入理解HTML、URL和JavaScript的規范細則和解析器,以及在解析一段XSS腳本時他們之間有着怎樣的差別。這些內容對讀者的難易程度取決於讀者對HTML規范和瀏覽器解析的知識是否充足。當然,我向您保證這篇文章比較長,因此請准備一小時或兩小時來從中獲益。在主題開始之前,請花費一點時間來看看下列語句並嘗試回答:這些腳本能夠正確執行嗎?
基礎部分
1
2
3
4
5
6
7
8
9
10
11
|
<
a
href
=
"%6a%61%76%61%73%63%72%69%70%74:%61%6c%65%72%74%28%31%29"
></
a
>
URL 編碼 "javascript:alert(1)"
<
a
href
=
"javascript:%61%6c%65%72%74%28%32%29"
>
HTML字符實體編碼 "javascript" 和 URL 編碼 "alert(2)"
<
a
href
=
"javascript%3aalert(3)"
></
a
>
URL 編碼 ":"
<
div
><img src=x onerror=alert(4)></
div
>
HTML字符實體編碼 < 和 >
<
textarea
><script>alert(5)</script></
textarea
>
HTML字符實體編碼 < 和 >
<
textarea
><
script
>alert(6)</
script
></
textarea
>
|
高級部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<
button
onclick
=
"confirm('7');"
>Button</
button
>
HTML字符實體編碼 " ' " (單引號)
<
button
onclick
=
"confirm('8\u0027);"
>Button</
button
>
Unicode編碼 " ' " (單引號)
<
script
>alert(9);</
script
>
HTML字符實體編碼 alert(9);
<
script
>\u0061\u006c\u0065\u0072\u0074(10);</
script
>
Unicode 編碼 alert
<
script
>\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0031\u0029</
script
>
Unicode 編碼 alert(11)
<
script
>\u0061\u006c\u0065\u0072\u0074(\u0031\u0032)</
script
>
Unicode 編碼 alert 和 12
<
script
>alert('13\u0027)</
script
>
Unicode 編碼 " ' " (單引號)
<
script
>alert('14\u000a')</
script
>
Unicode 編碼換行符(0x0A)
|
額外贈送
1
|
<
a
href
=
"javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(15)"
></
a
>
|
這些問題的答案在這里,測試頁面在這里。如果你能答對大部分問題並且沒有感覺困惑,看到這就停下吧。如果沒有,那么余下的內容將能夠很好的幫助你理解瀏覽器解析過程。
在解析一篇HTML文檔時主要有三個處理過程:HTML解析,URL解析和JavaScript解析。每個解析器負責解碼和解析HTML文檔中它所對應的部分,其工作原理已經在相應的解析器規范中明確寫明。
0x01 HTML解析
從XSS的角度來說,我們感興趣的是HTML文檔是如何被詞法解析的,因為我們並不想讓用戶提供的數據最終被解析為一段可執行腳本的script標簽。HTML詞法解析細則在這里。HTML詞法解析細則是一篇冗長的文檔,這篇博文並不會覆蓋它的所有內容。這篇博文只會覆蓋有關文檔解碼如何結束,以及新token何時被創建這兩個有趣的部分。
一個HTML解析器作為一個狀態機,它從輸入流中獲取字符並按照轉換規則轉換到另一種狀態。在解析過程中,任何時候它只要遇到一個'<'符號(后面沒有跟'/'符號)就會進入“標簽開始狀態(Tag open state)”。然后轉變到“標簽名狀態(Tag name state)”,“前屬性名狀態(before attribute name state)”......最后進入“數據狀態(Data state)”並釋放當前標簽的token。當解析器處於“數據狀態(Data state)”時,它會繼續解析,每當發現一個完整的標簽,就會釋放出一個token。
(譯者注:詞法解析是《編譯原理》所涉及的內容,學習過編譯原理的讀者可以更好的理解“狀態機”的工作原理)。
這里有三種情況可以容納字符實體,“數據狀態中的字符引用”,“RCDATA狀態中的字符引用”和“屬性值狀態中的字符引用”。在這些狀態中HTML字符實體將會從“&#...”形式解碼,對應的解碼字符會被放入數據緩沖區中。例如,在問題4中,“<”和“>”字符被編碼為“<”和“>”。當解析器解析完“<div>”並處於“數據狀態”時,這兩個字符將會被解析。當解析器遇到“&”字符,它會知道這是“數據狀態的字符引用”,因此會消耗一個字符引用(例如“<”)並釋放出對應字符的token。在這個例子中,對應字符指的是“<”和“>”。讀者可能會想:這是不是意味着“<”和“>”的token將會被理解為標簽的開始和結束,然后其中的腳本會被執行?答案是腳本並不會被執行。原因是解析器在解析這個字符引用后不會轉換到“標簽開始狀態”。正因為如此,就不會建立新標簽。因此,我們能夠利用字符實體編碼這個行為來轉義用戶輸入的數據從而確保用戶輸入的數據只能被解析成“數據”。
(譯者注:這里要解釋幾個概念)
字符實體(character entities)
字符實體是一個轉義序列,它定義了一般無法在文本內容中輸入的單個字符或符號。一個字符實體以一個&符號開始,后面跟着一個預定義的實體的名稱,或是一個#符號以及字符的十進制數字。
HTML字符實體(HTML character entities)
在HTML中,某些字符是預留的。例如在HTML中不能使用“<”或“>”,這是因為瀏覽器可能誤認為它們是標簽的開始或結束。如果希望正確地顯示預留字符,就需要在HTML中使用對應的字符實體。一個HTML字符實體描述如下:
需要注意的是,某些字符沒有實體名稱,但可以有實體編號。
字符引用(character references)
字符引用包括“字符值引用”和“字符實體引用”。在上述HTML例子中,'<'對應的字符值引用為'<',對應的字符實體引用為‘<’。字符實體引用也被叫做“實體引用”或“實體”。)
現在你大概會明白為什么我們要轉義“<”、“>”、“'” (單引號)和“"” (雙引號)字符了。但為什么我們還要轉義“&”呢?大概 “&” 是無辜的,任何跟在“&”后面的內容僅會被解釋為字符引用,這並不會開始或閉合一個標簽。事實上,“&”字符並不會打斷HTML級別的轉義過程,但它可能會打斷其他級別的轉義過程。我們將在JavaScript解析的部分討論這個問題。
這里要提一下RCDATA的概念。要了解什么是RCDATA,我們先要了解另一個概念。在HTML中有五類元素:
1. 空元素(Void elements),如<area>,<br>,<base>等等
2. 原始文本元素(Raw text elements),有<script>和<style>
3. RCDATA元素(RCDATA elements),有<textarea>和<title>
4. 外部元素(Foreign elements),例如MathML命名空間或者SVG命名空間的元素
5. 基本元素(Normal elements),即除了以上4種元素以外的元素
五類元素的區別如下:
1. 空元素,不能容納任何內容(因為它們沒有閉合標簽,沒有內容能夠放在開始標簽和閉合標簽中間)。
2. 原始文本元素,可以容納文本。
3. RCDATA元素,可以容納文本和字符引用。
4. 外部元素,可以容納文本、字符引用、CDATA段、其他元素和注釋
5. 基本元素,可以容納文本、字符引用、其他元素和注釋
如果我們回頭看HTML解析器的規則,其中有一種可以容納字符引用的情況是“RCDATA狀態中的字符引用”。這意味着在<textarea>和<title>標簽中的字符引用會被HTML解析器解碼。這里要再提醒一次,在解析這些字符引用的過程中不會進入“標簽開始狀態”。這樣就可以解釋問題5了。另外,對RCDATA有個特殊的情況。在瀏覽器解析RCDATA元素的過程中,解析器會進入“RCDATA狀態”。在這個狀態中,如果遇到“<”字符,它會轉換到“RCDATA小於號狀態”。如果“<”字符后沒有緊跟着“/”和對應的標簽名,解析器會轉換回“RCDATA狀態”。這意味着在RCDATA元素標簽的內容中(例如<textarea>或<title>的內容中),唯一能夠被解析器認做是標簽的就是“</textarea>”或者“</title>”。當然,這要看開始標簽是哪一個。因此,在“<textarea>”和“<title>”的內容中不會創建標簽,就不會有腳本能夠執行。這也就解釋了為什么問題6中的腳本不會被執行。
我們來迅速看一下CDATA元素。任何在CDATA元素中的內容將不會觸發解析器創建開始標簽。閉合CDATA元素的標志是“]]>”序列。因此如果用戶想逃出CDATA元素,就要用未經任何編碼的“]]>”序列,不然是不會逃出CDATA元素的。
0x02 URL解析
URL解析器也是一個狀態機模型,從輸入流中進來的字符可以引導URL解析器轉換到不同的狀態。解析器的解析細則在這里。其中有很多有關安全或XSS轉義的內容。
首先,URL資源類型必須是ASCII字母(U+0041-U+005A || U+0061-U+007A),不然就會進入“無類型”狀態。例如,你不能對協議類型進行任何的編碼操作,不然URL解析器會認為它無類型。這就是為什么問題1中的代碼不能被執行。因為URL中被編碼的“javascript”沒有被解碼,因此不會被URL解析器識別。該原則對協議后面的“:”(冒號)同樣適用,即問題3也得到解答。然而,你可能會想到:為什么問題2中的腳本被執行了呢?如果你記得我們在HTML解析部分討論的內容的話,是否還記得有一個情況叫做“屬性值中的字符引用”,在這個情況中字符引用會被解碼。我們將稍后討論解析順序,但在這里,HTML解析器解析了文檔,創建了標簽token,並且對href屬性里的字符實體進行了解碼。然后,當HTML解析器工作完成后,URL解析器開始解析href屬性值里的鏈接。在這時,“javascript”協議已經被解碼,它能夠被URL解析器正確識別。然后URL解析器繼續解析鏈接剩下的部分。由於是“javascript”協議,JavaScript解析器開始工作並執行這段代碼,這就是為什么問題2中的代碼能夠被執行。
其次,URL編碼過程使用UTF-8編碼類型來編碼每一個字符。如果你嘗試着將URL鏈接做了其他編碼類型的編碼,URL解析器就可能不會正確識別。
0x03 JavaScript 解析
JavaScript解析過程與HTML解析過程有點不一樣。JavaScript語言是一門內容無關語言。對應着有一份內容無關的語法來描述它。我們可以利用內容無關語法來解釋JavaScript是如何解析的。ECMAScript-262細則在這里,語法文件在這里。
這里有一些與安全相關的事情:字符是如何被解碼的?對一些字符進行轉義是否有效?
開始之前,讓我們來回到HTML解析過程中的“原始文本”元素。我故意將HTML中的一部分留到這個章節是因為它與JavaScript解析有關。所有的“script”塊都屬於“原始文本”元素。“script”塊有個有趣的屬性:在塊中的字符引用並不會被解析和解碼。如果你去看“腳本數據狀態”的狀態轉換規則,就會發現沒有任何規則能轉移到字符引用狀態。這意味着什么?這意味着問題9中的腳本並不會執行。所以如果攻擊者嘗試着將輸入數據編碼成字符實體並將其放在script塊中,它將不會被執行。
那像“\uXXXX”(例如\u0000,\u000A)這樣的字符呢,JavaScript會解析這些字符來執行嗎?簡單的說:視情況而定。具體的說就是要看被編碼的序列到底是哪部分。首先,像\uXXXX一樣的字符被稱作Unicode轉義序列。從上下文來看,你可以將轉義序列放在3個部分:字符串中,標識符名稱中和控制字符中。
字符串中:當Unicode轉義序列存在於字符串中時,它只會被解釋為正規字符,而不是單引號,雙引號或者換行符這些能夠打破字符串上下文的字符。這項內容清楚地寫在ECMAScript中。因此,Unicode轉義序列將永遠不會破環字符串上下文,因為它們只能被解釋成字符串常量。
ECMA-262 5.1版 6章 6節
“ECMAScript 與 JAVA 編程語言在對待Unicode轉義序列時的行為不同。在Java程序中,如果Unicode轉義序列\u000A出現在單行字符串注釋中,它會被解釋為行結束符(換行符),因此會導致接下來的Unicode字符不是注釋的一部分。同樣的,如果Unicode轉義序列\u000A出現在Java程序的字符串常量中,它同樣會被解釋為行結束符(換行符),這在字符串常量中是不被允許的——如果需要在字符串常量中表示換行,需要用\n來代替\u000A。在ECMAScript程序中,出現在注釋中的Unicode轉義序列永遠不會被解釋,因此不會導致注釋換行問題。同樣地,ECMAScript程序中,在字符串常量中出現的Unicode轉義序列會被當作字符串常量中的一個Unicode字符,並且不會被解釋成有可能結束字符串常量的換行符或者引號。”
標識符名稱中:當Unicode轉義序列出現在標識符名稱中時,它會被解碼並解釋為標識符名稱的一部分,例如函數名,屬性名等等。這可以用來解釋問題10。如果我們深入研究JavaScript細則,可以看到如下內容:
“Unicode轉義序列(如\u000A\u000B)同樣被允許用在標識符名稱中,被當作名稱中的一個字符。而將'\'符號前置在Unicode轉義序列串(如\u000A000B000C)並不能作為標識符名稱中的字符。將Unicode轉義序列串放在標識符名稱中是非法的。”
控制字符:當用Unicode轉義序列來表示一個控制字符時,例如單引號、雙引號、圓括號等等,它們將不會被解釋成控制字符,而僅僅被解碼並解析為標識符名稱或者字符串常量。如果你去看ECMAScript的語法,就會發現沒有一處會用Unicode轉義序列來當作控制字符。例如,如果解析器正在解析一個函數調用語句,圓括號部分必須為“(”和“)”,而不能是\u0028和\u0029。
總的來說,Unicode轉義序列只有在標識符名稱里不被當作字符串,也只有在標識符名稱里的編碼字符能夠被正常的解析。如果我們回看問題11,它並不會被執行。因為“(11)”不會被正確的解析,而“alert(11)”也不是一個有效的標識符名稱。問題12不會被正確執行要么是因為'\u0031\u0032'不會被解釋為字符串常量(因為它們沒有用引號閉合)要么是因為它們是ASCII型數字。問題13不會執行的原因是'\u0027'僅僅會被解釋成單引號文本,而此時字符串是未閉合的。問題14能夠執行的原因是'\u000a'會被解釋成換行符文本,這並不會導致真正的換行從而引發JavaScript語法錯誤。
0x04 解析流
在討論過HTML,URL和JavaScript解析之后,讀者應該能夠對“什么會被解碼”、“在什么地方被解碼”和“如何被解碼”這幾件事有了清楚的認識。現在,另一個重要的概念是所有這些是如何協同工作的?在網頁中有很多地方需要多個解析器來協同工作。因此,對於解碼和轉義問題,我們將簡要的討論瀏覽器如何解析一篇文檔。
當瀏覽器從網絡堆棧中獲得一段內容后,觸發HTML解析器來對這篇文檔進行詞法解析。在這一步中字符引用被解碼。在詞法解析完成后,DOM樹就被創建好了,JavaScript解析器會介入來對內聯腳本進行解析。在這一步中Unicode轉義序列和Hex轉義序列被解碼。同時,如果瀏覽器遇到需要URL的上下文,URL解析器也會介入來解碼URL內容。在這一步中URL解碼操作被完成。由於URL位置不同,URL解析器可能會在JavaScript解析器之前或之后進行解析。考慮如下兩種情況
1
2
|
Example A: <
a
href
=
"UserInput"
></
a
>
Example B: <
a
href=#
onclick
=
"window.open('UserInput')"
></
a
>
|
在例A中,HTML解析器將首先開始工作,並對UserInput中的字符引用進行解碼。然后URL解析器開始對href值進行URL解碼。最后,如果URL資源類型是JavaScript,那么JavaScript解析器會進行Unicode轉義序列和Hex轉義序列的解碼。再之后,解碼的腳本會被執行。因此,這里涉及三輪解碼,順序是HTML,URL和JavaScript。
在例B中,HTML解析器首先工作。然而接下來,JavaScript解析器開始解析在onclick事件處理器中的值。這是因為在onclick事件處理器中是script的上下文。當這段JavaScript被解析並被執行的時候,它執行的是“window.open()”操作,其中的參數是URL的上下文。在此時,URL解析器開始對UserInput進行URL解碼並把結果回傳給JavaScript引擎。因此這里一共涉及三輪解碼,順序是HTML,JavaScript和URL。
有沒有可能解碼次數超過3輪呢?考慮一下這個例子
1
|
Example C: <
a
href
=
"javascript:window.open('UserInput')"
>
|
例C與例A很像,但不同的是在UserInput前多了window.open()操作。因此,對UserInput多了一次額外的URL解碼操作。總的來說,四輪解碼操作被完成,順序是HTML,URL,JavaScript和URL。
此時此刻,讀者應該已經獲得解答博文開始提到的所有問題的必要知識。如果你有任何的問題,歡迎留言討論。
0x05 總結
簡而言之,作為攻擊者為了弄明白如何讓XSS向量逃逸出上下文,或者為了使你的應用能夠正確編碼用戶的輸入,你必須真正明白瀏覽器的解析原理以及它們(HTML,URL和JavaScript解析器)是如何協同工作的。只有這樣,你才能從瀏覽器的角度去正確編碼你的向量。
本文由 安全客 翻譯,轉載請注明“轉自安全客”,並附上鏈接。
原文鏈接:http://www.attacker-domain.com/2013/04/deep-dive-into-browser-parsing-and-xss.html