php open_basedir poc分析
近日在CTF交流群中看到一个绕过open_basedir限制的poc
对这个poc产生了极大的兴趣,因此翻出php的源码来下断点分析一波php open_basedir的机制。
在/main/fopen_wrappers.c
中PHPAPI int php_check_open_basedir_ex(const char *path, int warn)
方法是php在处理文件操作时用于验证open_basedir的方法。我们查看一下他的实现方法
1 | PHPAPI int php_check_open_basedir_ex(const char *path, int warn) |
跟进php_check_specific_open_basedir
,这个函数是具体实现每一个路径的判断,一个很长的函数,重点在如下几行:
1 | if (expand_filepath(path, resolved_name) == NULL) { |
这里是将传入的path扩展为绝对路径存放于resolved_name
第214行
1 | if (expand_filepath(local_open_basedir, resolved_basedir) != NULL) { |
这里会根据local_open_basedir的值扩展为绝对路径存放于resolved_basedir
241行
1 | if (strncasecmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0) { |
可以看到这里在判断是否在路径范围内时,主要比较依据是先用strncmp判断与resolved_basedir长度内的部分是否完全一致,一致的话如果resolved_name与resolved_basedir长度相等则说明就在同一路径,返回0表示允许,长度大于resolved_basedir则判断超出的第一个字符是否不是/
,是则返回成功,不是则返回失败。
这里我们再重点看一下expand_filepath
这个函数的实现,主要实现为PHPAPI char *expand_filepath_with_mode
,重点位于814行
1 |
|
查看virtual_file_ex
的实现,1337行之前的操作为如果path不是绝对路径则将path拼接至state.cwd得到resolved_path,重点第1337行
1 | path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0, NULL); |
跟进tsrm_realpath_r
,可以看到操作主要是递归去掉双斜杠和.
以及..
这便是php在处理文件操作判断open_basedir的实现。我们再看php的内置函数ini_set
的实现方法,在ext/standard/basic_functions.c
中
1 | PHP_FUNCTION(ini_set) |
由于我们ini_set的是open_basedir
于是重要一行便落到了
1 | if (zend_alter_ini_entry_ex(varname, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) { |
查看zend_alter_ini_entry_ex
的实现,重要几行为
1 | if (!ini_entry->on_modify |
调试可知,open_basedir
对应的on_modify
函数为OnUpdateBaseDir
,重要几行为
1 | ptr = pathbuf = estrdup(ZSTR_VAL(new_value)); |
可见这里便是调用了php_check_open_basedir_ex
来判断要更改的open_basedir是否合法。
回到zend_alter_ini_entry_ex
中
1 | duplicate = zend_string_copy(new_value); |
可以看到open_basedir
便会被直接设置为我们设置的值。
再来看我们的poc
1 |
|
假定我们的open_basedir
为/var/www/html
,我们位于/var/www/html/test
目录下
执行第一个ini_set
时,首先判断/var/www/html/test/..
即/var/www/html/
是否为open_basedir内,判断成功,因此直接更新open_basedir为..
执行chdir('..')
时,检测open_basedir,..
根据当前目录补全后为/var/www/html
,而我们的open_basedir
为..
,补全后也是/var/www/html
,因此可以chdir成功。
再次chdir('..')
,检测open_basedir,..
补全为/var/www
,而此时的open_basedir
补全也为/var/www
,判断成功。
因此一系列的chdir('..')
都会成功执行,最后当前目录跳到了/
,open_basedir
为..
,设置open_basedir('/')
同样可以执行成功,便成功实现了调整open_basedir
至任意目录。
这个poc的构造十分巧妙,修复建议便是禁止在open_basedir已有的情况下修改open_basedir
或者禁open_basedir
可以被设置为相对路径。