WordPress < 3.6.1 PHP 物件注入漏洞

wyzsk發表於2020-08-19
作者: 五道口殺氣 · 2013/09/13 11:36

From:WordPress < 3.6.1 PHP Object Injection

0x00 背景


當我讀到一篇關於Joomla的“PHP物件注射”的漏洞blog後,我挖深了一點就發現Stefan Esser大神在2010年黑帽大會的文章:

http://media.blackhat.com/bh-us-10/presentations/Esser/BlackHat-USA-2010-Esser-Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits-slides.pdf

這篇文章提到了PHP中的unserialize函式當操作使用者生成的資料的時候會產生安全漏洞。

所以呢,基本來說,unserialize函式拿到表現為序列化的資料,然後就解序列化它(unserialize嘛,當然就幹這個啊~)為php的值。這個值它可以是resource之外的任何型別,可為(integer, double, string, array, boolean, object, NULL),當這個函式操作一個使用者生成的字串的時候,在低版本的PHP中可能會產生記憶體洩露的漏洞,當然這也不是這篇blog要關注的問題。如果你對這個問題感興趣,你可以再去看看我上面說的大神的文章。

另外一種攻擊方式發生在當攻擊者的輸入被unserialize函式操作的時候,這種就是我說到的“PHP物件注入”。在這種方式中,物件型別的被unserialize的話,允許攻擊者設定他選擇物件的屬性。當這些物件中的方法被呼叫的時候,會出現一些效果(例如:刪除一些檔案),當攻擊者可以去選擇物件裡的一些屬性的時候,他就能夠刪除一個他所提交的檔案。

讓我們舉個例子吧,想象以下的程式碼中的class是使用者自己生成的內容被unserialize時載入的:

(ps:老外貼出的程式碼語法有問題,改了一下測試成功……)

#!php
<?php
class Foo {
    private $bar; 
    public  $file;

    public function __construct($fileName) {
        $this->bar = 'foobar';
        $this->file = $fileName;
    }

    // 一些其他的程式碼……

    public function __toString() {
        return file_get_contents($this->file);
    }
} 
?>

如果受害的缺陷程式碼同時還存在以下的程式碼:

#!php
echo unserialize($_GET['in']);

這攻擊者就可以讀取任意檔案,攻擊者可以如下去構造它的物件。

#!php
<?php
class Foo {
    public $file;
}
$foo = new Foo();
$foo->file = '/etc/passwd';
echo serialize($foo); 
?>

上面這段程式碼的結果是:O:3:"Foo":1:{s:4:"file";s:11:"/etc/passwd";} ,攻擊者現在要做的事情就事透過提交get請求到存在漏洞的頁面觸發他的攻擊程式碼。這個頁面會吐出/etc/passwd的內容來。能讀到這些檔案的內容怎麼看都不是一個好事情,你就想象一下,萬一缺陷程式碼中的函式不是file_get_contents而是eval呢?

我相信上面這部分已經能讓人明白允許使用者輸入進入unserialize這個函式危害有多大了。就連PHP手冊裡也說了不要把使用者生成的內容交給unserialize函式。

警告:

不要把不可信的使用者輸入交給unserialize,使用該函式解序列化內容能導致訪問且自動載入物件,惡意使用者可以利用這一點,從安全的角度,如果你想讓使用者可以標準的傳遞資料,可以使用json (json_encode json_decode)。

好,讓我們繼續說這些問題怎麼影響到Wordpress。

0x01 wordpress的安全問題


Stefan Esser's的黑帽演講中,他提到Wordpress是一款使用了serialize和unserialize函式的知名應用。在他的幻燈片中,unserialize用來接收來自Wordpress站點上的資料。所以攻擊者可以在受害站點上發起一次中間人攻擊。他可以修改來自Wordpress站點的返回資料,把他的程式碼加進去。有趣的是就在我編寫這篇文章的時候,Wordpress最新的版本也包含這個問題(距離那演講似乎過去三年了),想象一下,如果有駭客可以劫持WordPress.org的DNS會發生什麼事情吧。

然而,這也不是Wordpress使用這個unserialize的唯一地方,它還用於用於在資料庫中資料。舉例來說,使用者的metadata就被序列化後儲存在資料庫中,metadata的取回方式在wp-includes/meta.php的272行的get_metadata(),我在這裡引用一下該函式的部分程式碼(292-297行)

#!php
if ( isset($meta_cache[$meta_key]) ) {
    if ( $single )
        return maybe_unserialize( $meta_cache[$meta_key][0] );
    else
        return array_map('maybe_unserialize', $meta_cache[$meta_key]); 
}

基本上,這個函式所幹的事情就是取回資料庫裡的metadata(它來自每篇文章或使用者輸入),資料在資料庫中的wp_postmeta和wp_usermeta表中,有些資料是被序列化的而有些沒有被序列化,所以maybe_unserialize()函式替代了unserialize()在這裡操作,這個函式在wp-includes/functions.php的230到234行之間被定義。

#!php
function maybe_unserialize( $original ) {
    if ( is_serialized( $original ) ) //序列化的資料才會走到這裡面
        return @unserialize( $original );
    return $original; 
}

所以,這個函式乾的事情是檢查給予它的值是不是一個序列化的資料,如果是的話,就解序列化。這裡用來判斷是否是序列化所使用的函式是is_serialized(),它的定義在同檔案的247到276行之間。

#!php
function is_serialized( $data ) {
    // 如果連字串都不是,那就不是序列化的資料了
    if ( ! is_string( $data ) )
        return false;
    $data = trim( $data );
     if ( 'N;' == $data )
        return true;
    $length = strlen( $data );
    if ( $length < 4 )
        return false;
    if ( ':' !== $data[1] )
        return false;
    $lastc = $data[$length-1];
    if ( ';' !== $lastc && '}' !== $lastc )
        return false;
    $token = $data[0];
    switch ( $token ) {
        case 's' :
            if ( '"' !== $data[$length-2] )
                return false;
        case 'a' :
        case 'O' :
            return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
        case 'b' :
        case 'i' :
        case 'd' :
            return (bool) preg_match( "/^{$token}:[0-9.E-]+;\$/", $data );
    }
    return false;
}

WordPress檢查一個值是否是序列化的字串為什麼那麼重要的原因馬上要變得清晰了。首先,我們看一下一個攻擊者如何把他的內容最終加入到metadata表中的。每個使用者的姓名,雅虎IM都儲存在wp_usermeta表裡。所以我們把自己的惡意程式碼加在那我們就可以搞掂掉WordPress,對不對?你可以試試在你該寫名字的地方寫個i:1試試,如果這個沒有被解序列化那這裡只會返回一個我們輸入的i:1。

麻痺的,看來要發幾個大招才可以搞掂WordPress啊。讓我們挖得再深一點,看看為什麼這個東西就沒有給解序列化。在 wp-includes/meta.php 中,這個update_metadata() 函式定義在101-164行,這裡有這個函式的部分程式碼。

#!php
// …
    $meta_value = wp_unslash($meta_value);
    $meta_value = sanitize_meta( $meta_key, $meta_value, $meta_type );
// …
    $meta_value = maybe_serialize( $meta_value );

    $data  = compact( 'meta_value' );
// …
    $wpdb->update( $table, $data, $where );
// …

這裡maybe_serialize函式可能能解釋為什麼我們剛才的操作沒成功,我們再跟進去看看這個函式,它定義在wp-includes/functions.php的314-324行。

#!php
function maybe_serialize( $data ) {
    if ( is_array( $data ) || is_object( $data ) )
        return serialize( $data );
    // 二次序列化是為了向下支援
    // 詳見 http://core.trac.wordpress.org/ticket/12930
    if ( is_serialized( $data ) )
        return serialize( $data );

    return $data; 
}

所以當我們傳入一個序列化的值的話,它就會再序列化一下,這就是現在發生的情況,你看,資料庫裡的東西不是i:1;而是s:4:"i:1;";,當解序列化的時候它就顯示為一個字串,那現在該怎麼辦呢?

你懂的,這帖子的內容也存在資料庫裡,上面這就說明了為什麼我們失敗了。如果我們現在想往資料庫插一個序列化後的東西,我們就需要在我們插入資料的時候讓is_serialized()這個函式返回一個false,而當我們再從資料裡取它的時候,它就應該返回個true了。

你懂的,Mysql資料庫,表和欄位都有他們自己的charset和collation(字符集和定序)。WordPress呢,預設的字符集是UTF-8。從這個名字就看的出來,這個字符集它不支援全部的Unicode字元,你要是對這個感興趣,你可以看看Mathias Bynens的這篇文章:http://mathiasbynens.be/notes/mysql-utf8mb4,這文章教了我UTF-8的表儲存不了Unicode編碼區間是U+010000到U+10FFFF的字元。所以當我們在這個情況下嘗試儲存這些字元呢?顯而易見,包括這個字元和這個字元之後的內容都會被忽略掉。所以在我們嘗試插入foo{0xf09d8c86}bar的時候,Mysql會忽略{0xf09d8c86}bar而儲存為foo。

這個迷題的最後一部分就是我們需要插入一個用以一會兒解序列化的內容,為了測試這個,你可以插入1:i{0xf09d8c86}為你的名字。正如所見到的,結果是1,意味著你的輸入被解序列化了,如果你還不相信我,你試著輸入一個空陣列的序列化並且以該字元結尾:a:0:{}{0xf09d8c86}。這個結果是Array。

讓我們繼續maybe_serialized('i:1;{0xf09d8c86}')插入了資料庫。WordPress不認為這是一個已序列化的資料,因為它不是;或者}結尾的。它會返回i:1;{0xf09d8c86},當插入資料庫的時候,它的值是i:1,當它從資料庫取回的時候,它有了;最為最後一個字元,所以它可以解序列化成功。碉堡了。漏洞。

0x02 WordPress 利用


現在我們展示了WordPress存在PHP物件注入漏洞。讓我們嘗試利用它。所以為了利用該漏洞(透過注入物件的方法),我們需要找到一個符合以下條件的class:

1,內有“有用”的方法可被呼叫。 2,存在該物件的類已經被包含了。

當一個物件被解序列化的時候,__wakeup函式會被呼叫,這被稱作PHP的魔術方法,這也是我們確定會被呼叫的方法,實際上這些函式會更多寫些,我寫了一個以下的class來獲取被呼叫的class到/tmp/fumc.log。

#!php
<?php
class Foo {
    public static function logFuncCall($funcName) {
        $fh = fopen('/tmp/func.log', 'a');
        fwrite($fh, $funcName."\n");
        fclose($fh);
    }
    public function __construct() { Foo::logFuncCall('__construct('.json_encode(func_get_args()).')');}
    public function __destruct() { Foo::logFuncCall('__destruct()');}
    public function __get($name) { Foo::logFuncCall("__get($name)"); return "Foo";}
    public function __set($name, $value) { Foo::logFuncCall("__set($name, value)");} 
    public function __isset($name) { Foo::logFuncCall("__isset($name)"); return true;} 
    public function __unset($name) { Foo::logFuncCall("__unset($name)");} 
    public function __sleep() { Foo::logFuncCall("__sleep()"); return array();} 
    public function __wakeup() { Foo::logFuncCall("__wakeup()");} 
    public function __toString() { Foo::logFuncCall("__toString()"); return "Foo";} 
    public function __invoke($a) { Foo::logFuncCall("__invoke(". json_encode(func_get_args()).")");}
    public function __call($a, $b) { Foo::logFuncCall("__call(". json_encode(func_get_args()).")");}
    public static function __callStatic($a, $b) { Foo::logFuncCall("__callStatic(". json_encode(func_get_args()).")");}
    public static function __set_state($a) { Foo::logFuncCall("__set_state(". json_encode(func_get_args()).")"); return null;}
    public function __clone() { Foo::logFuncCall("__clone()");} 
} 
?>

為了列出這些被呼叫的函式,首先要確認這個函式在解序列化發生的時候是被引入被包含過的(php中的include require等)。你可以把require_once('foo.php')加到functions.php的頂端。接下來,把名字改為O:3:"Foo":0:{}{0xf09d8c86}來嘗試利用這個PHP物件注入漏洞,當重新整理頁面後,你回看到你的名字變成了Foo,這也就是意味著這是上面那class中__toString()函式的返回,然後讓我們看看都有哪些函式被呼叫了。

$ sort -u /tmp/func.log
__destruct()
__toString() 
__wakeup()

給出了我們三個函式:__wakeup(), __destruct() 和 __toString()

很不幸的是我不能再WordPress中找到一個載入了並且解序列化時能被利用造成影響的類。所以不是一個WordPress的安全問題,而是一個可能利用的地方。

所以是不是WordPress是有安全隱患的,但是無法被利用?不一定,如果你熟悉WordPress,你可能會覺察到可能有一堆外掛存在漏洞。這些外掛有他們自己的類並且可能暴露出可被利用的安全漏洞。我想到這個後,已經發現了一款著名的外掛存在漏洞並且可以導致遠端任意程式碼執行。

由於道德考慮,這個時候我不會發布PoC的,有太多存在安全漏洞的WordPress了。

0x03 修復WordPress


這個修復方式是修改is_serialized函式,我簡單的說說:

#!php
function is_serialized( $data, $strict = true ) {
     // 如果不是字串就不會是序列化後的資料
     if ( ! is_string( $data ) )
         return false;
     if ( ':' !== $data[1] )
         return false;
    if ( $strict ) {
        $lastc = $data[ $length - 1 ];
        if ( ';' !== $lastc && '}' !== $lastc )
            return false;
    } else {
         //確認存在;或}但不是在第一個字元
        if ( strpos( $data, ';' ) < 3 && strpos( $data, '}' ) < 4 )
            return false;
    }
     $token = $data[0];
     switch ( $token ) {
         case 's' :
            if ( $strict ) {
                if ( '"' !== $data[ $length - 2 ] )
                    return false;
            } elseif ( false === strpos( $data, '"' ) ) {
                 return false;
            }
         case 'a' :
         case 'O' :
             return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
         case 'b' :
         case 'i' :
         case 'd' :
            $end = $strict ? '$' : '';
            return (bool) preg_match( "/^{$token}:[0-9.E-]+;$end/", $data );
     }
     return false; 
 }

這主要的區別是當$strict引數設定為false的時候,會有一些強制操作導致一個字串被標記為已序列化。舉例說明,最後一個字元不需要必須是;或者{(譯者注:作者此處應該筆誤了,應該是;或者}),修復了我所提交的漏洞。現在大家有沒有相似的內容可以拿出來做個討論的?

WordPress依舊使用著不安全的unserialize()而非安全的json_decode。它的安全性全在判斷規則或者Mysql的規則實現上。我在上面揭露的漏洞實際上是使用Mysql的規則去掉我跟在特殊符號後的所有字元。

有一個很簡潔的修復方案,修改一下資料庫編碼不被截斷就好:

ALTER TABLE wp_commentmeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE wp_postmeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE wp_usermeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章