PHP 反序列化
0x01 前言 Java 基础的反序列化已经学了一些内容了,因为最近要打省赛,打算粗略的过一遍 PHP 反序列化吧,学习 PHP 也是给自己少点压力,学 Java 确实难。
0x02 PHP 的反序列化
序列化:serialize()
反序列化:unserialize()
简单的 demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class Test { public $age = '19' ; public $name = 'Drunkbaby' ; public function __construct ( ) { echo 'Test ' .$this ->name . ' is ' . $this ->age . 'years old ' ; } } $a = new Test ();echo serialize ($a ) ;echo PHP_EOL;$b = serialize ($a );echo $b ;?>
PHP 反序列化其实是有点让人难受的,因为它的序列化过程,将对象转化为字符串,这个字符串并非是二进制文件,而且说实话,我个人觉得可读性有点不太好,看着不太舒服吧……
输出结果:
1 Test Drunkbaby is 19 years old O:4 :"Test" :2 :{s:3 :"age" ;s:2 :"19" ;s:4 :"name" ;s:9 :"Drunkbaby" ;}
此时就可以看出序列化后这里有多个字母,下面依次来进行解释
1 2 3 O:4 :"Test" :2 :{s:3 :"age" ;s:2 :"19" ;s:4 :"name" ;s:9 :"Drunkbaby" ;} 对象类型:长度:类名:变量个数:{类型:长度:"值" ;类型:长度:"值" ;类型:长度:"值" ;类型:长度:"值" ;}
字母的含义如下
1 2 3 4 5 6 a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
正常遇到的这种反序列化和序列化都是和 PHP 类与对象这部分知识点相关联的,所以我们需要简单了解类和对象这个知识点,同时掌握魔术方法的基础用法
0x03 PHP 魔术方法
翻看自己以前的文章,发现已经有所提及到了,但是我自己却没什么印象哈哈,还是重新过一下吧,也要不了多少时间。
常见魔术方法有以下几种
1 2 3 4 5 6 7 8 9 10 11 12 13 14 __construct () 当一个对象创建时被调用,__destruct () 当一个对象销毁时被调用,__toString () 当一个对象被当作一个字符串被调用。__wakeup () 使用unserialize时触发__sleep () 使用serialize时触发__destruct () 对象被销毁时触发__call () 在对象上下文中调用不可访问的方法时触发__callStatic () 在静态上下文中调用不可访问的方法时触发__get () 用于从不可访问的属性读取数据__set () 用于将数据写入不可访问的属性__isset () 在不可访问的属性上调用isset ()或empty ()触发__unset () 在不可访问的属性上使用unset ()时触发__toString () 把类当作字符串使用时触发,返回值需要为字符串__invoke () 当脚本尝试将对象调用为函数时触发
这是整体的,但这样看似乎显得过于抽象,因此我们将其进行分类,依次进行举例讲解
__construct() 与 __destruct() 1 __construct : 在创建对象时候初始化对象,一般用于对变量赋初值。 __destruct : 和构造函数相反,当对象所在函数调用完毕后执行。
__destruct
还有一种利用方式,就是 __destruct()
在对象被主动销毁的时候,其实这里用销毁这个词我觉得并不妥当,严格意义上来说,其实是对象在结束整个过程后,最后进行的这么一个操作。
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 <?php class bai { public $name ; public $age ; public function __construct ($name ,$age ) { echo "__construct()初始化 " ; $this ->name=$name ; $this ->age=$age ; } public function __destruct ( ) { echo "__destruct()执行结束 " ; } } $a =new bai ('quan9i' ,19 ); unset ($a ); echo "777 " ; echo "------------分隔符---------------- " ; $b =new bai ('quan9i' ,19 ); echo "123 " ; ?>
__sleep() 1 __sleep () serialize 之前被调用,可以指定要序列化的对象属性。
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class bai { public $name ; public $age ; public function __construct ($name ,$age ) { echo "__construct()初始化 " ; $this ->name=$name ; $this ->age=$age ; } public function __sleep ( ) { echo "当使用serialize时触发此方法 " ; return array ('name' ,'age' ); } } $a =new bai ('Drunkbaby' ,19 );echo serialize ($a );?>
__wakeup() 1 __wakeup () 反序列化恢复对象之前调用该方法
实例如下
实例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class bai { public $name ; public $age ; public function __construct ($name ,$age ) { echo "__construct()初始化<br> " ; $this ->name=$name ; $this ->age=$age ; } public function __wakeup ( ) { echo "当使用unserialize时触发此方法<br> " ; $this ->age=1000 ; } } $a =new bai ('Drunkbabye' ,19 );$b = serialize ($a );var_dump (unserialize ($b ));?>
__toString() 1 __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 <?php class Test { public $a = 'This is a string' ; public function good ( ) { echo $this ->a.' ' ; } public function __toString ( ) { return '__toString方法被调用 ' ; } } $a = new Test ();$a ->good ();echo $a ;?>
__invoke() 1 __invoke () :将对象当作函数来使用时执行此方法。
示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class Test { public $data ="调用正常方法 " ; public function __invoke ( ) { echo "调用__invoke()方法" ; } } $a = new test ();echo $a ();?>
__get
实例如下
1 2 3 4 5 6 7 8 9 10 11 12 <?php class Test { public $n =123 ; public function __get ($name ) { echo '__get 方法被调用,其中不存在变量 ' .$name .' ' ; } } $a = new Test ();echo $a ->Drunkbaby;?>
__set 1 __set () :设置不存在的成员变量时调用的;
这一块 get 和 set 是可以放一起讲的,这两个方法很有意思,从某种程度来说是一种特别的抛出异常的手段。
如果我一个类当中不存在某个属性,如果在 Java 里面,会直接抛出异常,而在 PHP 里面会调用魔术方法,这其实是有概率造成 Gadget Chain 的反序列化的,不同于 Java 里面死的抛出异常,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 25 26 27 28 29 30 31 32 33 <?php class xg { public $data = 100 ; public function __set ($name ,$value ) { echo '__set 不存在成员变量 ' .$name .' ' ; $this ->test=$value ; } public function __get ($name ) { echo '__get 方法被调用,其中不存在变量 ' .$name .' ' ; } public function show ( ) { echo $this ->test; } } $a = new xg ();$a ->show (); echo ' ' ;$a ->test= 777 ; $a ->show ();echo ' ' ;$a ->Drunkbaby = 566 ;$a ->show ();?>
__call() 1 __call :当调用对象中不存在的方法会自动调用该方法
示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class Test { public $data ="调用正常方法 " ; public function __call ($name ,$value ) { echo "__call 被调用,Test 类中不存在方法 " .$name .' ' ; var_dump ($value ); } public function show ( ) { echo $this ->data; } } $a = new test ();$a ->show (); echo "--------------------分隔符---------------- " ;$a ->Drunkbaby_Be_Clever ('Drunkbaby' ,123 ); ?>
我的理解里面,这也算是一种抛出异常
__isset() 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 25 26 27 28 29 <?php class test { public $name ; private $age ; public function __construct ($name , $age ) { $this ->name = $name ; $this ->age = $age ; } public function __isset ($content ) { echo "当使用isset()函数,自动调用<br> " ; return isset ($this ->$content ); } } $a = new test ("Drunkbaby" , 19 );echo ($a ->name),"<br> " ;echo isset ($a ->name);echo "-----------分隔符--------------<br> " ;echo isset ($a ->age);?>
可以发现私有属性时会调用 isset 魔术方法(调用 protected 作用域的属性也会调用),这两个就属于是不可访问的属性了,被访问的时候会进到 __isset()
里面
__unset() 1 __unset () :在不可访问的属性上使用 unset () 时触发
代码如下
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 test { public $name ; private $age ; public function __construct ($name , $age ) { $this ->name = $name ; $this ->age = $age ; } public function __unset ($content ) { echo "当使用unset()函数,自动调用 " ; echo "Drunkbaby is silly" ; } } $a = new test ("Drunkbaby" , 19 );unset ($a ->name);echo " " ;echo "-----------分隔符-------------- " ;unset ($a ->age);echo " " ;?>
常用魔术函数汇总例子 代码如下
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 <?php class test { public $varr1 ="abc" ; public $varr2 ="123" ; public function echoP ( ) { echo $this ->varr1." " ; } public function __construct ( ) { echo "__construct " ; } public function __destruct ( ) { echo "__destruct " ; } public function __toString ( ) { return "__toString " ; } public function __sleep ( ) { echo "__sleep " ; return array ('varr1' ,'varr2' ); } public function __wakeup ( ) { echo "__wakeup " ; } } $obj = new test (); $obj ->echoP (); echo $obj ; $s =serialize ($obj ); echo $s .' ' ;echo unserialize ($s ); ?>
简单的 PHP 反序列化 CTF 题目 0x04 反序列化之字符串逃逸
其实也不能说算是字符串逃逸吧,个人认为说是字符串拼接,或许是更合理的。逃逸总是听着太高大上了
首先来介绍一下这个 str_replace
函数
1 2 3 4 5 str_replace str_replace — 字符串替换 说明 mixed str_replace ( mixed $search , mixed $replace , mixed $subject [, int &$count ] )该函数返回一个字符串或者数组。该字符串或数组是将 subject 中全部的 search 都被 replace 替换之后的结果。
过滤后字符变多 看一下我们本地的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php function change ($str ) { return str_replace ("x" ,"xx" ,$str ); } $name = $_GET ['name' ];$age = "I am 11" ;$arr = array ($name ,$age );echo "反序列化字符串:" ;var_dump (serialize ($arr ));echo "<br/>" ;echo "<br/>" ;echo "过滤后:" ;$old = change (serialize ($arr ));$new = unserialize ($old );var_dump ($new );echo "<br/>" ;echo "<br/>此时,age=$new [1]" ;
正常情况下是这样的
如果此时多传入一个x的话会怎样,毫无疑问反序列化失败,由于溢出(s本来是4结果多了一个字符出来,这也就是 str_replace
)
传入的参数 name,先与 age 一起组成了一个数组,经过过滤之后,再进行反序列化的工作,所以这里我们可以构造恶意的 name,此时我们传入
1 name=Drunkbabyxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"whoami";}
后面这部分:";i:1;s:6:"whoami";}
的长度是 20
由于一个x会被替换为两个,我们输入了一共20个x,现在是40个,多出来的20个x其实取代了我们的这二十个字符 ";i:1;s:6:"whoami";}
,从而造成";i:1;s:6:"whoami";}
的溢出,而 "
闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出 whoami
最后的 ;}
闭合反序列化全过程导致原来的 ";i:1;s:7:"I am 11";}"
被舍弃,不影响反序列化过程
构造 payload 之后成功
bugku Web new php 0CTF 2016 piapiapia 1 字符串逃逸后变多 题目是有源码泄露的,得到源码之后我们看到 config.php 里面有一个 $flag
从这里面就可以猜想,可能是要我们去读取 config.php
继续看,在 profile.php
里面,很明显可以看到存在一个危险函数 file_get_contents
,这就更加容易使我们去想到是读取 config.php 的了。
做的 file_get_contents
里面是什么东西呢,是 $profile['photo']
,我们溯上去,看一看 $profile
是什么
可以看到 $profile
是 $user
对象中的 show_profile()
方法,传入的参数是 $_SESSION['username']
。
跟进一下 show_profile()
方法,先看返回值,是 $object->profile
,也就是返回一个 profile 对象。$object
则是通过 parent::select($this->table, $where)
,也就是经过了 select 搞来的,再往上看,发现还调用了一个 filter()
方法,跟进去看一下。
字符串逃逸昭然若揭。
现在基本的攻击思路已经明确了,基本上是依靠 username 这里的可控点进行字符串逃逸的攻击,可是我们看的东西其实全程都和 file_get_contents($profile['photo'])
无关
终于,在 update.php 里面找到了关于 $profile
序列化的代码
所以就是用 username 进行字符串逃逸的工作,让 photo 那里直接读取 config.php
,去到 update 界面开始攻击。
我们要把 filename 修改成恶意的 config.php,构造出的结果如下
1 2 3 4 5 6 7 8 9 10 11 $profile = a:4 :{s:5 :"phone" ;s:11 :"12345678901" ;s:5 :"email" ;s:8 :"ss@q.com" ;s:8 :"nickname" ;s:8 :"sea_sand" ;s:5 :"photo" ;s:10 :"config.php" ;}s:39 :"upload/804f743824c0451b2f60d81b63b6a900" ;}print_r (unserialize ($profile ));结果如下: Array ( [phone] => 12345678901 [email] => ss@q.com [nickname] => sea_sand [photo] => config.php )
但是很明显,不可能直接可以读到 config.php,需要我们在 nickname 那里进行恶意构造,所以 payload
1 nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:" photo";s:10:" config.php";}
nickname 最后面塞上";}s:5:“photo”;s:10:“config.php”;}
,一共是34个字符,如果利用正则替换34个where,就可以直接把后面这些字符挤出去,让他们成为 photo
值得注意的是,nickname 要从原本的 nickname 修改成数组,因为要进行闭合
此时我们已经可以读到 config.php,只需要把内容进行 base64 解码即可
过滤后字符变少
异曲同工,之前我们是把字符挤出去,这里我们要把字符塞进来。
源代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php function change ($str ) { return str_replace ("xx" ,"3" ,$str ); } $arr ['name' ] = $_GET ['name' ];$arr ['age' ] = $_GET ['age' ];echo "反序列化字符串:" ;var_dump (serialize ($arr ));echo "<br/>" ;echo "过滤后:" ;$old = change (serialize ($arr ));var_dump ($old );echo "<br/>" ;$new = unserialize ($old );var_dump ($new );echo "<br/>此时,age=" ;echo $new ['age' ];
正常情况下,回显如此
现在我们把 name 改成 1xx,这里肯定会报错,过滤之后是减少了的,这就和字符变多的道理是一样的。
所以此处我们的利用思路应该是这样的:在后面构造恶意的 payload,然后通过字符减少,把恶意 payload 提前闭合。
payload
1 ?name=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&age=11 ";s:3:" age";s:6:" whoami";}
0x05 Session 反序列化