摸鱼停不住先生头像
不安全反序列化利用篇(二):修改对象属性与数据类型封面图

不安全反序列化利用篇(二):修改对象属性与数据类型

上一节主要解决的是两个基础问题:什么是序列化,以及如何识别、解码、修改序列化对象。进入利用阶段后,重点就不再只是“这段数据能不能改”,而是要进一步判断:修改后的对象会不会被服务端反序列化?反序列化后的字段会不会进入认证、授权或其他安全敏感逻辑?

不安全反序列化的核心问题,不是序列化格式本身危险,而是服务端把客户端可控的数据恢复成程序对象后,又把这个对象当成可信状态使用。只要攻击者能够影响对象中的字段值、字段类型或对象结构,就可能改变服务端眼中的“用户身份”和“权限状态”。

本节结合两个 PortSwigger Lab 作为例子,提炼反序列化利用中的基础分析框架:

  • 修改序列化对象中的属性值,影响授权判断;
  • 修改序列化对象中的数据类型,影响认证比较。

这两个 Lab 只是参考案例,重点是理解它们背后的共性:客户端可控对象一旦穿过反序列化边界,并被服务端信任,就可能导致安全边界失效。

一、反序列化利用的核心链路

序列化数据在客户端看起来可能只是一段 Cookie、一串 Base64、一个 URL 编码后的值,或者某个请求参数。但对服务端来说,它被反序列化后会重新变成对象、数组或类实例。

因此,攻击者修改的不是普通文本,而是服务端即将恢复出的对象状态。

可以把基础攻击链抽象为:

客户端持有或影响序列化数据
        ↓
攻击者修改字段值、字段类型或对象结构
        ↓
服务端反序列化并恢复对象
        ↓
恢复后的对象进入认证、授权或业务逻辑
        ↓
服务端基于被篡改的状态做出安全决策
        ↓
权限提升或认证绕过

所以判断一个反序列化问题是否可利用,不能只看“数据能不能被解码”,还要看它是否真的影响了服务端后续逻辑。能改 Cookie 只是入口,服务端信任修改后的对象才是漏洞成立的关键。

二、正常安全边界:身份和权限应该由服务端确认

在正常设计中,客户端可以携带 Session ID、Cookie 或 Token,但这些数据不应该直接决定用户是不是管理员、是否已经认证、是否拥有某个权限。

更合理的模型是:

客户端提交不可预测的会话标识
        ↓
服务端根据标识查询服务端保存的会话状态
        ↓
服务端从数据库或权限系统确认用户身份
        ↓
服务端基于可信状态执行授权判断

也就是说,客户端可以证明“我持有某个会话”,但不能自己声明“我是管理员”。

不安全反序列化的问题在于,应用可能把完整的用户对象、权限对象或认证状态序列化后交给客户端保存。下一次请求时,服务端又从客户端 Cookie 中取回这段数据,反序列化后直接使用。

这时安全边界就被移动了:

用户是否是管理员
不再完全取决于服务端数据库或权限系统
而可能取决于客户端 Cookie 中某个字段的值

这就是本节两个例子的共同根因。

三、第一类利用:修改对象属性,影响授权状态

最直接的利用方式是修改序列化对象中的属性值。

例如,一个序列化用户对象中可能包含:

username = wiener
isAdmin = false

如果服务端反序列化后直接使用 isAdmin 判断是否允许访问后台,那么攻击者只需要把它改成:

username = wiener
isAdmin = true

服务端恢复出的对象就可能变成“管理员状态”。

这里真正的问题不是字段名叫 isAdmin,而是服务端信任了客户端提交的权限状态。类似风险字段可能有很多种形式:

role
admin
isAdmin
userType
accessLevel
permission
authenticated

字段名不固定,但分析方法固定:先识别哪些字段具有安全语义,再判断这些字段是否会进入服务端授权逻辑。

PortSwigger 的 Modifying serialized objects 实验就是这个模型的例子。应用使用基于序列化对象的会话机制,普通用户登录后,客户端 Cookie 中保存了一份序列化后的会话对象。如果其中的权限属性可以被修改,并且服务端反序列化后直接信任该属性,普通用户就可能获得管理员权限。

这个 Lab 可以抽象为:

普通用户登录
        ↓
获得客户端保存的序列化对象
        ↓
识别对象中的权限字段
        ↓
修改权限字段
        ↓
服务端反序列化修改后的对象
        ↓
授权逻辑读取被篡改的字段
        ↓
普通用户进入管理员功能

实验最后要求删除用户 carlos,这个动作不是漏洞本质,而是结果验证。它证明修改后的对象不只是影响了页面显示,而是真的进入了管理员授权分支。

这一类问题的关键判断点是:服务端有没有重新确认权限。如果服务端反序列化对象后,又根据用户 ID 去数据库查询真实权限,那么单纯修改客户端对象未必成功。反过来,如果服务端直接把对象中的 role、isAdmin 或 accessLevel 当作最终授权依据,权限提升就可能成立。

四、第二类利用:修改数据类型,影响认证比较

第二类利用更隐蔽。攻击者修改的不只是字段值,而是字段的数据类型。

在 PHP 序列化格式中,类型信息会被明确保存。例如:

s:1:"0";

表示字符串 "0"。

而:

i:0;

表示整数 0。

这两个值看起来都像 0,但服务端反序列化后得到的类型不同。普通 HTTP 参数通常以字符串形式传入,而序列化数据可以明确告诉服务端:“这个字段恢复出来后应该是整数”。

这就是修改数据类型的核心:不是把字段内容改成 0,而是让服务端恢复出整数 0。

这个问题在 PHP 中尤其典型,因为 PHP 的宽松比较运算符 == 会在比较不同类型时尝试做隐式类型转换。例如:

5 == "5";              // true
5 == "5 of something"; // true

简单来说,当整数和字符串用 == 比较时,PHP 会尝试把字符串转换成数字。在 PHP 7.x 及更早版本中,如果字符串不是以数字开头,例如 "Example string",它在这种比较场景下可能被转换成整数 0,于是:

0 == "Example string"; // true,PHP 7.x 及更早版本

这里不是字符串本身真的等于整数 0,而是宽松比较过程中发生了临时类型转换。PHP 8 以后,0 == "Example string" 不再返回 true,但以数字开头的字符串比较规则仍然需要注意,例如 5 == "5abc" 仍可能按数字前缀参与比较。

假设服务端逻辑类似于:

$login = unserialize($_COOKIE['session']);

if ($login['access_token'] == $user->access_token) {
    // 认证通过
}

如果攻击者能把反序列化对象中的 access_token 改成整数 0,而服务端保存的真实 token 是一个不以数字开头的字符串,那么在受影响的 PHP 版本中,比较可能变成:

0 == "真实 access token 字符串"
        ↓
字符串被转换为整数 0
        ↓
0 == 0
        ↓
认证通过

PortSwigger 的 Modifying serialized data types 实验就是这个模型的例子。这个实验不是简单要求把 token 改成 0,而是要求把 token 的类型从字符串改成整数。

下面这种修改是不够的:

s:12:"access_token";s:1:"0";

因为它仍然表示字符串 "0"。

真正关键的是改成:

s:12:"access_token";i:0;

这里需要同步理解序列化格式:

  • s 表示 string;
  • i 表示 integer;
  • 字符串有长度标记,例如 s:1:"0";
  • 整数没有字符串长度标记,因此应写成 i:0。

如果只改值,不改类型,服务端恢复出的仍然是字符串。此时可能出现“用户名已经变成 administrator,但 access token 无效”的错误。这种错误反而说明对象修改已经部分生效,只是关键字段的类型还没有满足利用条件。

五、两个例子共同说明了什么

这两个实验不是两个孤立技巧,而是在同一个反序列化利用框架下展示了两个层次。

第一个例子关注“字段值”:

服务端信任客户端对象中的权限字段
        ↓
攻击者修改字段值
        ↓
授权状态改变

第二个例子关注“字段类型”:

服务端信任客户端对象中的认证字段
        ↓
攻击者修改字段类型
        ↓
宽松比较触发隐式类型转换
        ↓
认证结果改变

它们共同说明:分析序列化数据时,不能只看字段值,还要看字段类型,以及服务端会如何使用这个字段。

面对一段客户端可控的序列化数据,至少要同时看三层:

字段名:这个字段代表什么业务含义?
字段值:这个值能不能改变身份、权限或状态?
字段类型:这个类型会不会影响比较、判断或分支?

如果只看字段值,容易漏掉类型转换类问题。如果只看格式能不能解析,也无法判断它是否真的影响了安全逻辑。

六、Gadget 在这里的位置

学习反序列化时,经常会遇到 Gadget 和 Gadget Chain,但不能把反序列化漏洞简单理解成 Gadget Chain。

Gadget 是应用或依赖库中已经存在的一段可利用代码片段。攻击者不能凭空上传新代码,但可以构造特定对象,让反序列化过程触发这些已有代码。多个 Gadget 按调用关系串联起来,就形成 Gadget Chain。链条最终到达的危险操作通常叫 Sink,例如命令执行、文件写入、模板解析、数据库操作等。

但本节讨论的两个例子并不依赖 Gadget Chain。它们利用的是更基础的信任问题:服务端直接相信反序列化后的对象状态。

可以这样区分:

修改属性值 / 修改数据类型
        ↓
直接影响认证或授权逻辑

Gadget Chain
        ↓
借助已有代码路径触发更危险的 Sink

前者不一定造成代码执行,但足以造成权限提升或认证绕过;后者通常用于进一步扩大影响范围。两者都属于反序列化风险,但利用层次不同。

七、通用分析框架

面对客户端可控的序列化数据,可以按下面的路径分析。

第一步,确认数据来源。

这段数据位于 Cookie、请求参数、隐藏表单字段、Header,还是请求体?它是服务端生成后交给客户端保存,还是攻击者可以直接构造?如果客户端可以读取、复制、修改并重新提交,就已经具备初步攻击面。

第二步,确认格式和结构。

判断它是否是序列化对象,能否解码,是否包含类名、字段名、字段值和类型标记。对于 PHP 序列化格式,还要注意 s、i、b、a、O 等类型标记,以及字符串长度是否需要同步修改。

第三步,定位安全敏感字段。

重点关注身份、权限、认证、状态流转相关字段,例如:

username
userId
role
isAdmin
accessLevel
authenticated
password
access_token

这些字段是否真的敏感,不取决于字段名看起来像不像权限字段,而取决于服务端后续是否使用它们做安全判断。

第四步,判断服务端是否信任反序列化结果。

这是最关键的一步。反序列化本身只是恢复对象;漏洞成立需要服务端把恢复后的对象直接用于认证、授权或业务分支。如果服务端重新查询数据库、重新校验 Session、重新计算权限,那么攻击链可能被截断。

第五步,判断是否存在类型相关逻辑缺陷。

如果目标语言或代码逻辑存在宽松比较、自动类型转换、空值判断、布尔转换等行为,就要检查能否通过修改序列化类型触发异常分支。PHP 中的 == 是典型例子,尤其是在认证 token、密码、权限标识等字段上更危险。

第六步,验证最终安全影响。

修改成功不等于利用成功。要看管理员页面是否可访问、敏感操作是否被执行、目标状态是否发生变化。实验中删除 carlos 的意义就在这里:它不是攻击链的核心,而是最终影响的验证点。

八、失败时如何定位问题

反序列化利用失败时,不应该盲目继续改更多字段,而要判断攻击链在哪一层断了。

如果服务端直接报解析错误,通常说明序列化格式损坏。常见原因包括字符串长度没有更新、类型标记错误、编码处理错误,或者删除了不该删除的分隔符。

如果对象能被解析,但权限没有变化,说明修改的字段可能没有进入授权逻辑,或者服务端在反序列化后重新从可信来源确认了权限。

如果用户名已经改变,但 token 校验失败,说明对象修改已经部分生效,问题很可能出在认证字段的值或类型上。比如把 access_token 改成字符串 "0",并不等于把它改成整数 0。

如果认证状态改变了,但最终操作失败,说明认证和授权可能是两层检查。登录为管理员不一定代表所有管理操作都会无条件通过,仍然可能存在额外权限、请求路径、CSRF token 或业务状态检查。

这种定位方式比机械套 payload 更重要。真正的反序列化分析,本质上是在追踪数据从客户端进入服务端后,在哪个判断点改变了程序行为。

九、防御思路

防御不安全反序列化,根本原则是:客户端提交的数据永远不应该直接决定身份、权限和认证结果。

更合理的做法包括:

  • 不把完整用户对象、权限对象或认证状态序列化后交给客户端保存;
  • 客户端只保存不可预测的 Session ID,真实状态保存在服务端;
  • 如果必须让客户端保存状态,应使用服务端密钥进行签名或 MAC 校验,防止篡改;
  • 反序列化后对字段名、字段类型、字段值进行白名单校验;
  • 认证和授权逻辑使用严格比较,例如 PHP 中使用 === 而不是 ==;
  • 不把 isAdmin、role、accessLevel 等客户端字段作为最终授权依据;
  • 对敏感操作重新从服务端 Session、数据库或权限系统确认用户身份;
  • 尽量避免对不可信输入使用通用反序列化机制,优先使用结构明确、类型受控的数据格式。

完整性保护可以防止客户端篡改对象,但它不能替代正确的权限设计。即使数据没有被篡改,服务端也应该避免把客户端状态当作最终安全事实。

十、小结

本节的重点不是两个 Lab 的具体操作,而是通过它们提炼出不安全反序列化的基础利用框架。

Modifying serialized objects 说明:如果客户端可控的序列化对象中包含权限属性,而服务端反序列化后直接信任这些属性,就可能导致权限提升。

Modifying serialized data types 说明:序列化数据不仅保存值,也保存类型。如果服务端把反序列化后的异常类型送入宽松比较,就可能导致认证绕过。这里的关键不是“把值改成 0”,而是“让服务端恢复出整数 0,并把它送进存在类型转换风险的比较逻辑”。

这两个例子共同指向同一个根因:服务端把客户端可控对象恢复成程序状态后,没有重新建立可信边界。

因此,分析不安全反序列化时,要始终围绕这条主线:

谁控制了序列化数据?
        ↓
服务端恢复出了什么对象?
        ↓
对象中哪些字段具有安全语义?
        ↓
字段的值和类型是否可以被攻击者改变?
        ↓
这些字段是否进入认证或授权逻辑?
        ↓
最终是否影响了真实的安全决策?

只要这条链路成立,即使没有 Gadget Chain,也可能造成权限提升或认证绕过。Gadget Chain 是反序列化利用中更进一步的攻击路径,而本节讨论的是更基础的一层:当对象状态本身被客户端控制时,服务端的认证和授权边界就已经开始失效。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_85321170/article/details/163675628

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--