由一道CTF题目引发的思考

从一个非预期谈起

开局先挂连接https://blog.zsxsoft.com/post/36
这里zsx师傅使用了上传index.php/.+条件竞争的方法getshell真的让人眼前一亮,尤其是对于我这种连index.php/.这种绕过方法都不甚了解的菜鸡,于是在仔细google了一番,终于在wonderkun师傅的博客里找到了这种绕过方式的介绍http://wonderkun.cc/index.html/?p=626
(其实这篇文章当时自己也看过,现在却完全想不起来T T)作者在文章后半段提到了利用1.php/.可以绕过后缀黑名单检测,但却不能覆盖文件。随后列出了相关的源码调用,阐述了问题形成的原因。这里我就不赘诉了。但是作者这里用的是file_put_content函数,而我们的问题是move_uploaded_file函数,这两个函数能相提并论么。其实这个问题P总在以前的小密圈里提到过,因为俩个函数都是文件读写类函数,都是需要打开文件流的,所以底层都会调用一个叫做tsrm_realpath的函数来将filename标准化为一个绝对路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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,

所以move_uploaded_file和file_put_content都会递归删除文件名最后的/.导致绕过了后缀名检测。
按理说故事到这里就该结束了,可是直到有一天我进入了土师傅的博客…

半路杀出个程咬金

继续挂链接https://www.lorexxar.cn/2018/04/05/0ctf2018-other/

看到这里的时候我就震惊了,index.php/.居然可以覆盖文件???,我前几天才看的文章是假的吧..index.php/.不是不可以覆盖文件么,我还本地测试过。于是怀着半信半疑的心情去测试了一下,很快发现了问题。


当我使用index.php/.是不可以覆盖的,但是使用x/../index.php/.就可以覆盖了,这一下勾起了我的好奇心,但是想弄懂这个问题,咱们还是得回到底层。

站在巨人的肩膀上

回到开头zsx师傅的连接,作者在文章里也从底层提到了问题的所在

这里的rename之所以会报错是因为这个函数并不会调用tsrm_realpath,导致当我们传入一个包含/.为后缀的文件时会出错
我们本地测试一下

1
2
3
<?php
rename('test.php','test.php/.');
?>


我们先抛开这道题,这里关键问题也就像zsx师傅在博客里说的那样lstat如果判断文件存在后就不打开文件了,而这个函数在linux下其实是有问题的,因为他会逐层解析php代码,一旦遇到不存在路劲他就会返回一个warning,导致move_uploaded_file认为这个文件不存在也就可以写入,最终达到了覆盖的效果,我们可以测试一下lstat的功能

1
2
<?php
print_r(lstat($_GET['test']));



ok,现在我们在回到这题来,我们用strace动态调试一下php,分别发送两个包

1
发送name=index.php/.


1
发送name=x/../index.php/.


很明显当传入name=x/../index.php/.的时候lstat的返回结果为-1,即不存在这个文件。这时候write函数就会继续执行从而将原内容覆盖。

来个小结

现在看来,php还有很多的文件操作函数存在问题,譬如今年跨年夜p总小密圈发的php文件读写函数这类需要打开文件流(file_put_content)与php判断文件属性这种无需打开文件流函数(unlink,file_exists)之间的区别以及围着这点展开的一系列问题。但是本质可能也就是一两个底层函数的问题,所以在这里也希望自己以后遇到问题不要嫌麻烦,多动手试试才能出真知。El psy congroo