case 'upload':
if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
stristr(pathinfo($name)["extension"], "h")) {
break;
}
move_uploaded_file($_FILES['file']['tmp_name'], $name);
case 'shell':
ini_set("open_basedir", "/var/www/html/$dir:/var/www/html/flag");
include $dir . "index.php";
看到这几行代码,我们的思路就很明显了:绕过pathinfo限制,然后通过上传某个文件get shell。预期解法是上传一个opcache覆盖,而非预期解法是直接上传index.php。
怎么直接上传呢?我们只要传入index.php/.就可以绕过pathinfo的检测了。但这里有个坑,源自于move_uploaded_file。
我们看一下系统调用:
对应move_uploaded_file的源码,简单来说,它就是首先尝试rename,如果失败,就使用copy。
if (VCWD_RENAME(path, new_path) == 0) {
successful = 1;
} else if (php_copy_file_ex(path, new_path, STREAM_DISABLE_OPEN_BASEDIR) == SUCCESS) {
VCWD_UNLINK(path);
successful = 1;
}
但是,如果文件已经存在的话,人家stat后就不继续打开文件了。于是返回失败。
那这个问题怎么解决呢?当然是删掉它啊!看到题目中有以下代码
if(!file_exists($dir)){
mkdir($dir);
}
if(!file_exists($dir . "index.php")){
touch($dir . "index.php");
}
function clear($dir)
{
if(!is_dir($dir)){
unlink($dir);
return;
}
foreach (scandir($dir) as $file) {
if (in_array($file, [".", ".."])) {
continue;
}
unlink($dir . $file);
}
rmdir($dir);
}
case 'reset':
clear($dir);
break;
……嗯,好像不太好删。删掉会连带着把文件夹删掉,重新访问时又会创建。注意到上传文件时存在任意路径上传漏洞,好,就拿你开刀。我们发现,删除文件夹方式是先scandir,再rmdir。如果我们能在它scandir以后再写入文件,就可以阻止文件夹被rmdir。同时,因为index.php肯定存在,所以它也会被删除。这就达成我们需要的效果。
准备两个IP,下称IP A与IP B。都先pwd拿到他们的路径。
然后,通过Burpsuite,让IP A往IP B的sandbox里写大量文件。
在这同时,使用IP B访问reset,打时间差。如果顺利的话,index.php就能被删除成功。再通过IP 1上传Shell。
就这样,Shell传上去了。
通过base64_encode拿到文件后,发现是一个opcode cache。服务器是PHP 7.0,于是切换到7.0环境。使用此库:https://github.com/GoSecure/php7-opcache-override.git 。但会发现,解析不了。到底是怎么回事呢?
魔改一下这个库嘛,加点调试信息,明显就看到这个文件头哪里不太对劲。
参照正确的Opcode格式,在Opcode头与 system_id 处加上一个00,问题解决,可以解析了。
但这个库解析出来的东西是正常人能看得下去的吗?JMP的地址不知道是什么,变量名全是None。
这个时候,就要祭出大杀器 vld 了。我在之前的文章(https://blog.zsxsoft.com/post/30 )里提到过这个库。因此,通过以下参数,启动一个PHP服务器,并强制打开vld扩展。
生成Opcache文件后,我们就知道,我们自己的服务器的 system_id 是多少了。将我们从Shell获取的 Opcache 里面的 system_id 替换成自己服务器上的。
我们执行以下,发现替换成功。
回到php -S,看看vld给我们反馈了什么:
有变量名了!好看多了!没错,我就是用第一部分的预期解做第二部分的题。于是手工逆向之,出以下代码。
function encode ($string) {
$hex = '';
for ($i = 0; $i < strlen($string); ++$i) {
$tmp = dechex(ord($string[$i]));
if (strlen($tmp) == 1) {
$hex .= '0' . $tmp;
} else {
$hex .= $tmp;
}
}
return $hex;
}
function encrypt($pwd, $data) {
mt_srand(1337);
$cipher = '';
$pwd_length = strlen($pwd);
$data_length = strlen($data);
for ($i = 0; $i < $data_length; ++$i) {
$cipher .= chr((ord($data[$i]) ^ ord($pwd[$i % $pwd_length])) ^ mt_rand(0, 255));
}
return encode($cipher);
}
$flag = 'input_your_flag_here';
if (encrypt('this_is_a_very_secret_key', $flag) === '85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab') {
echo 'Congratulation! You got it!';
} else {
echo 'Wrong Answer';
}
解题脚本:
function decrypt($key) {
mt_srand(1337);
$f = [133, 185, 84, 252, 131, 128, 164, 102, 39, 110, 74, 72, 36, 157, 221, 74, 25, 159, 195, 78, 91, 6, 20, 100, 228, 41, 95, 197, 2, 12, 136, 191, 216, 84, 85, 25, 171];
$keyLength = strlen($key);
$flagLength = count($f);
for ($i = 0; $i < $flagLength; $i++) {
$flag[$i] = chr($f[$i] ^ mt_rand(0, 255) ^ ord($key[$i]));
print_r($flag[$i]);
}
}
flag{0pc4che_b4ckd00r_is_4_g0o6_ide4}
——然后这里又有一个大坑了。服务器上的PHP是7.0.8,于是我打算用Shell从服务器上取rand序列。但取不动。后来发现,本地的PHP 7.2可以直接跑动以上脚本。翻了下changelog,7.1起貌似改了PHP的mt_rand……
好了,这题的全盘非预期解做法结束。