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可以被设置为相对路径。