csrf攻擊,即cross site request forgery跨站(域名)請求偽造,這里的forgery就是偽造的意思。網上有很多關于csrf的介紹,比如一位前輩的文章csrf的攻擊方式詳解,參考這篇文章簡單解釋下:csrf 攻擊能夠實現依賴于這樣一個簡單的事實:我們在用瀏覽器瀏覽網頁時通常會打開好幾個瀏覽器標簽(或窗口),假如我們登錄了一個站點a,站點a如果是通過cookie來跟蹤用戶的會話,那么在用戶登錄了站點a之后,站點a就會在用戶的客戶端設置cookie,假如站點a有一個頁面sitea-page.php(url資源)被站點b知道了url地址,而這個頁面的地址以某種方式被嵌入到了b站點的一個頁面siteb-page.php中,如果這時用戶在保持a站點會話的同時打開了b站點的siteb-page.php,那么只要siteb-page.php頁面可以觸發這個url地址(請求a站點的url資源)就實現了csrf攻擊。
上面的解釋很拗口,下面舉個簡單的例子來演示下。
1,背景和正常的請求流程
a站點域名為html5.yang.com,它有一個/get-update.php?uid=uid&username=username地址,可以看到這個地址可以通過get方法來傳遞一些參數,假如這個頁面的邏輯是:它通過判斷uid是否合法來更新username,這個頁面腳本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?php // 這里簡便起見, 從data.json中取出數據代替請求數據庫 $str = file_get_contents ( 'data.json' ); $data = json_decode( $str , true); // 檢查cookie和請求更改的uid, 實際應檢查數據庫中的用戶是否存在 empty ( $_cookie [ 'uid' ]) || empty ( $_get [ 'uid' ]) || $_get [ 'uid' ] != $data [ 'id' ] ? die ( '非法用戶' ) : '' ; // 檢查username參數 $data [ 'username' ] = empty ( $_get [ 'username' ]) ? die ( '用戶名不能為空' ) : $_get [ 'username' ]; // 更新數據 $data [ 'username' ] = $_get [ 'username' ]; if ( file_put_contents ( 'data.json' , json_encode( $data ))) { echo "用戶名已更改為{$data['username']}<br>" ; } else { die ( '更新失敗' ); } |
正常情況下這個頁面的鏈接是放在站點a下面的,比如a站點的csrfdemo.php頁面,用戶登錄站點a以后可以通過點擊這個鏈接來發送請求,比如站點a有一個頁面腳本,包含了這個鏈接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?php // 這里用一個data.json文件保存用戶數據,模擬數據庫中的數據 // 先初始化data.json中的數據為{"id":101,"username":"jack"}, 注意這句只讓它執行一次, 然后把它注釋掉 // file_put_contents('data.json','{"id":101,"username":"jack"}'); $data = json_decode( file_get_contents ( 'data.json' ), true); // 這里為了簡便, 省略了用戶身份驗證的過程 if ( $data [ 'username' ]) { // 設置cookie setcookie( 'uid' , $data [ 'id' ], 0); echo "登錄成功, {$data['username']}<br>" ; } ?> <a href= "http://html5.yang.com/csrfdemo/get-update.php?uid=101&username=json" rel= "external nofollow" > 更新用戶名為json </a> |
加載這個頁面如下:
用點擊頁面中的鏈接來到get-update.php頁面:
上面是正常的請求流程,下面來看b站點是如何實現csrf攻擊的。
2,csrf攻擊的最簡單實現
b站點域名為test.yang.com,它有一個頁面csrf.php,只要用戶在維持a站點會話的同時打開了這個頁面,那么b站點就可以實現csrf攻擊。至于為什么會打開......,其實這種情景在我們瀏覽網頁時是很常見的,比如我在寫這篇博客時,寫著寫著感覺對csrf某個地方不懂,然后就百度了,結果百度出來好多結果,假如說有個網站叫csrf百科知識,這個網站對csrf介紹的非常詳細、非常權威,那么我很可能會點進去看,但是這個網站其實是個釣魚網站,它在某個訪問頻率很高的頁面中嵌入了我博客編輯頁面的url地址,那么它就可以實現對我博客的csrf攻擊。好了,言歸正傳,下面來看下csrf.php腳本代碼:
1
2
3
|
<?php ?> <img src= "http://html5.yang.com/csrfdemo/get-update.php?uid=101&username=jsonp" > |
下面再來訪問下a站點的csrfdemo.php頁面:
可以看到用戶名被修改為了jsonp。
簡單分析下:b站點的這個csrf.php利用了html中的img標簽,我們都知道img標簽有個src屬性,屬性值指向需要加載的圖片地址,當頁面載入時,加載圖片就相當于向src指向的地址發起http請求,只要把圖片的地址修改為某個腳本地址,這樣自然就實現了最簡單的csrf攻擊。如此說來,其實csrf很容易實現,只不過大家都是“正人君子”,誰沒事會閑著去做這種“下三濫”的事情。但是害人之心不可有,防人之心不可無。下面看下如何簡單防范這種最簡單的csrf攻擊。
3,簡單防范措施
其實防范措施也比較簡單,a站點可以在get-update.php腳本中判斷請求頭的來源,如果來源不是a站點就可以截斷請求,下面在get-update.php增加些代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?php // 檢查上一頁面是否為當前站點下的頁面 if (! empty ( $_server [ 'http_referer' ])) { if ( parse_url ( $_server [ 'http_referer' ], php_url_host) != 'html5.yang.com' ) { // 可以設置http錯誤碼或者指向一個無害的url地址 //header('http/1.1 404 not found'); //header('http/1.1 403 forbiden'); header( 'location: http://html5.yang.com/favicon.ico' ); // 這里需要注意一定要exit(), 否則腳本會接著執行 exit (); } } $str = file_get_contents ( 'data.json' ); // 代碼省略 |
但是,這樣就萬事大吉了嗎,如果http請求頭被偽造了呢?a站點升級了防御,b站點同時也可以升級攻擊,通過curl請求來實現csrf,修改b站點的csrf.php代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php $url = 'http://html5.yang.com/csrfdemo/get-update.php?uid=101&username=jsonp' ; $refer = 'http://html5.yang.com/' ; // curl方法發起csrf攻擊 $ch = curl_init(); curl_setopt( $ch , curlopt_url, $url ); // 設置referer curl_setopt( $ch , curlopt_referer, $refer ); // 這里需要攜帶上cookie, 因為a站點get-update.php對cooke進行了判斷 curl_setopt( $ch , curlopt_cookie, 'uid=101' ); curl_exec( $ch ); curl_close( $ch ); ?> <img src= "http://html5.yang.com/csrfdemo/get-update.php?uid=101&username=jsonp" > |
4,小結
下面我們回到問題的開始,站點a通過cookie來跟蹤用戶會話,在cookie中存放了重要的用戶信息uid,get-update.php腳本通過判斷用戶的cookie正確與否來決定是否更改用戶信息,看來靠cookie來跟蹤會話并控制業務邏輯是不太安全的,還有最嚴重的一點:get-update.php通過get請求來修改用戶信息,這個是大忌。所以站點a可以接著升級防御:用session來代替cookie來跟蹤用戶會話信息,將修改用戶信息的邏輯重寫,只允許用post方法來請求用戶信息。站點b同樣可以升級攻擊:curl可以構造post請求,劫持session等等,不過這些我還沒研究過,后續再說吧。