This is a detailed discussion of the generic PHP-CGI remote code execution bug we found while playing Nullcon CTF. We found that giving
the query string ‘?-s’ somehow resulted in the “-s” command line
argument being passed to php, resulting in source code disclosure. We
explored this bug further and managed to improve our exploit to remote
code execution, and trace the bug to a PHP commit in 2004.
PHP has been working on a patch for this for quite a while. We have
been waiting to post this blog entry until a fix was released, but today
the bug was posted to reddit because it was apparently accidentally
marked public.
Executive summary:
- PHP-CGI installations are vulnerable to remote code execution
There is no official fix, but we provide some workarounds in the ‘mitigation’ section
The PHP bug report is now public and contains an official patch and a mod_rewrite based workaround
PHP has released versions PHP 5.3.12 and PHP 5.4.2, as well as an official mod_rewrite based workaround which fix the issue described in this post.
- The new PHP versions as well as the official php patch contain a bug
which makes the fix trivial to bypass. Use our mitigations for now.
- New versions of PHP which incorporate this revised fix will be
released soon. The issue that the bug was not initially properly fixed
is being tracked as CVE-2012-2311.
- FastCGI installations are not vulnerable
- The vulnerability can only be exploited if the HTTP server follows a
fairly obscure part of the CGI spec. Apache does this, but many other
servers do not.
Now, without further ado, the bug…
The Vulnerability
The hosting service Dreamhost (which Nullcon makes use of)
recommends
users that wish to modify their php.ini configuration file to run their
sites through a CGI wrapper, using Apache mod_actions’ Action directive
like this:
1
2
3
4
|
Options +ExecCGI
AddHandler php5-cgi .php
Action php-cgi /cgi-bin/php-wrapper.fcgi
Action php5-cgi /cgi-bin/php-wrapper.fcgi
|
php-wrapper.fcgi is a shell script that wraps php5-cgi, which has the aforementioned -s option.
1
2
|
#!/bin/sh
exec /dh/cgi-system/php5.cgi $*
|
Edit: (Note that the shell-script is inherently
insecure because it does shell expansion, the correct way to pass on
arguments would be
"$@")
We’ve tested this and have confirmed that the query parameters are
passed to the php5-cgi binary in this configuration. Since the wrapper
script merely passes all the arguments on to the actual php-cgi binary,
the same problem exists with configurations where php-cgi is directly
copied into the cgi-bin directory.
It’s interesting to note that while slashes get added to any shell
metacharacters we pass in the query string, spaces and dashes (‘-’) are
not escaped. So we can pass as many options to PHP as we want!
There is one slight complication: php5-cgi behaves differently
depending on which environment variables have been set, disabling the
flag -r for direct code execution among others.
1
2
3
4
5
6
7
8
9
10
11
|
if (!fastcgi) {
if (getenv("SERVER_SOFTWARE") ||
getenv("SERVER_NAME") ||
getenv("GATEWAY_INTERFACE") ||
getenv("REQUEST_METHOD")
) {
cgi = 1;
}
}
|
However, this can be trivially bypassed. We’re removing the remote
code execution PoC out of an abundance of caution, but at this point
anyone should be able to figure this out.
And for the record: safe_mode, allow_url_include and other
security-related ini settings will not save you. See the bottom of this
post for how you can protect yourself until the official patch is out.
Whose fault is this exactly? And why does the query string get parsed
into command line arguments anyway? We went on a little trip around the
internet to find out.
To answer the first question, there is the following text in the
CGI RFC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
4.4. The Script Command Line
Some systems support a method for supplying an array of strings to
the CGI script. This is only used in the case of an 'indexed' HTTP
query, which is identified by a 'GET' or 'HEAD' request with a URI
query string that does not contain any unencoded "=" characters. For
such a request, the server SHOULD treat the query-string as a
search-string and parse it into words, using the rules
search-string = search-word *( "+" search-word )
search-word = 1*schar
schar = unreserved | escaped | xreserved
xreserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "," |
"$"
After parsing, each search-word is URL-decoded, optionally encoded in
a system-defined manner and then added to the command line argument
list.
If the server cannot create any part of the argument list, then the
server MUST NOT generate any command line information. For example,
the number of arguments may be greater than operating system or
server limits, or one of the words may not be representable as an
argument.
The script SHOULD check to see if the QUERY_STRING value contains an
unencoded "=" character, and SHOULD NOT use the command line
arguments if it does.
|
We checked the Apache source, and it complies exactly with the RFC:
if there is NO unescaped ‘=’ in the query string, the string is split on
‘+’ (encoded space) characters, urldecoded, passed to a function that
escapes shell metacharacters (the “encoded in a system-defined manner”
from the RFC) and then passes them to the CGI binary.
Unfortunately, it appears the PHP devs
forgot about this section of the RFC, and decided to remove the code which defends against it somewhere in 2004:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
From: Rasmus Lerdorf lerdorf.com>
Subject: [PHP-DEV] php-cgi command line switch memory check
Newsgroups: gmane.comp.php.devel
Date: 2004-02-04 23:26:41 GMT (7 years, 49 weeks, 3 days, 20 hours and 39 minutes ago)
In our SAPI cgi we have a check along these lines:
if (getenv("SERVER_SOFTWARE")
|| getenv("SERVER_NAME")
|| getenv("GATEWAY_INTERFACE")
|| getenv("REQUEST_METHOD")) {
cgi = 1;
}
if(!cgi) getopt(...)
As in, we do not parse command line args for the cgi binary if we are
running in a web context. At the same time our regression testing system
tries to use the cgi binary and it sets these variables in order to
properly test GET/POST requests. From the regression testing system we
use -d extensively to override ini settings to make sure our test
environment is sane. Of course these two ideas conflict, so currently our
regression testing is somewhat broken. We haven't noticed because we
don't have many tests that have GET/POST data and we rarely build the cgi
binary.
The point of the question here is if anybody remembers why we decided not
to parse command line args for the cgi version? I could easily see it
being useful to be able to write a cgi script like:
#!/usr/local/bin/php-cgi -d include_path=/path
...
?>
and have it work both from the command line and from a web context.
As far as I can tell this wouldn't conflict with anything, but somebody at
some point must have had a reason for disallowing this.
-Rasmus
|
Oddly enough, the
PHP documentation
still claims that PHP ignores command line arguments when run in CGI
mode. That documentation page also describes another mitigation used in
PHP: the REDIRECT_STATUS environment variable must be set, or PHP will
refuse to run as a CGI script. This means we cannot directly access
/cgi-bin/php5-cgi. This doesn’t really inconvenience us though, as
mentioned earlier :-)
Mitigation
NOTE: This section is now out of date! PHP has released versions PHP 5.3.12 and PHP 5.4.2, as well as an official mod_rewrite based workaround which fix the issue described in this post.
The new PHP release is
buggy. You can use their mitigation mod_rewrite rule, but the patch and
new released versions do not fix the problem. At the bottom we have
added a version of the PHP patch that fixes the obvious problem with the
patch merged in the recently released security update.
The following tarball contains two ways of mitigating the vulnerability.
See
CVE-2012-1823-mitigation.tar.gz
The first method is to have a small wrapper binary around the php-cgi binary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
#include
#include
#include
#include
#include
#define PHP_ORIG "/usr/bin/php5-cgi.orig" /* Original binary */
typedef union _sa_t {
struct sockaddr sa;
struct sockaddr_un sa_unix;
struct sockaddr_in sa_inet;
} sa_t;
int is_fastcgi(void)
{
sa_t sa;
socklen_t len = sizeof(sa);
return ( getpeername(0, (struct sockaddr *)&sa, &len) != 0 &&
errno == ENOTCONN );
}
int main(int argc, char **argv)
{
if ( !is_fastcgi() &&
(getenv("SERVER_SOFTWARE") ||
getenv("SERVER_NAME") ||
getenv("GATEWAY_INTERFACE") ||
getenv("REQUEST_METHOD") ) )
argv[1] = NULL;
execv(PHP_ORIG, argv);
}
|
The second way is a patch for PHP, which disables the parsing of arguments if
php-cgi is invoked as non-fastcgi cgi.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
Disable argument parsing when invoked as CGI (and NOT when invoked as
FastCGI.) This to prevent programs from passing arguments to php-cgi
via the query string as specified by RFC 3875. [1]
This patch may break CGI scripts that depend on arguments passed via
shebang arguments, eg. '#!/usr/bin/php-cgi -dmagic_quotes_gpc=Off',
but this is inherently unsafe, since these arguments may have come from
the network.
Backward compatibility could theoretically be faked by parsing the
shebang arguments from the file itself, but this leads to a circular
dependency since the script filename depends on the configuration which
may be changed in the shebang line of the file (due to cgi.fix-pathinfo.)
Index: sapi/cgi/cgi_main.c
===================================================================
--- sapi/cgi/cgi_main.c (revision 322984)
+++ sapi/cgi/cgi_main.c (working copy)
@@ -1552,7 +1552,7 @@
}
}
- while ((c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) {
+ if (!cgi) while ((c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) {
switch (c) {
case 'c':
if (cgi_sapi_module.php_ini_path_override) {
@@ -1801,7 +1801,7 @@
}
zend_first_try {
+ if (!cgi) while ((c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 1, 2)) != -1) {
switch (c) {
case 'T':
benchmark = 1;
|
UPDATE: There is now a
third option. This patch should be applied on top of the current PHP
source (including the security update that was supposed to fix the issue
described in this blog entry).
This patch fixes an obvious mistake made by the PHP devs. We have
verified that without this patch, the most recent PHP can still be
exploited.
UPDATE: as Christopher Kunz from http://www.php-security.net points out to us that If you use the the same (insecure) wrapper as in our example,
the patch below can still be circumvented by prepending a ‘+’, like
‘+-s’. Our solutions above should work just fine in this case.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c
index e6d011b..8e2d0ba 100644
--- a/sapi/cgi/cgi_main.c
+++ b/sapi/cgi/cgi_main.c
@@ -1809,7 +1809,7 @@ int main(int argc, char *argv[])
if(query_string = getenv("QUERY_STRING")) {
decoded_query_string = strdup(query_string);
php_url_decode(decoded_query_string, strlen(decoded_query_string));
+ if(*decoded_query_string == '-' && strchr(query_string, '=') == NULL) {
skip_getopt = 1;
}
free(decoded_query_string);
|
Disclosure timeline
13-01: Vulnerability discovered, used to pwn Nullcon Hackim 2012 scoreboard
13-01: We discuss the issue with Nullcon admins, find out it is a php 0day
17-01: We contact security@php.net with a full report and a suggested patch
01-02: We ask PHP to confirm receipt, state our intent to hand off the vulnerability to CERT if progress is not made
01-02: PHP forwards vulnerability report to PHP CGI maintainer
23-02: CERT acknowledges receipt of vulnerability and attempts to contact PHP.
05-04: We ask CERT for a status update
05-04: CERT responds saying that PHP is still working on a fix
20-04: We ask CERT to proceed with disclosure unless a patch is imminent
26-04: CERT prepares draft advisory.
02-05: CERT notifies us that PHP is testing a patch and would like more time. we agree.
03-05: Someone posts a
mirror of the internal PHP bug to reddit /r/netsec /r/opensource and /r/technology. It was apparently accidentaly marked public.
UPDATE: The PHP bug report is now public again
and is marked as closed. Go there for information on the patch and a
mod_rewrite based workaround. Do NOT rely on the homebrew workarounds in
the comments below, they do not provide adequate protection!
UPDATE2: PHP has released versions PHP 5.3.12 and PHP 5.4.2, as well as an official mod_rewrite based workaround which fix the issue described in this post.
UPDATE3: The new PHP
release is buggy. You can use their workaround, but the new releases and
their patch do not fix the issue. Use our mitigations for now.
UPDATE4: Added a new patch
which should be applied on top of PHP’s new security update. This patch
fixes the mistake made by PHP in their security update, and without it
your PHP will still be exploitable. Look at the bottom of the mitigation
section.
UPDATE5: We have received
word that new PHP updates with the revised fix will be released soon.
The issue that this problem was not properly fixed by the original
security update is being tracked as CVE-2012-2311. Updates to this blog
will be less frequent for the following hours due to it being nighttime /
early morning in the Netherlands.
We apologize for the mess this blogpost has become; if anything is
unclear please don’t hesitate to ask in the comments. We don’t want to
reorganise the blog post too much so people can still find what they
want.
source from: eindbazen.net