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