从一个非预期谈起
开局先挂连接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
23i = 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
rename('test.php','test.php/.');
我们先抛开这道题,这里关键问题也就像zsx师傅在博客里说的那样lstat如果判断文件存在后就不打开文件了,而这个函数在linux下其实是有问题的,因为他会逐层解析php代码,一旦遇到不存在路劲他就会返回一个warning,导致move_uploaded_file认为这个文件不存在也就可以写入,最终达到了覆盖
的效果,我们可以测试一下lstat的功能1
2
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