Title:    PHP parse_str() arbitrary variable overwrite
      Vendor:    http://www.php.net/
    Advisory:    http://www.acid-root.new.fr/advisories/14070612.txt
      Author:    DarkFig < gmdarkfig (at) gmail (dot) com >
 Released on:    2007/06/12
 Last update:    2007/06/15
  Risk level:    Medium / High
         CVE:    CVE-2007-3205




 [I].BACKGROUND

 [Quote from php.net] PHP is a popular open-source programming
 language used primarily for developing server-side applications
 and dynamic web content, and more recently, other software. The
 name is a recursive acronym for "PHP: Hypertext Preprocessor".
 This is actually a retronym; see history of PHP.[/quote]

 While I was coding the new version of phpsploitclass, I was
 reading the manual of parse_url(), then I saw the parse_str()
 function. I decided to see how it works. During some test that
 I did, I discovered a vulnerability which can be exploited to
 overwrite some variables.



 [II].MANUAL

 void parse_str ( string $str [, array &$arr] )

 Parses str as if it were the query string passed via a URL and
 sets variables in the current scope. If the second parameter arr
 is present, variables are stored in this variable as array
 elements instead.

 Note: Support for the optional second parameter was added in
 PHP 4.0.3. 
 
 Note: To get the current QUERY_STRING, you may use the variable
 $_SERVER['QUERY_STRING']. Also, you may want to read the section
 on variables from outside of PHP. 

 Note: The magic_quotes_gpc setting affects the output of this
 function, as parse_str() uses the same mechanism that PHP uses
 to populate the $_GET, $_POST, etc. variables. 



 [III].SOURCE CODE

 --- ./ext/standard/string.c ---
 4025. /*
 4025. {{{ proto void parse_str(string encoded_string [, array result])
 4026. Parses GET/POST/COOKIE data and sets global variables
 4026. */
 4027. PHP_FUNCTION(parse_str)
 4028. {
 4029.   zval **arg;
 4030.   zval **arrayArg;
 4031.   zval *sarg;
 4032.   char *res = NULL;
 4033.   int argCount;
 4034.   
 4035.   argCount = ZEND_NUM_ARGS();
 4036.   if (argCount < 1 ||
 4036.       argCount > 2 ||
 4036.       zend_get_parameters_ex(argCount,&arg,&arrayArg) == FAILURE)
 4036.   {
 ####.          /* Not enough or too many args */
 4037.   	WRONG_PARAM_COUNT;
 4038.   }
 4039. 
 4040.  convert_to_string_ex(arg);
 4041.  sarg = *arg;
 4042.  if (Z_STRVAL_P(sarg) && *Z_STRVAL_P(sarg)) {
 4043.  	res = estrndup(Z_STRVAL_P(sarg), Z_STRLEN_P(sarg));
 4043.
 ####.  /* Allocate Z_STRLEN_P(sarg)+1 bytes of memory and copy
 ####.     Z_STRLEN_P(sarg) bytes from Z_STRVAL_P(sarg) to the
 ####.     newly allocated block */
 4044.  }
 4045.
 ####.  /* parse_str(argv1) */
 4046.  if (argCount == 1)
 4046.  {
 4047.  	zval tmp;
 4048.  	Z_ARRVAL(tmp) = EG(active_symbol_table);
 4049.
 ####.  /* The problem is here, there is no conditions before setting
 ####.     the variable. If a variable already exists, it will overwrite it */
 4049.
 4050.  	sapi_module.treat_data(PARSE_STRING, res, &tmp TSRMLS_CC);
 4051.  }
 ####.  /* parse_str(argv1,argv2) */
 4051.  else
 4051.  {
 4052.  	/* Clear out the array that was passed in. */
 4053.  	zval_dtor(*arrayArg);
 4054.  	array_init(*arrayArg);
 4055.  	
 4056.  	sapi_module.treat_data(PARSE_STRING, res, *arrayArg TSRMLS_CC);
 4057.  }
 4058. }



 [IV].EXPLANATIONS

 As you can see in the manual, the user who want to use this
 function is not prevented against overwriting. That's why I
 think that overwriting is not wanted, they forgot to check it.
 Simple proof of concept:

 <?php
 
 # ?var=new
 ###########
 $var   = 'init';                     #
 parse_str($_SERVER['QUERY_STRING']); #
 print $var;                          # new

 # ?array[]=new                       # Array
 ##############                       # (
 $array = array('init');              #    [0] => init
 parse_str($_SERVER['QUERY_STRING']); #    [1] => new
 print_r($array);                     # )

 # ?array=new
 ############                                # Array
 $array = array('init');                     # (
 parse_str($_SERVER['QUERY_STRING'],$array); #    [array] => new
 print_r($array);                            # )

 ?>

 This type of vulnerability can open a door to many vulnerabilities,
 that's why it's difficult to define the risk level. Unlike extract()
 there is no option such as EXTR_SKIP which will define what to do
 if there is a collision. So if you want to secure your code, don't
 use this function. I didn't contacted the php team but maybe they
 will release a fix for this vulnerability.



 [V].POST REPLIES

 ===========================================================
 From: < admin (at) batznet (dot) com >

 Hardened-PHP and PHP with suhosin are also affected to this
 issue.
 ===========================================================
 From: Steven M. Christey < coley (at) mitre (dot) org >

 Nice find, although it's not really clear to me whether this
 is intended functionality or not. I assume it's not intended
 by Hardened-PHP and Suhosin, at least :)

 You didn't mention this, but even if register_globals is
 disabled, this seems to work, at least in my PHP 4.4.4.

 Try the code below with:

 ?var=new
 --> generates an error (display_errors=1) that var2 is
     undefined

 ?var2=new
 --> prints "var2 = new"

 <?php
 $var = 'init';                       #
 parse_str($_SERVER['QUERY_STRING']); #
 print "var = $var<p>\n";             # new
 print "var2 = $var2<p>\n";           # new
 ?>

 - Steve
 ===========================================================
 From: Chuck Swiger < cswiger (at) mac (dot) com >

 On Jun 12, 2007, at 4:53 PM, Steven M. Christey wrote:
 > Nice find, although it's not really clear to me whether
 > this is intended functionality or not. I assume it's not
 > intended by Hardened-PHP and Suhosin, at least :)

 Agreed-- using parse_str() against the query passed in is
 going to let one overwrite arbitrary local variables in the
 PHP script just by crafting the arguments in the URL
 appropriately.

 It seems to impossible to use the single-argument variant of
 parse_str() against QUERY_STRING safely. One ought to always
 use the two-argument form of parse_str() and put the variables
 into an array, and then selectively pull them out of that
 into variables in the local context while doing any necessary
 sanity checking of their values at the same time.

 > You didn't mention this, but even if register_globals is
 > disabled, this seems to work, at least in my PHP 4.4.4.

 I get the same results as you've described below using both:

 Apache/2.0.59 (FreeBSD) DAV/2 PHP/4.4.7 with Suhosin-Patch
 ...and:
 Apache/2.2.4 (Darwin) PHP/5.2.3
 ...so this behavior seems to be intended by design.

 > Try the code below with:
 >
 > ?var=new
 >
 > --> generates an error (display_errors=1) that var2 is
 >     undefined
 >
 > ?var2=new
 >
 > --> prints "var2 = new"

 -- 
 -Chuck
 ===========================================================
 From: darkfig < gmdarkfig (at) gmail (dot) com >

 You're all right. I added your replies in the advisory (if
 you want that I retire them, feel free to contact me).
 ===========================================================



 [VI].GREETZ

 benjilenoob, ddx39, lorenzo, romano, shaka, sparah