include->php对于文件路径的处理
Aug 17, 2019 09:54 · 782 words · 4 minute read
除草….
结论
在看phpmyadmin4.8.1文件包含漏洞时看到了图中的文件包含姿势。
自己也做了测试如下
很好奇include
为什么可以成功包含到同目录下的a.txt。
这里先放下结论,因为自己一开始没有找到相关的文章,但是随着调试php源码找资料,在最后找到php处理文件路径函数tsrm_realpath_r
后,找到了很多师傅的分析文章。
出现这种情况是因为php在文件路径处理上有一定的缺陷,上图的主要原因就是tsrm_realpath_r
函数会对于传入的路径做规范化处理。会删除掉2.php%3f/../
,只剩下来一个a.txt。
下面是我调试跟踪的一些过程,比较基础。师傅轻喷。
环境搭建
调试
这里的php版本是的7.1.31
先找到include函数的入口,在php-src/Zend/zend_execute.c
文件的zend_include_or_eval函数处下断点,找到传入的文件名,
Breakpoint 1, zend_include_or_eval (inc_filename=0x7ffff6813080, type=0x2) at /home/yang1k/Desktop/php/php-src/Zend/zend_execute.c:2783
2783 zend_op_array *new_op_array = NULL;
gdb-peda$ p *inc_filename.value.str.val@20
$8 = "2.php%3f/../a.txt\000ph"
在这里文件名还是传入的内容,单步跟进,发现在2845行的compile_filename
处理后,new_op_array发生变化如下
gdb-peda$ p *new_op_array.filename.val@50
$12 = "/home/yang1k/Desktop/php/myphp/bin/test/a.txt\000\000\000\000"
接下来就是找到new_op_array.filename.val@50
是哪里来的。
跟进compile_filename
函数,其定义在Zend/zend_language_scanner.l
643行,继续跟进能看到在zend_compile_file
函数处理后发生变化,继续跟进该函数。就这样一步步跟进,经过几个函数最终定位到
php-src/Zend/zend_compile.c
383行的zend_get_compiled_filename
函数。
ZEND_API zend_string *zend_get_compiled_filename(void) /* {{{ */
{
return CG(compiled_filename);
}
CG
是一个宏,会在PHP转换为Opcode过程中保存一些信息。接下来需要找到赋值给CG(compiled_filename)
的地方。
就在zend_get_compiled_filename
上面几行的zend_set_compiled_filename
,就可以看到CG(compiled_filename)
被赋值,代码如下。
ZEND_API zend_string *zend_set_compiled_filename(zend_string *new_compiled_filename) /* {{{ */
{
zval *p, rv;
if ((p = zend_hash_find(&CG(filenames_table), new_compiled_filename))) {
ZEND_ASSERT(Z_TYPE_P(p) == IS_STRING);
CG(compiled_filename) = Z_STR_P(p);
return Z_STR_P(p);
}
ZVAL_STR_COPY(&rv, new_compiled_filename);
zend_hash_update(&CG(filenames_table), new_compiled_filename, &rv);
CG(compiled_filename) = new_compiled_filename;
return new_compiled_filename;
}
通过在zend_set_compiled_filename
函数下断点,通过一步步调试可以知道zend_set_compiled_filename
函数在zend_get_compiled_filename
函数之前执行,并且将new_compiled_filename的值返回给CG(compiled_filename)。
接下来就看哪里调用了zend_set_compiled_filename
函数。
通过全局搜索找到了位于Zend/zend_language_scanner.c
的open_file_for_scanning
函数的第562行处执行了zend_set_compiled_filename
函数,传入compiled_filename
变量,这里的compiled_filename
变量已经变为/home/yang1k/Desktop/php/myphp/bin/test/a.txt
。
open_file_for_scanning
部分代码
ZEND_API int open_file_for_scanning(zend_file_handle *file_handle)
{
…………
if (file_handle->opened_path) {
compiled_filename = zend_string_copy(file_handle->opened_path);
} else {
compiled_filename = zend_string_init(file_handle->filename, strlen(file_handle->filename), 0);
}
zend_set_compiled_filename(compiled_filename);
…………
在函数中可以看到compiled_filename
的值是来自于zend_string_copy(file_handle->opened_path);
接下来就查看file_handle->opened_path
的值。file_handle->opened_path
是file_handle
结构体中的值,file_handle
是 open_file_for_scanning
函数的参数,在此函数下断点,可以看到file_handle
在刚进入函数时的内容如下
gdb-peda$ p*file_handle
$54 = {
handle = {
fd = 0x0,
fp = 0x0,
stream = {
handle = 0x0,
isatty = 0xf6813080,
mmap = {
len = 0x7ffff68591e0,
pos = 0x800000017c9c2442,
map = 0x7fff00000007,
buf = 0x555555da9210 <executor_globals+304> "\001",
old_handle = 0x7fffffffa6b0,
old_closer = 0x555555819539 <zend_compile+388>
},
reader = 0x7ffff68591e0,
fsizer = 0x0,
closer = 0x555555a6c5b8
}
},
filename = 0x7ffff6801f18 "2.php%3f/../a.txt",
opened_path = 0x0,
type = ZEND_HANDLE_FILENAME,
free_filename = 0x0
}
可以看到file_handle->opened_path
在一开始为空,接下来就寻找file_handle->opened_path
的值何时发生变化。
排查能找到在513行处执行zend_stream_fixup
函数后file_handle->opened_path
的值发生变化。
if (zend_stream_fixup(file_handle, &buf, &size) == FAILURE) {
return FAILURE;
}
zend_stream_fixup
函数定义在php-src/Zend/zend_stream.c
,继续又在186行调用了zend_stream_open
函数,这里是将file_handle->filename
传入了函数,此时file_handle->filename
的值为2.php%3f/../a.txt
.
if (zend_stream_open(file_handle->filename, file_handle) == FAILURE)
zend_stream_open
定义在php-src/Zend/zend_stream.c
的128行。
继续调试可知在131行zend_stream_open_function
函数处理后*handle.opened_path
变为/home/yang1k/Desktop/php/myphp/bin/test/a.txt
继续跟进来到php-src/main/main.c
的1412行
static int php_stream_open_for_zend(const char *filename, zend_file_handle *handle) /* {{{ */
{
return php_stream_open_for_zend_ex(filename, handle, USE_PATH|REPORT_ERRORS|STREAM_OPEN_FOR_INCLUDE);
}
跟进到php_stream_open_for_zend_ex
函数的1420行,再继续跟进该处的php_stream_open_wrapper
函数,来到main/streams/streams.c
2010行的_php_stream_open_wrapper_ex
函数处,继续看到2055行的wrapper->wops->stream_opener
,跟进这个方法到php-src/main/streams/plain_wrapper.c
的1076行,然后定位到1080行的php_stream_fopen_rel
函数。然后跟
进到该函数的定义到该文件的957行
继续跟进到1029行的代码如下
*opened_path = zend_string_init(realpath, strlen(realpath), 0);
将这里的realpath打印出来是
home/yang1k/Desktop/php/myphp/bin/test/a.txt\000
所以接下来跟进realpath变量,该变量为_php_stream_fopen函数中的变量,在第994行经过expand_filepath
函数处理后值发生变化。
跟进该函数到main/fopen_wrappers.c
的750行,代码如下
PHPAPI char *expand_filepath(const char *filepath, char *real_path)
{
return expand_filepath_ex(filepath, real_path, NULL, 0);
}
继续跟进expand_filepath_ex
函数到该文件的758行
PHPAPI char *expand_filepath_ex(const char *filepath, char *real_path, const char *relative_to, size_t relative_to_len)
{
return expand_filepath_with_mode(filepath, real_path, relative_to, relative_to_len, CWD_FILEPATH);
}
再继续跟进expand_filepath_with_mode
函数,就在764行,分析该函数定位到827行的memcpy函数。
在memcpy
函数处打印出传入的变量
827 memcpy(real_path, new_state.cwd, copy_len);
gdb-peda$ p real_path
$54 = 0x7fffffff91c0 ""
gdb-peda$ p new_state.cwd
$55 = 0x7ffff6801f50 "/home/yang1k/Desktop/php/myphp/bin/test/a.txt"
memcpy指的是c和c++使用的内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
所以接下来要跟踪new_state
,可发现在817行new_state
赋值为/home/yang1k/Desktop/php/myphp/bin/test
.
继续定位到820行的virtual_file_ex
函数
if (virtual_file_ex(&new_state, filepath, NULL, realpath_mode)) {
此处函数的参数值如下
gdb-peda$ p new_state
$69 = {
cwd = 0x7ffff6801f50 "/home/yang1k/Desktop/php/myphp/bin/test",
cwd_length = 0x27
}
gdb-peda$ p filepath
$70 = 0x7ffff6801f18 "2.php%3f/../a.txt"
gdb-peda$ p realpath_mode
$71 = 0x1
继续跟进 virtual_file_ex
函数,来到zend/zend_virtual_cwd.c
的1277行,定义如下
CWD_API int virtual_file_ex(cwd_state *state, const char *path, verify_path_func verify_path, int use_realpath) /* {{{ */
然后跟进到1471行,代码如下
memcpy(state->cwd, resolved_path, state->cwd_length+1);
这里的state
便是上个函数中的new_state
,这里的state->cwd和resolved_path的值如下
gdb-peda$ p state->cwd
$83 = 0x7ffff6801f50 "/home/yang1k/Desktop/php/myphp/bin/test"
gdb-peda$ p resolved_path
$82 = "/home/yang1k/Desktop/php/myphp/bin/test/a.txt\000\063f\000../a.txt\000\a\000\000\000\000\000\360p\377\377\377\177\000\000\267{\203UUU", '\000' <repeats 14 times>, "\021\b\000\000\030\236\246UUU\000\000x \207\366\377\177\000\000\060\201\377\377\377\177\000\000\305ȊUUU\000\000\000\222\377\377\377\177\000\000\000\202\377\377\377\177\000\000/home/yang1k/Desktop/php/myphp/bin/test", '\000' <repeats 3657 times>...
接下来重新运行程序,来看resolved_path
的变化情况。
在1414行代码执行后,resolved_path变为
$106 = "/home/yang1k/Desktop/php/myphp/bin/test/a.txt\000\063f\000../a.txt\000\a\000\000\000\000\000\360p\377\377\377\177\000\000\267{\203UUU", '\000' <repeats 14 times>, "\021\b\000\000\030\236\246UUU\000\000x \207\366\377\177\000\000\060\201\377\377\377\177\000\000\305ȊUUU\000\000\000\222\377\377\377\177\000\000\000\202\377\377\377\177\000\000/home/yang1k/Desktop/php/myphp/bin/test", '\000' <repeats 3657 times>...
1414行代码如下
path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0, NULL);
到这里就定位到tsrm_realpath_r
函数了。
整个调用栈如下
gdb-peda$ bt
#0 tsrm_realpath_r (path=0x7fffffff7080 "/home/yang1k/Desktop/php/myphp/bin/test/a.txt", start=0x1, len=0x39, ll=0x7fffffff707c, t=0x7fffffff7070, use_realpath=0x1, is_dir=0x0,
link_is_dir=0x0) at /home/yang1k/Desktop/php/php-src/Zend/zend_virtual_cwd.c:1260
#1 0x00005555558aae8a in virtual_file_ex (state=0x7fffffff90e0, path=0x7ffff6801f18 "2.php%3f/../a.txt", verify_path=0x0, use_realpath=0x1)
at /home/yang1k/Desktop/php/php-src/Zend/zend_virtual_cwd.c:1414
#2 0x00005555557ea815 in expand_filepath_with_mode (filepath=0x7ffff6801f18 "2.php%3f/../a.txt", real_path=0x7fffffff91c0 "", relative_to=0x0, relative_to_len=0x0, realpath_mode=0x1)
at /home/yang1k/Desktop/php/php-src/main/fopen_wrappers.c:820
#3 0x00005555557ea5d5 in expand_filepath_ex (filepath=0x7ffff6801f18 "2.php%3f/../a.txt", real_path=0x7fffffff91c0 "", relative_to=0x0, relative_to_len=0x0)
at /home/yang1k/Desktop/php/php-src/main/fopen_wrappers.c:758
#4 0x00005555557ea59d in expand_filepath (filepath=0x7ffff6801f18 "2.php%3f/../a.txt", real_path=0x7fffffff91c0 "") at /home/yang1k/Desktop/php/php-src/main/fopen_wrappers.c:750
#5 0x00005555558082a5 in _php_stream_fopen (filename=0x7ffff6801f18 "2.php%3f/../a.txt", mode=0x555555a4b7b9 "rb", opened_path=0x7fffffffa620, options=0x81,
__php_stream_call_depth=0x2, __zend_filename=0x555555a4f0b0 "/home/yang1k/Desktop/php/php-src/main/streams/plain_wrapper.c", __zend_lineno=0x438,
__zend_orig_filename=0x555555a4adf8 "/home/yang1k/Desktop/php/php-src/main/main.c", __zend_orig_lineno=0x58c) at /home/yang1k/Desktop/php/php-src/main/streams/plain_wrapper.c:994
#6 0x00005555558086a9 in php_plain_files_stream_opener (wrapper=0x555555d82220 <php_plain_files_wrapper>, path=0x7ffff6801f18 "2.php%3f/../a.txt", mode=0x555555a4b7b9 "rb",
options=0x81, opened_path=0x7fffffffa620, context=0x0, __php_stream_call_depth=0x1, __zend_filename=0x555555a4e4d8 "/home/yang1k/Desktop/php/php-src/main/streams/streams.c",
__zend_lineno=0x809, __zend_orig_filename=0x555555a4adf8 "/home/yang1k/Desktop/php/php-src/main/main.c", __zend_orig_lineno=0x58c)
at /home/yang1k/Desktop/php/php-src/main/streams/plain_wrapper.c:1080
#7 0x0000555555801c32 in _php_stream_open_wrapper_ex (path=0x7ffff6801f18 "2.php%3f/../a.txt", mode=0x555555a4b7b9 "rb", options=0x89, opened_path=0x7fffffffa620, context=0x0,
__php_stream_call_depth=0x0, __zend_filename=0x555555a4adf8 "/home/yang1k/Desktop/php/php-src/main/main.c", __zend_lineno=0x58c, __zend_orig_filename=0x0, __zend_orig_lineno=0x0)
at /home/yang1k/Desktop/php/php-src/main/streams/streams.c:2055
#8 0x00005555557e095e in php_stream_open_for_zend_ex (filename=0x7ffff6801f18 "2.php%3f/../a.txt", handle=0x7fffffffa5c0, mode=0x89)
at /home/yang1k/Desktop/php/php-src/main/main.c:1420
#9 0x00005555557e090b in php_stream_open_for_zend (filename=0x7ffff6801f18 "2.php%3f/../a.txt", handle=0x7fffffffa5c0) at /home/yang1k/Desktop/php/php-src/main/main.c:1412
#10 0x00005555558927af in zend_stream_open (filename=0x7ffff6801f18 "2.php%3f/../a.txt", handle=0x7fffffffa5c0) at /home/yang1k/Desktop/php/php-src/Zend/zend_stream.c:131
#11 0x0000555555892968 in zend_stream_fixup (file_handle=0x7fffffffa5c0, buf=0x7fffffffa458, len=0x7fffffffa450) at /home/yang1k/Desktop/php/php-src/Zend/zend_stream.c:186
#12 0x000055555581913d in open_file_for_scanning (file_handle=0x7fffffffa5c0) at Zend/zend_language_scanner.l:513
#13 0x0000555555819587 in compile_file (file_handle=0x7fffffffa5c0, type=0x2) at Zend/zend_language_scanner.l:627
#14 0x00005555558196e8 in compile_filename (type=0x2, filename=0x7ffff6813080) at Zend/zend_language_scanner.l:662
#15 0x00005555558ca5ee in zend_include_or_eval (inc_filename=0x7ffff6813080, type=0x2) at /home/yang1k/Desktop/php/php-src/Zend/zend_execute.c:2845
#16 0x0000555555912f8a in ZEND_INCLUDE_OR_EVAL_SPEC_CV_HANDLER () at /home/yang1k/Desktop/php/php-src/Zend/zend_vm_execute.h:35499
#17 0x00005555558ca818 in execute_ex (ex=0x7ffff6813030) at /home/yang1k/Desktop/php/php-src/Zend/zend_vm_execute.h:429
#18 0x00005555558ca8fc in zend_execute (op_array=0x7ffff6880000, return_value=0x0) at /home/yang1k/Desktop/php/php-src/Zend/zend_vm_execute.h:474
#19 0x000055555586d149 in zend_execute_scripts (type=0x8, retval=0x0, file_count=0x3) at /home/yang1k/Desktop/php/php-src/Zend/zend.c:1482
#20 0x00005555557e2a28 in php_execute_script (primary_file=0x7fffffffddc0) at /home/yang1k/Desktop/php/php-src/main/main.c:2577
#21 0x000055555594a6da in do_cli (argc=0x2, argv=0x555555dae1b0) at /home/yang1k/Desktop/php/php-src/sapi/cli/php_cli.c:993
#22 0x000055555594b5e7 in main (argc=0x2, argv=0x555555dae1b0) at /home/yang1k/Desktop/php/php-src/sapi/cli/php_cli.c:1381
#23 0x00007ffff6ce32e1 in __libc_start_main (main=0x55555594af83 <main>, argc=0x2, argv=0x7fffffffe168, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>,
stack_end=0x7fffffffe158) at ../csu/libc-start.c:291
#24 0x000055555563df6a in _start ()
也就是找到tsrm_realpath_r
函数之后,然后才找到了师傅们的分析,这里贴一下wonderkun师傅的代码
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,
小结
其实之前看过一些类似的分析文章,不过分析的函数一般都是file_put_content,move_uploaded_file,fopen,fwrite,fclose
这类文件操作函数,其实include
也是文件操作函数,但是一开始就没想到这边,还是太菜了。
链接
https://gywbd.github.io/posts/2016/2/debug-php-source-code.html https://segmentfault.com/a/1190000019782678 http://wonderkun.cc/index.html/?p=626 http://d1iv3.me/2018/04/15/%E4%BB%8EPHP%E6%BA%90%E7%A0%81%E7%9C%8BPHP%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C%E7%BC%BA%E9%99%B7%E4%B8%8E%E5%88%A9%E7%94%A8%E6%8A%80%E5%B7%A7/