PHP is a really powerful language, and as a wise man once said, with great power comes great responsibilities. There is nothing more frustrating than obtaining a remote code execution and be restricted by open_basedir and disable_functions, preventing us from reading etc/passwd or turning it into a remote shell. Let’s focus here on the former.
The directive open_basedir can be set in the php.ini file, and prevents PHP from reading file beyond a defined scope. For instance, if it is set to /var/www/html/myapp, trying to execute file_get_contents("/etc/passwd") should miserably fail. One could consider it as a security measure, but this is not everyone’s opinion. Let’s suppose now that an attacker managed to upload their webshell on a server where open_basedir is enforced, and exec-like function disabled. How to read /etc/passwd, then ?
It is quite well known that having the ability to call ini_set at runtime could lead to an open_basedir bypass. The latter is defined in a ini file but can be tighten at runtime, meaning that we can programmatically add more directories to the directive as long as they are sub-directories from the first one. For instance, if chevre is a child of /var/www/html/myapp, one can add it in the directive:
1 | open_basedir=/var/www/html/myapp:chevre/ |
but trying to add /etc is forbidden.
To do so, the attacker first needs to create a sub-directory and to move into it (let’s assume we are in /var/www/html/myapp, and the latter is writeable. Let’s also assume that open_basedir is set to /var/www/html/myapp, preventing it us from reading/writing in upper directories):
1 |
|
Now, once the execution context has moved to the directory /var/www/html/myapp/dindon, we can refer to the parent directory by appending ../. Indeed, since this ../ would refer to /var/www/html/myapp, the path would still belong to the already restricted path, and thus be allowed, leading to the following directive:
1 | open_basedir=/var/www/html/myapp:../ |
Once done, we can simply move upper, and reach the Holy Grail:
1 |
|
The issue was reported https://bugs.php.net/bug.php?id=76359, and as someone wrote:
Indeed, this is not a security issue according to our classification[1].
Hmmm, still, the issue was patched, but still disputed, considered as a misused directive instead of a real bug (https://github.com/php/php-src/pull/7024).
I was peacefully working on a PHP tool to bypass disable_functions directive, with the desire to first bypass open_basedir. It was PHP 7.3.29 and the bypass was working like a charm. But when it came to run it on a VM with PHP 8.1, it failed. I was indeed not able to append ../ to the directive. Quite frustrated, I took a look at the issue, and then at its patch:
One can see here that the code checks whether the 1st and 2nd character are dots, and that the 3rd one is either a null or a slash. But what if the 2nd character is a slash, with a path being something like './..' ?
Indeed, I changed my payload to something like:
1 |
|
and landed into / :smiley:
I then wrote a small PoC and tried it on a Docker running PHP 8.2.2RC1.:
1 |
|
And the result was as follows:
The issue was reported here and I guess that it will not be considered as a security issue. But still, does it really make sense to allow dot-dot-slash in such path ??