<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址:http://drops.wooyun.org/papers/596

    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;
    

    97av