Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time

预期解法

访问一下url,从代码里面看到有六个操作

  • pwd 输出当前路径
  • phpinfo 获取phpinfo
  • reset 重置沙箱
  • time 获取服务器时间戳
  • upload 上传文件
  • shell 包含沙箱文件夹下index.php

第一眼看代码,能发现一个很明显的上传漏洞:

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

这里只要用 ../../../ 就可以上传到任意路径了,但是问题在于限制了后缀,不能直接上传php文件。

继续看,发现phpinfo里面有比较有趣的几行:

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

用opcache和backdoor作为关键字搜索到这篇文章,意识到这里可以用opcache来获取shell。因为opcache的后缀是bin,不会受这里的后缀限制,那只要能重写掉沙箱目录下index.php的opcache,就可以get shell了。继续看配置,发现这里检查了opcache的时间戳和校验和。

这里可以找到检查的代码,检查的条件有:

memcmp(info.magic, "OPCACHE", 8) == 0;
memcmp(info.system_id, ZCG(system_id), 32) == 0);
zend_get_file_handle_timestamp(file_handle, NULL) == info.timestamp;
zend_adler32(ADLER32_INIT, mem, info.mem_size + info.str_size) != info.checksum;

其中info是opcache的文件头,mem是根据文件头中的信息读取的文件内容。这里可以知道,opcache在执行前有四个检查:

  • 前八个字节要为OPCACHE
  • 接下来32个字节要符合system_id
  • 时间戳和文件一致
  • 校验和一致

其中system_id可以计算出来。校验和因为不涉及到文件头,只涉及到后面序列化后的代码,并不需要修改,需要修改的只是时间戳。

时间戳检查的是文件的生成时间,那么可以调用reset操作后使用time操作,即可获得正确的时间戳。

获取shell之后,考虑到代码限制了open_basedir,那么flag应该在/var/www/html/flag目录下。这里有一个小坑,phpinfo没有给disable_functions,但是考虑到页面用到了scandirfile_get_contents,那么至少这两个函数是可用的。于是用scandir列目录之后file_get_contents获取文件内容,发现是一个opcache。

可以使用这个工具 来逆向,不过这里有一个bug没有修复,做题时不一定有时间修复bug,可以直接使用指定版本的库来安装。

pip install -Iv construct==2.8.3

这里有一个小坑,下载下来的opcache文件如果直接反编译,会报错。在二进制编辑器中看能发现文件头的opcache只有七位,没有最后的\x00,需要补齐才能正常解析,或者可以更改解析的脚本,magic只读取前七个字符。

另外解析工具中的opcode不是很全,应该是作者是根据php文档编写的工具,而文档中没有给出完整的引用,可以通过这里补全。

逆向之后发现文件中包含encrypt和encode两个函数,以及一个主要的逻辑。

主逻辑能比较简单的看出来,大概为

if(encrypt("this_is_a_very_secret_key", "input_your_flag_here") === "85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab") { 
    echo "Congratulation! You got it!";
} else {
    echo "Wrong Answer";
}

可以猜测只要写出对应的解密脚本,然后以this_is_a_very_secret_key作为key,85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab作为密文即可。

逆向encrypt和encode两个函数会相对麻烦一些,这里有两个思路,一个思路是动态调试,另一个思路是用工具逆向之后看伪代码,手工根据opcache写出逻辑。

动态调试调试配置会比较麻烦,但是成功后逆向的难度会小很多。如果能成功载入,可以尝试直接使用函数来猜测函数的功能,不过这里也有一个小坑,flag.php在执行完后调用了exit,所以直接载入运行后就会退出,这里可以考虑重写原生函数或者使用register_shutdown_function和析构函数。

重写函数可以参考这个链接,重写掉exit即可载入。或者把调用写在register_shutdown_function中也能执行。

另外也可以参考这个链接,执行后根据结果可以比较容易的猜测逻辑。

不过这道题目的逆向并没有设置太多的难度,相对比较简单,直接逆向也是可行的,如果要根据这个工具来逆向,可以更改其中parse_zval的代码,对变量的偏移做一个简单的定位以减小逆向的难度。逆向后可写出解密脚本:

function decode($string){
    $hex='';
    for ($i=0; $i < strlen($string); $i+=2){
        $hex .= chr(intval($string[$i].$string[$i+1], 16));
    }
    return $hex;
}

function decrypt($pwd, $cipher)
{
    mt_srand(1337);
    $cipher = decode($cipher);
    $data = "";
    $pwd_length = strlen($pwd);
    $data_length = strlen($cipher);
    for ($i = 0; $i < $data_length; $i++) {
        $data .= chr(ord($cipher[$i]) ^ ord($pwd[$i % $pwd_length]) ^ mt_rand(0, 255));
    }
    return $data;
}

echo(decrypt("this_is_a_very_secret_key", "85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab"));

这里还有一个坑,php的mt_rand在实现上有一个bug,在版本7.1之前都使用了错误的随机数算法,具体的链接在这里

在出题的时候记错了修复bug的版本,所以使用了7.2.x的版本来生成密文,之后经选手提醒放出了说明,这里对大家造成的不便感到抱歉。

最后,可以在这里看我的exp。

非预期解法

后缀绕过

题目用了pathinfo($name)["extension"]来check后缀,但是这里存在一个绕过。name=xx/../index.php/.的时候可以用自己上传的php文件覆盖原本的php文件。

我曾经考虑过这个问题,但是我测试时使用的exp为name=index.php/.,并没有成功。发现选手用上述的方法解出之后,看了一下PHP代码找了下原因,大概原因如下。

首先move_uploaded_file 的实现在这里 ,然后调用到了php_copy_file_ctx,在php_copy_file_ctx调用中尝试使用php_stream_copy_to_stream_ex打开目标位置的stream,当目标路径没有../的时候且文件存在的情况下没有打开成功,函数执行失败。而目标路径存在../的时候,则会打开成功。能解释这个原因的一个简单例子如下:

int main(int argc, char const *argv[])
{
    int fd = open(argv[1], O_RDONLY);
    printf("normal %s, status %d, Message : %s\n", argv[1], fd, strerror(errno));
    close(fd);
    return 0;
}

跑一下三种输入,代码运行的结果为:

Test ./sandbox/index.php, Fd 3, Message : Success
Test ./sandbox/index.php/., Fd -1, Message : Not a directory
Test ./sandbox/xx/../index.php/., Fd -1, Message : No such file or directory

这里可以注意到两次调用的结果是有所不同的,这里的原因是因为,这里直接把路径作为参数,没有解析路径的过程,如果在sandbox路径下存在xxx,则报错同样会为Not a directory。这也是这种方法可以成功的原理,php_copy_file_ctx系列函数并没有对路径做出解析,而是直接传入系统调用。

条件竞争

这里另外一个非预期解法是通过上传大文件,触发删除操作,然后在删除时使用name=./index.php/.上传自己的php文件,从而getshell。但是因为防火墙对大量的流量做了一定的限制,这种方法的成功率并不高。所以我在提示中说不需要条件竞争。