0CTF2018之ezDoor的全盘非预期解法
zsx2018/4/2 in 代码分析 / 3 / 1902

官方wp(https://github.com/LyleMi/My-CTF-Challenges/blob/master/ezDoor/README_ZH.md )出来了以后,发现我的解法从开头到结尾都是非预期解法,因此写下这篇文章作为记录。不得不说,我觉得我的解法还挺有意思的。

题目及环境复现见:https://github.com/LyleMi/My-CTF-Challenges/tree/master/ezDoor/source

 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给我们反馈了什么:

有变量名了!好看多了!没错,我就是用第一部分的预期解做第二部分的题。于是手工逆向之,出以下代码。

<?php

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……

好了,这题的全盘非预期解做法结束。

如果本文对你有帮助,你可以用支付宝支持一下:

Alipay QrCode
XXXXX at 2018/5/4[回复]
师傅你那个查看php系统调用是用什么怎么做的呀?
XXXXX at 2018/5/4[回复]
查到了 是用strace
Coxxs at 2018/4/25[回复]
vld 竟然还能这样用,学习了.. 当时找了半天也没找到 vld 解析 opcache 文件的方法 orz