前言
根據紅日安全寫的文章,學習PHP代碼審計的第四節內容,題目均來自PHP SECURITY CALENDAR 2017,講完題目會用一個實例來加深鞏固,這是之前寫的,有興趣可以去看看:
PHP代碼審計01之in_array()函數缺陷
PHP代碼審計02之filter_var()函數缺陷
PHP代碼審計03之實例化任意對象漏洞
漏洞分析
現在咱們看第一題,代碼如下:
<?php
class Login {
public function __construct($user, $pass) {
$this->loginViaXml($user, $pass);
}
public function loginViaXml($user, $pass) {
if (
(!strpos($user, '<') || !strpos($user, '>')) &&
(!strpos($pass, '<') || !strpos($pass, '>'))
) {
$format = '<?xml version="1.0"?>' .
'<user v="%s"/><pass v="%s"/>';
$xml = sprintf($format, $user, $pass);
$xmlElement = new SimpleXMLElement($xml);
// Perform the actual login.
$this->login($xmlElement);
}
}
}
new Login($_POST['username'], $_POST['password']);
?>
題目分析
我們來看上面的代碼,看第1214行,這里通過格式話字符串的方式,使用XML結構來存儲用戶的登陸信息,這樣很容易造成注入,再看上面的代碼,最后一行實例化了這個類,17行調用了login來進行登陸操作。下面到重點了,看代碼810行,這里用到了strpos()函數來進行過濾<>符號,這個函數的用法如下:
如下:
<?php
var_dump(strpos("abcd","a"));
var_dump(strpos("abcd","y"));
?>
我們發現,找到子字符串的話就會返回對應的下標,沒找到會返回false。而在PHP中,0和false的取反都是true,這點需要我們注意,這道題目就是開發者在使用這個函數時,只考慮了返回false的情況,而沒有考慮當首字符匹配時返回0的情況。導致了過濾被繞過從而實施XML攻擊。
現在知道了如何繞過,那么構造payload:user=<"><injected-tag%20property="&pass=<injected-tag>
為了更好的理解,來看一下構造這個payload時,strpos()函數的返回結果如下:
$user='<"><injected-tag property="';
$pass='<injected-tag>';
var_dump(strpos($user,'<'));
var_dump(!strpos($user,'<'));
var_dump(strpos($user,'>'));
var_dump(!strpos($user,'>'));
是不是理解了一些呢,現在看上面的代碼,思考一下如果我們使用這個payload會返回什么呢。
var_dump((!strpos($user, '<') || !strpos($user, '>')) &&
(!strpos($pass, '<') || !strpos($pass, '>')));
返回了true,其實就是var_dump((true || false) &&(true || false))
成功繞過,可以進行XML注入。
通過上面的學習講解是不是對strpos()函數了解更深一些了呢?下面咱們看一個實例,這個實例也是設計者沒有考慮周全,導致任意用戶密碼重置。
實例分析
這次的實例是DeDecms V5.7SP2正式版,源碼可以從網上下載搭建,這個很容易找到,就不詳細說了,上面說了,這個漏洞是任意用戶密碼重置,下面我們來分析代碼,漏洞的觸發點在 member/resetpassword.php 文件中,是因為對接收的參數safeanswer沒有進行嚴格的類型判斷導致被繞過。下面看代碼:
現在來對上面的代碼做一個分析,當$dopost==safequestion時,通過$mid對應的id值來查詢當前用戶的safequestion,safeanswer,userid,email等值,現在來看第84行,這里的意思是當我們傳入的安全問題和安全答案等於之前設置的值時,就傳入sn()函數,重點來了,注意看,這里用的是雙等於來驗證,而沒有用三等於,所以,這里是可以被繞過的。當用戶沒有設置安全問題時,那么默認情況安全問題值為0,安全答案值為null,這里指的是數據庫中的值,而我們如果傳入空值時,那么就是空字符串,84行語句也就變成了if('0' == '' && null == ''),也就是if(false&&true),所以我們只需要讓前半部分轉為true就可以了,通過測試如下圖,都可以和0比較等於true。
if("0"=="0.0"){echo "成功1"."\n";}
if("0"=="0e1"){echo "成功2"."\n";}
if("0"=="0e12"){echo "成功3"."\n";}
if("0"=="0e123"){echo "成功4"."\n";}
if("0"=="0."){echo "成功5"."\n";}
上面的幾種payload均能使得 safequestion 為 true,成功進入sn()函數
我們跟進sn()函數,代碼如下:
在sn()內部,會根據id到pwd_tmp表中判斷是否存在對應的臨時密碼記錄,根據結果確定分支,走向 newmail 函數。現在我們假設第一次來進行忘記密碼操作,那么現在的$row的值應該為空,也就會進入if(!is_array($row))分支,然后在newmail()函數中執行INSERT操作,具體代碼在這個文件的上面,如圖:
這個代碼的功能是發送郵件到相關郵箱,並插入一條記錄到dede_pwd_tmp表中,漏洞觸發點在這里,我們現在看92~95行。如果 ($send == 'N') 這個條件為真,那就跳轉到修改頁,通過 ShowMsg 打印出修改密碼功能的鏈接。拼接的url為:
http://www.dmsj.com/DedeCMS-V5.7-UTF8-SP2/uploads/member/resetpassword.php?dopost=getpasswd&id=$mid&key=$randval
現在咱們跟進dopost=getpasswd去看看,在member/resetpassword.php文件中,代碼如下:
這里用empty()函數來判斷$id和$row是否為空,如果不為空的話,就繼續向下走,進入if(empty($setp))中,先判斷是否超時,如果沒超時的話進入修改頁面,我圈起來了,現在跟過去看一下具體代碼如下:
發現數據包中 $setp=2,所以代碼功能又回到了member/resetpassword.php文件中,如下:
分析上面代碼,我們發現如果傳入的$key和數據庫中的$row['pwd']相同時,則完成密碼的重置,也完成了攻擊的分析過程。
漏洞驗證
現在注冊兩個賬號,分別是test123和test456,查看mid的值如下,test456的mid為3。test123的mid為2。
我現在登陸test456賬戶,訪問咱們構造的payload。來修改test123的密碼。
http://www.dmsj.com/DedeCMS-V5.7-UTF8-SP2/uploads/member/resetpassword.php?dopost=safequestion&safequestion=0e1&safeanswer=&id=2
我現在登陸的是test456賬戶,訪問url抓包。
獲取到了key值,然后來訪問修改密碼的url:
http://www.dmsj.com/DedeCMS-V5.7-UTF8-SP2/uploads/member/resetpassword.php?dopost=getpasswd&id=2&key=xy8UzeOI
我們發現到了修改密碼的頁面,直接可以修改密碼。我們將密碼修改為abcdef,然后登陸test123,發現登陸成功,密碼成功被修改。
小結
通過這篇文章的學習與講解,是不是對strpos()函數和PHP弱類型繞過有了一定的了解了呢?下一篇文章會對正則使用不當來進行學習與講解,一起努力吧!