作者:SeaFood@知道創宇404實驗室

0x00 背景

近日,WordPress爆出了一個單引號的逃逸。漏洞利用較為困難,但思路非常值得學習。


0x01 漏洞分析

漏洞發生在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'

之后這條語句會進入查詢,結果為真代碼才能繼續,所以要修改_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注入不會報錯,只能使用延時注入,而且需要后臺的上傳權限,所以利用起來比較困難。


0x02 漏洞原理

上述WordPress的SQLi的核心問題在于在sprintf中,'%s'的前一個'被吃掉了,這里利用了sprintfpadding功能

單引號后的一個字符會作為padding填充字符串。

此外,sprintf函數可以使用下面這種寫法

%后的數字代表第幾個參數,$后代表類型。

所以,payload%1$'%s'中的'%被視為使用%進行 padding,導致了'的逃逸。


0x03 php格式化字符串

但在測試過程中,還發現其他問題。php的sprintfvsprintf函數對格式化的字符類型沒做檢查。

如下代碼是可以執行的,顯然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語句如果繼續進入格式化字符串,\會被%吃掉,'成功逃逸

<?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行

可以發現php的sprintf是使用switch..case..實現,對于未知的類型default,php未做任何處理,直接跳過,所以導致了這個問題。

截斷吃掉\"

之前也有過利用因為utf-8和gbk的長度不同而吃掉\

幾者的問題同樣出現在字符串的處理,可以導致'的轉義失敗或其他問題,可以想到其他字符串處理函數可能存在類似的問題,值得去繼續發掘。


0x04 利用條件

  1. 執行語句使用sprintfvsrptinf進行拼接

  2. 執行語句進行了兩次拼接,第一次拼接的參數內容可控,類似如下代碼

<?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#'


0x05 總結

此次漏洞的核心還是sprintf的問題,同一語句的兩次拼接,意味著可控的內容被帶進了格式化字符串,又因為sprintf函數的處理問題,最終導致漏洞的發生。

此問題可能仍會出現在WordPress的插件,原文的評論中也有人提到曾在Joomla中發現過類似的問題。而其他使用sprintf進行字符串拼接的cms,同樣可能因此導致SQL注入和代碼執行等漏洞。


0x06 參考鏈接

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


------------------------------------2017.11.01 更新------------------------------------


0x07 WordPress 4.8.2補丁問題

國外安全研究人員Anthony Ferrara給出了另一種此漏洞的利用方式,并指出了WordPress 4.8.2補丁存在的問題。

如下代碼

<?php

$input1 = '%1$c) OR 1 = 1 /*';
$input2 = 39;
$sql = "SELECT * FROM foo WHERE bar IN ('$input1') AND baz = %s";
$sql = sprintf($sql, $input2);
echo $sql;
//result: SELECT * FROM foo WHERE bar IN ('') OR 1 = 1 /*') AND baz = 39

%c起到了類似chr()的效果,將數字39轉化為',從而導致了sql注入。

對此,WordPress 4.8.2補丁在WPDB::prepare()中加入

$query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query );

從而,禁用了除%d%s%F之外的格式,這種方法導致了三個問題。

1.大量開發者在開發過程中使用了例如%1$s的格式,此次補丁導致代碼出錯。

2.在例如以下代碼中

   $db->prepare("SELECT * FROM foo WHERE name= '%4s' AND user_id = %d", $_GET['name'], get_current_user_id());

%4s會被替換成%%4s%%在sprintf中代表字符%,沒有格式化功能。所以,$_GET['name']會被寫到%d處,攻擊者可以控制user id,可能導致越權問題的出現。

3.補丁可以被繞過

meta.php的漏洞處

   if ( $delete_all ) {
     $value_clause = '';
     if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value ) {
       $value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value );
     }
     $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key ) );
   }

如果輸入

   $meta_value = ' %s ';
   $meta_key = ['dump', ' OR 1=1 /*'];

之后兩次進入prepare(),因為

   $query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); 

使得%s變為''%s''

最后結果

   SELECT type FROM table WHERE meta_key = 'dump' AND meta_value = '' OR 1=1 /*''

WordPress也承認這是一個錯誤的修復


在WordPress 4.8.3的補丁中,一是修改了meta.php中兩次使用prepare()的問題,二是使用隨機生成的占位符替換%,在進入數據庫前再替換回來。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/386/