Typecho 反序列化漏洞導致前臺getshell 2017-10-26

最早知道這個漏洞是在一個微信群里,說是install.php文件里面有個后門,看到別人給的截圖一看就知道是個PHP反序列化漏洞,趕緊上服務器看了看自己的博客,發現自己也中招了,相關代碼如下:

然后果斷在文件第一行加上了die:

今天下午剛好空閑下來,就趕緊拿出來代碼看看。

漏洞分析

先從install.php開始跟,229~235行:

要讓代碼執行到這里需要滿足一些條件:

//判斷是否已經安裝
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}

// 擋掉可能的跨站請求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }

    $parts = parse_url($_SERVER['HTTP_REFERER']);
    if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }

    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}

首先是$_GET['finish']不為空,其次是referer需要是本站,比較容易實現。

繼續跟反序列化的地方:

$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));

首先使用Typecho_Cookieget方法獲取__typecho_config,get方法如下:

public static function get($key, $default = NULL)
{
    $key = self::$_prefix . $key;
    $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
    return is_array($value) ? $default : $value;
}

可以看到給$value賦值這一行,如果$_COOKIE里面沒有就從$_POST里面獲取,所以我們測試漏洞的時候直接POST也是可以的,不用每次設置Cookie了。

反序列化漏洞要利用勢必離不開魔術方法,我之前收集了一些和PHP反序列化有關的PHP函數:

__wakeup() //使用unserialize時觸發
__sleep() //使用serialize時觸發
__destruct() //對象被銷毀時觸發
__call() //在對象上下文中調用不可訪問的方法時觸發
__callStatic() //在靜態上下文中調用不可訪問的方法時觸發
__get() //用于從不可訪問的屬性讀取數據
__set() //用于將數據寫入不可訪問的屬性
__isset() //在不可訪問的屬性上調用isset()或empty()觸發
__unset() //在不可訪問的屬性上使用unset()時觸發
__toString() //把類當作字符串使用時觸發
__invoke() //當腳本嘗試將對象調用為函數時觸發

install.php中有一行:

$db = new Typecho_Db($config['adapter'], $config['prefix']);

其中Typecho_Db的構造函數如下,如果我們反序列化構造一個數組,其中adapter設置為一個類,那么就可以觸發這個類的__toString()方法。

    /**
     * 數據庫類構造函數
     *
     * @param mixed $adapterName 適配器名稱
     * @param string $prefix 前綴
     * @throws Typecho_Db_Exception
     */
    public function __construct($adapterName, $prefix = 'typecho_')
    {
        /** 獲取適配器名稱 */
        $this->_adapterName = $adapterName;
        /** 數據庫適配器 */
        $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

然后我們全局搜索__toString()方法,發現兩個有搞頭的文件:

/var/Typecho/Feed.php
/var/Typecho/Db/Query.php

我這里跟一下Feed.php,查看Feed.php__toString()方法,其中第290行:

foreach ($this->_items as $item) {
    $content .= '' . self::EOL;
    $content .= '' . htmlspecialchars($item['title']) . '' . self::EOL;
    $content .= '' . $item['link'] . '' . self::EOL;
    $content .= '' . $item['link'] . '' . self::EOL;
    $content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
    $content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;
    //省略........
}

其中調用了$item['author']->screenName,$item$this->_items的foreach循環出來的,并且$this->_itemsTypecho_Feed類的一個private屬性。

我們可以利用這個$item來調用某個類的__get()方法,上面說過__get()方法是用于從不可訪問的屬性讀取數據,實際執行中這里會獲取該類的screenName屬性,如果我們給$item['author']設置的類中沒有screenName就會執行該類的__get()方法,我們繼續來全局搜索一下__get()方法。

發現/var/Typecho/Request.php中的__get()方法如下:

public function __get($key)
{
    return $this->get($key);
}

跟進$this->get()方法如下:

public function get($key, $default = NULL)
{
    switch (true) {
        case isset($this->_params[$key]):
            $value = $this->_params[$key];
            break;
        case isset(self::$_httpParams[$key]):
            $value = self::$_httpParams[$key];
            break;
        default:
            $value = $default;
            break;
    }

    $value = !is_array($value) && strlen($value) > 0 ? $value : $default;
    return $this->_applyFilter($value);
}

這里沒什么問題,但最后一行:

return $this->_applyFilter($value);

跟進一下發現:

private function _applyFilter($value)
{
    if ($this->_filter) {
        foreach ($this->_filter as $filter) {
            $value = is_array($value) ? array_map($filter, $value) :
            call_user_func($filter, $value);
        }

        $this->_filter = array();
    }

    return $value;
}

這個foreach里面判斷如果$value是數組就執行array_map否則調用call_user_func,這倆函數都是執行代碼的關鍵方法。而這里$filter$value我們幾乎都是可以間接控制的,所以就可以利用call_user_func或者array_map來執行代碼,比如我們設置$filter為數組,第一個數組鍵值是assert,$value設置php代碼,即可執行。

然后我們來完成Exploit如下:

一级A片不卡在线观看