include函数
🌀

include函数

Tags
php
data
Aug 17, 2019
2019-8-17

结论

在看phpmyadmin4.8.1文件包含漏洞时看到了图中的文件包含姿势。
notion image
自己也做了测试如下
notion image
很好奇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.l643行,继续跟进能看到在zend_compile_file函数处理后发生变化,继续跟进该函数。就这样一步步跟进,经过几个函数最终定位到php-src/Zend/zend_compile.c383行的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.copen_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_pathfile_handle结构体中的值,file_handleopen_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.c2010行的_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/