include->php对于文件路径的处理

Aug 17, 2019 09:54 · 782 words · 4 minute read PHP

除草….

结论

在看phpmyadmin4.8.1文件包含漏洞时看到了图中的文件包含姿势。 439a6a9b1bab58565162b48a501f76ed

自己也做了测试如下

79c25f654343cb2bb4ed72dbd5177d80

很好奇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/

tweet Share