PHP是現在網站中最為常用的后端語言之一,是一種類型系統 動態、弱類型的面向對象式編程語言。可以嵌入HTML文本中,是目前最流行的web后端語言之一,並且可以和Web Server 如apache和nginx方便的融合。目前,已經占據了服務端市場的極大占有量。
但是,弱類型,一些方便的特性由於新手程序員的不當使用,造成了一些漏洞,這篇文章就來介紹一下一些滲透中可以用的特性。
上面都是廢話,下面我們進入正題
1.弱類型的比較==導致的漏洞
注:這些漏洞適用於所有版本的php
先來復習一下基本的語法:php中有如下兩種比較符號:兩個等號和三個等號(這一點和Javascript)有些類似
$a==$b
$a===$b
我們來一下php官方手冊的說法
$a == $b
等於 TRUE,如果類型轉換后 $a 等於 $b。$a === $b
全等 TRUE,如果 $a 等於 $b,並且它們的類型也相同。
明確的看到,兩個等於號的等於會在比較的時候進行類型轉換的比較。
如果比較一個數字和字符串或者比較涉及到數字內容的字符串,則字符串會被轉換為數值並且比較按照數值來進行。此規則也適用於 switch 語句。當用 === 或 !== 進行比較時則不進行類型轉換,因為此時類型和數值都要比對.
明確的寫出了 如果一個數值和一個字符串比較,那么會將字符串轉換為數值(而不是相反,將數值轉化為字符串)
然而,php是如何將一個字符串轉化為數值的呢,我們繼續查看php手冊
當一個字符串被當作一個數值來取值,其結果和類型如下:如果該字符串沒有包含 ‘.’,’e’ 或 ‘E’ 並且其數字值在整型的范圍之內(由 PHP_INT_MAX 所定義),該字符串將被當成 integer 來取值。其它所有情況下都被作為 float 來取值。該字符串的開始部分決定了它的值。如果該字符串以合法的數值開始,則使用該數值。否則其值為 0(零)。合法數值由可選的正負號,后面跟着一個或多個數字(可能有小數點),再跟着可選的指數部分。指數部分由 ‘e’ 或 ‘E’ 后面跟着一個或多個數字構成。
這是官方手冊上面的幾個例子
<?php
$foo = 1 + "10.5"; // $foo is float (11.5) $foo = 1 + "-1.3e3"; // $foo is float (-1299) $foo = 1 + "bob-1.3e3"; // $foo is integer (1) $foo = 1 + "bob3"; // $foo is integer (1) $foo = 1 + "10 Small Pigs"; // $foo is integer (11) $foo = 4 + "10.2 Little Piggies"; // $foo is float (14.2) $foo = "10.0 pigs " + 1; // $foo is float (11) $foo = "10.0 pigs " + 1.0; // $foo is float (11) ?>
我們大概可以總結出如下的規則:當一個字符串被轉換為數值時
- 如果一個字符串為 “合法數字+e+合法數字”類型,將會解釋為科學計數法的浮點數
- 如果一個字符串為 “合法數字+ 不可解釋為合法數字的字符串”類型,將會被轉換為該合法數字的值,后面的字符串將會被丟棄
-
如果一個字符串為“不可解釋為合法數字的字符串+任意”類型,則被轉換為0! 為0…為0
<?php 'a'==0 // true '12a'==12 //true '1'==1 //true '1aaaa55sss66'==1 //true
當然,上面的那些等式對於===都是false的,原本一些應該用===的地方誤用了==,導致了可以注入的地方。
示例代碼 1:利用轉為數字后相等的漏洞
<?php if (isset($_GET['v1']) && isset($_GET['v2'])) { $logined = true; $v1 = $_GET['v1']; $v2 = $_GET['v2']; if (!ctype_alpha($v1)) {$logined = false;} if (!is_numeric($v2) ) {$logined = false;} if (md5($v1) != md5($v2)) {$logined = false;} if ($logined){ // continuue to do other things } else { echo "login failed" } } ?
這是一個ctf的題目,非常有趣,可以看到,要求給出兩字符串,一個是純數字型,一個只能出現字符,使兩個的md5哈希值相等,然而這種強碰撞在密碼學上都是無法做到的。
但是我們看到,最終比較兩者的哈希的時候,使用的是等於 而不是 全等於 ,因此可以利用一下這個漏洞
再回頭看一 md5()
函數
string md5 ( string $str [, bool $raw_output = false ] )
str原始字符串。raw_output如果可選的 raw_output 被設置為 TRUE,那么 MD5 報文摘要將以16字節長度的原始二進制格式返回。
可以知道,第二個參數為true的時候,顯示16位的結果,而為false和沒有第二個參數時,為32位的16進制碼(16位的結果是把32位的作為ASCII碼進行解析)
16進制的數據中是含有e的,可以構建使得兩個數字比較的,這里有一個現成的例子:
md5('240610708') //0e462097431906509019562988736854. md5('QNKCDZO') //0e830400451993494058024219903391
可以看到,這兩個字符串一個只包含數字,一個只包含字母,雖然兩個的哈希不一樣,但是都是一個形式:0e 純數字這種格式的字符串在判斷相等的時候會被認為是科學計數法的數字,先做字符串到數字的轉換。
轉換后都成為了0的好多好多次方,都是0,相等。(大家可以自己嘗試一下)因此
md5('240610708')==md5('QNKCDZO'); //True md5('240610708')===md5('QNKCDZO'); //False
用===可以避免這一漏洞。
示例代碼2: 利用 類’a'==0的漏洞
<?php if (isset($_POST['json'])) { $json = json_decode($_POST['json']); $key ="**********************"; if ($json->key == $key) { //login success ,continue } else { //login failed ,return } ?>
這次這個例子是傳入一個JSON的數據,JSON在RESTful的網站中是很常用的一種數據傳輸的格式。這個表單會把一個name為key的input的數據作為json傳到服務端
{"key":"your input"}
我們該如何破解?想”a”==0這個漏洞,之用我們使$json->key
是一個數字類型的變量就可以,怎么做到呢?
php的json_decode()
函數會根據json數據中的數據類型來將其轉換為php中的相應類型的數據,也就是說,如果我們在json中傳一個string類型,那么該變量就是string,如果傳入的是number,則該變量為number。因此,我們如果傳入一個數字,就可以使之相等。網頁中的表單可能限制了所有的輸入都是string,即使輸入數字,傳入的東西也是
{"key":"0"}
這是一個字符串0,我們需要讓他為數字類型,用burp攔截,把兩個雙引號去掉,變成這樣:
{"key":0}
即可。
值得討論的一點是,在這種方法的漏洞利用中,很難在直接表單類型的POST的數據中使用,這是為什么呢,這個和HTTP協議有關。首先,我們看一下,在POST給服務器的數據中,有幾種類型,也就是HTTP header中的Content-Type:
application/x-www-form-urlencoded
multipart/form-data
application/json
application/xml
第一個application/x-www-form-urlencoded,是一般表單形式提交的content-type第二個,是包含文件的表單。第三,四個,分別是json和xml,一般是js當中上傳的.
但是因為在直接的POST的payload當中是無法區分字符串和數字的,因為在其中並沒有引號出現,舉一個抓包的例子
POST /login HTTP/1.1 Host: xxx.com Content-Length: 41 Accept: application/json, text/javascript,application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.8 Connection: close username=admin&password=admin
可以看到,payload是放在http包的最后面的,而且都是以沒有引號的形式傳遞的,並沒有辦法區分到底是字符串還是數字。因此,PHP將POST的數據全部保存為字符串形式,也就沒有辦法注入數字類型的數據了而JSON則不一樣,JSON本身是一個完整的字符串,經過解析之后可能有字符串,數字,布爾等多種類型。
2. strcmp漏洞
注:這一個漏洞適用與5.3之前版本的php
我們首先看一下這個函數,這個函數是用於比較字符串的函數
int strcmp ( string $str1 , string $str2 )
參數 str1第一個字符串。str2第二個字符串。如果 str1 小於 str2 返回 < 0; 如果 str1 大於 str2 返回 > 0;如果兩者相等,返回 0。
可知,傳入的期望類型是字符串類型的數據,但是如果我們傳入非字符串類型的數據的時候,這個函數將會有怎么樣的行為呢?實際上,當這個函數接受到了不符合的類型,這個函數將發生錯誤,但是在5.3之前的php中,顯示了報錯的警告信息后,將return 0 !!!! 也就是雖然報了錯,但卻判定其相等了。這對於使用這個函數來做選擇語句中的判斷的代碼來說簡直是一個致命的漏洞,當然,php官方在后面的版本中修復了這個漏洞,使得報錯的時候函數不返回任何值。但是我們仍然可以使用這個漏洞對使用老版本php的網站進行滲透測試。看一段示例代碼:
<?php $password="***************" if(isset($_POST['password'])){ if (strcmp($_POST['password'], $password) == 0) { echo "Right!!!login success";n exit(); } else { echo "Wrong password.."; } ?>
對於這段代碼,我們能用什么辦法繞過驗證呢, 只要我們$_POST['password']
是一個數組或者一個object即可,但是上一個問題的時候說到過,只能上傳字符串類型,那我們又該如何做呢。
其實php為了可以上傳一個數組,會把結尾帶一對中括號的變量,例如 xxx[]
的name(就是$_POST中的key),當作一個名字為xxx
的數組構造類似如下的request
POST /login HTTP/1.1 Host: xxx.com Content-Length: 41 Accept: application/json, text/javascript User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.8 Connection: close password[]=admin
即可使得上述代碼繞過驗證成功。
3 總結
這一類型的漏洞的特點主要就是利用PHP中 的類型特性來繞過驗證。由於 == 和 === 有着明顯的區分,因此,估計短期內PHP的作者並不會調整對於這兩個符號的策略。而對於開發市場而言,隨着培訓機構的增多,后端程序員尤其是php后端程序員的門檻越來越低,其水平必定也是良莠不齊,這些二把刀程序員可能帶來更多的此類對於特性的不當使用導致的漏洞,因此這類漏洞仍然是非常具有利用價值的。
總結一下,對於開發人員,需要堅持幾個習慣:
- 認真閱讀PHP manual,不能以其他語言的經驗來完全帶入php進行編碼
- 在使用一個運算符或者函數之前,詳細的查看文檔,搞清楚函數在什么樣的條件下,會有怎樣的行為。
記住保證安全的幾句箴言:任何用戶輸入都是不可信的!對於web應用來說,前端(瀏覽器端)的安全限制只能起到防止一般用戶的誤輸入行為,完全不可能對於黑帽子的行為有任何的防御作用
因此,在防御這個漏洞的過程中,保證幾件事情:
- 在所有可能的地方,都使用
===
來代替==
- 對於用戶輸入做過濾和類型檢查
- 盡量使用新版本的php,apache
基本上就可以完美的防御這一類的漏洞。
而對於滲透測試人員,在代碼審計的過程中,對於有==
,strcmp
的比較也應極為敏感 。在黑盒滲透的時候也可以對於代碼進行猜測,結合信息搜集過程中的一些版本特性,利用這些漏洞來繞過驗證。