PHP程式碼審計04之strpos函式使用不當

雪痕*發表於2020-10-29

前言

根據紅日安全寫的文章,學習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弱型別繞過有了一定的瞭解了呢?下一篇文章會對escapeshellarg與escapeshellcmd使用不當來進行學習與講解,一起努力吧!

相關文章