|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2015-12-05 22:00 UTC] nachms+php at gmail dot com
Description: ------------ This affects all recent PHP versions includeding 7. The functions defined in ext/standard/exec.c which work with strings (escapeshellcmd, eschapeshellarg, shell_exec), all ignore the length of the PHP string, and work with NULL termination instead. Since the base pointer of the string is passed directly onto C functions, NULLs in the middle of the string silently truncate the string with no warning or error passed onto the user. If for some reason the string in question is not NULL terminated at all (some function uses RETURN_STRINGL(buf, len, 0); where buf is not NULL terminated), running these functions will buffer overflow. Test script: --------------- <?php $v = "hello\0world"; echo $v, "\n", escapeshellarg($v), "\n"; Expected result: ---------------- NOT the following: helloworld 'hello' The second line should either be a warning or error, or maybe 'helloworld', but definitly not just 'hello'. PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sun Oct 26 12:00:01 2025 UTC |
Consider the following scenario, an application among various parameters takes a parameter called "deleteall". The PHP script tries to filter out calling this command of the application but wants to allow the external input to call any other sub-command. $param = json_decode($_GET['param']); if ($param !== 'deleteall') { system('application ' . escapeshellarg($param)); } else { echo 'Action prohibited', "\n"; } The malicious user calls script.php?param="deleteall\u0000" The if sees that the param is not equal to "deleteall" and allows it to go through. escapeshellarg() which is supposed to take external user input and make it safe instead takes a value which was viewed as being safe, truncates it at the NULL byte, and allows it to be malicious instead of rejecting it. There really should be NULL byte protection on these functions. I would add to this that php_exec_ex() used by exec(), system(), and passthru() also contains a bug in its NULL byte detection: if (strlen(cmd) != cmd_len) { php_error_docref(NULL, E_WARNING, "NULL byte detected. Possible attack"); RETURN_FALSE; } strlen() is unsafe to call on a string which is not NULL terminated. The main PHP internals are pretty good about ensuring strings are NULL terminated despite the fact that they carry length, but it's possible something has been overlooked, and when it comes to various PECL extensions, some of them fail to NULL terminate their strings. The following should be used instead: if (memchr(cmd, 0, cmd_len)) { php_error_docref(NULL, E_WARNING, "NULL byte detected. Possible attack"); RETURN_FALSE; } It would probably be a good idea to follow this up with a check against cmd[cmd_len] to ensure it is a null byte, in case for whatever reason the string in question was not NULL terminated. Passing on a non-NULL terminated string to exec*(), popen() or whatever C function is asking for a segmentation fault, or worse.> This wont work, because === leads to is_identical_function() which checks for both string lengths (binary safe), and in your case, they don't match. I'm not sure what you mean by "wont work". They don't match, correct, which proceeds to run: system('application ' . escapeshellarg($param)); If just system('application ' . $param) was ran, the NULL byte protection in system() would also catch this malicious input and reject it. escapeshellarg() which is supposed to protect against unsafe external input is taking a value which would have been safe (thanks to system()'s NULL byte protection), and turns it into something malicious, "deleteall" in this case. And as you said, thanks to !== being binary safe, the user created check doesn't protect against it either as the user is not considering this side effects of escapeshellarg(), and only uses what turns out to be a naive check. escapeshellarg() and all the related functions should either have NULL byte protection or ensure the NULL byte is not stripped out, so the follow up calls to an execution function has their NULL byte protection protect against it. > Having strings that are not NULL terminated is impossible inside the engine I recall that not being the case with some of the random string generation functions, at least in older versions when I was reviewing those functions. Not that anyone should be passing randomly generated string to external programs, it still looks like a bad idea to assume everything is NULL terminated when strings have an explicit length. > If in PECL someone doesn't create valid C strings - aka he doesn't terminate them with NULL - then we are not responsible of the problem that can happen into the engine and the bug is a PECL extension bug that the maintainer should fix. I've spoken to some developers about this when finding some bugs in this regard. I have gotten them to NULL terminate. However their claim is that PHP strings are not "C strings". They have length, and can be assigned with macros like RETURN_STRINGL (no-copy) without NULL terminating them. No warnings are printed when compiling such extensions that use RETURN_STRINGL without NULL termination. Also, documentation on using these macros in various places is lacking in this regard. The developers I've spoken with are simply unaware that everything must be NULL terminated. Being that NULL termination is not sufficiently warned against, documented, and intuitively it seems they don't need it (they carry length), it seems like a bad idea to assume everything is NULL terminated.@nachms to the first part. The pattern you brought is unsuitable, because it allows wildcards. It doesn't really matter whether to pass "deleteall" or anything else. When wrapping some shell command allowing access from web, this pattern is insecure per se. Even naming the _GET variables same way as commands is bad. It doesn't regard to any concrete programming language, it's a programmers choice. What I would suggest instead is like switch ($command) { case "delete": /* do delete command */ break; case "some_other": /* do other command */ break ...................... default: /* unknown command */ } Doing "if (!my_bad_command)" wildcards any possible stuff. Variables SHOULD explicitly match the input. Especially with some "deleteall" - whitelisting is the only feasible way. Bad coding is always bad coding. With changing the exec API to be binary safe - well, is this going to fix the mistakes like non \0 termitation? Like passing wrong length for example. But if done, it will be a BC break that should be really justified. Some functions in the API are binary safe, some not. I'm personally afraid that changing the current APIs will be just exchanging one can of bugs with another one. What I personally could imagine - if we had a decision that exceptions in the functions were allowed in any functions, then the binary safe API for exec would help to determine that there were a \0 in a command and throw then (or alike example). Thanks.Hi ab. I think your proposed changes to escapeshellcmd() is fine (and should also be applied to the other functions I listed in my first post). However, users will need to be made aware to check the return value for false. As to escaping \0, one option as I've been saying before is to just leave it in along with whatever follows it (no truncation). This would ensure that follow up calls to exec() or system() with concatenated escaped input hit NULL byte detection in those functions and error out there. Users should already be checking return values on those functions. Throwing an exception I believe would be the most correct procedure here if possible. In the end any of these 3 approaches pass technical muster for a safe API. The 3rd seems the safest and the least ambiguous. From the first two, which to prefer I think boils down to whether we think users may find other uses for escapeshellcmd() other than escaping parameters for shell commands (database input?, piping commands to applications that accept instruction via stdin? network strings?). If you decide to go with your suggested patch, please apply it to the other functions too, and you have my blessing. Although I would personally change: if (ZSTR_LEN(Z_STR_P(argument)) != strlen(ZSTR_VAL(Z_STR_P(argument)))) { to: if (memchr(ZSTR_VAL(Z_STR_P(argument)), 0, ZSTR_LEN(Z_STR_P(argument)))) { Or if you like verbosity: if (memchr(ZSTR_VAL(Z_STR_P(argument)), '\0', ZSTR_LEN(Z_STR_P(argument))) != NULL) { This accomplishes the same thing with a safer (and potentially faster) idiom. I personally also think it's clearer as to what your intention is (does this string contain a \0 ?).