作者:知道創(chuàng)宇404實驗室
近日,WordPress爆出了一個SQLi漏洞,漏洞發(fā)生在WP的后臺上傳圖片的位置,通過修改圖片在數據庫中的參數,以及利用php的sprintf
函數的特性,在刪除圖片時,導致'
單引號的逃逸。漏洞利用較為困難,但思路非常值得學習。
漏洞發(fā)生在wp-admin/upload.php的157行,進入刪除功能,
之后進入函數wp_delete_attachment( $post_id_del )
,$post_id_del可控,而且沒有做(int)格式轉化處理。
wp_delete_attachment位于wp-includes\post.php
的 4863 行。其中
圖片的post_id被帶入查詢,$wpdb->prepare中使用了sprintf,會做自動的類型轉化,可以輸入22 payload
,會被轉化為22
,因而可以繞過。
之后進入4898行的delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
函數。
delete_metadata函數位于wp-includes\meta.php
的307行,
在這里代碼拼接出了如下sql語句,meta_value為傳入的media參數
SELECT meta_id FROM wp_postmeta WHERE meta_key = '_thumbnail_id' AND meta_value = 'payload'
之后這條語句會進入查詢,結果為真代碼才能繼續(xù),所以要修改_thumbnail_id對應的meta_value的值為payload,保證有查詢結果。
因此,我們需要上傳一張圖片,并在寫文章
中設置為特色圖片。
在數據庫的wp_postmeta
表中可以看到,_thumbnail_id
即是特色圖片設定的值,對應的meta_value即圖片的post_id。
原文通過一個 WP<4.7.5 版本的xmlrpc漏洞修改_thumbnail_id
對應meta_value的值,或通過插件importer
修改。這里直接在數據庫里修改,修改為我們的payload。
之后在365行,此處便是漏洞的核心,問題在于代碼使用了兩次sprintf
拼接語句,導致可控的payload進入了第二次的sprintf
。輸入payload為22 %1$%s hello
代碼會拼接出sql語句,帶入$wpdb->prepare
SELECT post_id FROM wp_postmeta WHERE meta_key = '%s' AND meta_value = '22 %1$%s hello'
進入$wpdb->prepare后,代碼會將所有%s
轉化為'%s'
,即meta_value = '22 %1$'%s' hello'
因為sprintf的問題 (vsprintf與sprintf類似) ,'%s'
的前一個'
會被吃掉,%1$'%s
被格式化為_thumbnail_id ,最后格式化字符串出來的語句會變成
單引號成功逃逸!
最后payload為
http://localhost/wp-admin/upload.php?action=delete&media[]=22%20%251%24%25s%20hello&_wpnonce=bbba5b9cd3
這個SQL注入不會報錯,只能使用延時注入,而且需要后臺的上傳權限,所以利用起來比較困難。
上述WordPress的SQLi的核心問題在于在sprintf
中,'%s'
的前一個'
被吃掉了,這里利用了sprintf
的padding
功能
單引號后的一個字符會作為padding填充字符串。
此外,sprintf
函數可以使用下面這種寫法
%后的數字代表第幾個參數,$后代表類型。
所以,payload%1$'%s'
中的'%
被視為使用%
進行 padding,導致了'
的逃逸。
但在測試過程中,還發(fā)現其他問題。php的sprintf
或vsprintf
函數對格式化的字符類型沒做檢查。
如下代碼是可以執(zhí)行的,顯然php格式化字符串中并不存在%y
類型,但php不會報錯,也不會輸出%y
,而是輸出為空
<?php
$query = "%y";
$args = 'b';
echo sprintf( $query, $args ) ;
?>
通過fuzz得知,在php的格式化字符串中,%后的一個字符(除了'%'
)會被當作字符類型,而被吃掉,單引號'
,斜杠\
也不例外。
如果能提前將%' and 1=1#
拼接入sql語句,若存在SQLi過濾,單引號會被轉義成\'
select * from user where username = '%\' and 1=1#';
然后這句sql語句如果繼續(xù)進入格式化字符串,\
會被%
吃掉,'
成功逃逸
<?php
$sql = "select * from user where username = '%\' and 1=1#';";
$args = "admin";
echo sprintf( $sql, $args ) ;
//result: select * from user where username = '' and 1=1#'
?>
不過這樣容易遇到PHP Warning: sprintf(): Too few arguments
的報錯。
還可以使用%1$
吃掉后面的斜杠,而不引起報錯。
<?php
$sql = "select * from user where username = '%1$\' and 1=1#' and password='%s';";
$args = "admin";
echo sprintf( $sql, $args) ;
//result: select * from user where username = '' and 1=1#' and password='admin';
?>
通過翻閱php的源碼,在ext/standard/formatted_print.c
的642行
可以發(fā)現php的sprintf
是使用switch..case..實現,對于未知的類型default
,php未做任何處理,直接跳過,所以導致了這個問題。
在高級php代碼審核技術中的5.3.5中,提及過使用$order_sn=substr($_GET["order_sn"], 1)
截斷吃掉\
或"
。
之前也有過利用iconv轉化字符編碼,iconv('utf-8', 'gbk', $_GET['word'])
因為utf-8和gbk的長度不同而吃掉\
。
幾者的問題同樣出現在字符串的處理,可以導致'
的轉義失敗或其他問題,可以想到其他字符串處理函數可能存在類似的問題,值得去繼續(xù)發(fā)掘。
sprintf
或vsrptinf
進行拼接<?php
$input = addslashes("%1$' and 1=1#");
$b = sprintf("AND b='%s'", $input);
...
$sql = sprintf("SELECT * FROM t WHERE a='%s' $b", 'admin');
echo $sql;
//result: SELECT * FROM t WHERE a='admin' AND b=' ' and 1=1#'
此次漏洞的核心還是sprintf
的問題,同一語句的兩次拼接,意味著可控的內容被帶進了格式化字符串,又因為sprintf
函數的處理問題,最終導致漏洞的發(fā)生。
此問題可能仍會出現在WordPress的插件,原文的評論中也有人提到曾在Joomla中發(fā)現過類似的問題。而其他使用sprintf
進行字符串拼接的cms,同樣可能因此導致SQL注入和代碼執(zhí)行等漏洞。
https://medium.com/websec/wordpress-sqli-bbb2afcc8e94
https://medium.com/websec/wordpress-sqli-poc-f1827c20bf8e
http://php.net/manual/zh/function.sprintf.php
https://github.com/php/php-src/blob/c8aa6f3a9a3d2c114d0c5e0c9fdd0a465dbb54a5/ext/standard/formatted_print.c
https://www.seebug.org/vuldb/ssvid-96376