Bandit: nivel 12 → 13
Un hexdump de un fichero comprimido una y otra vez. Lo reconstruimos con xxd -r, identificamos cada capa y escribimos un script en bash que descomprime hasta llegar a la contraseña.
En el nivel anterior bastaba con deshacer una sola transformación. Este nivel es el primero de Bandit que de verdad se hace largo: el fichero es un hexdump (una representación en hexadecimal) de otro fichero que ha sido comprimido muchas veces, mezclando formatos distintos. Hay que darle la vuelta al hexdump, ir pelando capas como una cebolla y, ya que no sabemos cuántas hay, automatizarlo con un pequeño script en bash.
Las contraseñas aparecen como
[REDACTED], siguiendo las normas de OverTheWire. En este nivel tampoco publicamos el hexdump completo: con él cualquiera podría reconstruir el fichero y sacar la contraseña.
Conexión al nivel 12
1
2
$ sshpass -p '[REDACTED]' ssh bandit12@bandit.labs.overthewire.org -p 2220
bandit12@bandit:~$ export TERM=xterm
El enunciado
Según el enunciado del nivel 13, la contraseña está en el fichero data.txt, que es un hexdump de un fichero que ha sido comprimido repetidamente.
1
2
3
4
5
6
bandit12@bandit:~$ ls
data.txt
bandit12@bandit:~$ cat data.txt
00000000: 1f8b 0808 b83e b86a 0203 6461 7461 322e .....>.j..data2.
00000010: 6269 6e00 0146 02b9 fd42 5a68 3931 4159 bin..F...BZh91AY
[...]
Cómo se lee un hexdump
Cada línea tiene tres columnas:
| Columna | Ejemplo | Qué es |
|---|---|---|
| Desplazamiento (offset) | 00000000: | Posición del primer byte de la línea, en hexadecimal |
| Bytes en hexadecimal | 1f8b 0808 b83e ... | 16 bytes por línea, agrupados de dos en dos |
| Vista ASCII | .....>.j..data2. | Los mismos bytes como texto; los no imprimibles salen como . |
Solo con estas dos líneas ya podemos sacar pistas:
1f8bal principio es la firma (magic bytes) de gzip. Casi todos los formatos de fichero empiezan con unos bytes fijos que los identifican, y así es comofilesabe qué tipo de fichero es sin mirar la extensión.- En la columna ASCII aparece
data2.bin: gzip guarda en su cabecera el nombre del fichero original. - Y en la segunda línea asoma
BZh, la firma de bzip2. Es decir, ya estamos viendo la siguiente capa: dentro del gzip hay un bzip2.
Del hexdump al fichero: xxd -r
Copiamos el hexdump completo a nuestra máquina, lo guardamos en un fichero (hexdump_data) y lo devolvemos a su forma binaria con xxd:
1
$ xxd -r hexdump_data compressed_file
xxd es la herramienta que genera hexdumps con este formato exacto, y con -r (reverse) hace lo contrario: lee el hexdump y escribe los bytes originales. El primer argumento es el fichero de entrada y el segundo, el de salida.
Comprobamos qué hemos obtenido:
1
2
$ file compressed_file
compressed_file: gzip compressed data, was "data2.bin", last modified: Sat Sep 26 21:52:56 2026, max compression, from Unix, original size modulo 2^32 582
Lo que intuíamos por los magic bytes: un gzip que contiene data2.bin.
Mirar dentro con 7z
1
2
3
4
5
6
7
$ 7z l compressed_file
[...]
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2026-09-26 23:52:56 ..... 582 615 data2.bin
------------------- ----- ------------ ------------ ------------------------
2026-09-26 23:52:56 582 615 1 files
7z l (list) muestra el contenido de un archivo comprimido sin extraerlo. La gran ventaja de 7z en este nivel es que entiende casi todos los formatos (gzip, bzip2, tar, zip…), así que no tenemos que preocuparnos de qué tipo es cada capa: 7z x (extract) sabe descomprimirlas todas.
Si extraemos compressed_file con 7z x, obtenemos data2.bin, y al hacerle 7z l… es otro fichero comprimido. Podríamos seguir así a mano, capa por capa, pero no sabemos cuántas hay. Es el momento perfecto para un script.
El script: descomprimir hasta que no quede nada
El script parte de un fichero llamado data.gz, así que primero renombramos el que hemos reconstruido:
1
$ mv compressed_file data.gz
Y este es el script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
function ctrl_c(){
echo -e "\n\n[!] Saliendo...\n"
exit 1
}
# Ctrl+C
trap ctrl_c INT
first_file_name="data.gz"
decompressed_file_name="$(7z l data.gz | tail -n 3 | head -n 1 | awk 'NF{print $NF}')"
7z x $first_file_name &>/dev/null
while [ $decompressed_file_name ]; do
echo -e "\n[+] Nuevo archivo descomprimido: $decompressed_file_name"
7z x $decompressed_file_name &>/dev/null
decompressed_file_name="$(7z l $decompressed_file_name 2>/dev/null | tail -n 3 | head -n 1 | awk 'NF{print $NF}')"
done
Vamos por partes.
Salir limpiamente con Ctrl+C
1
2
function ctrl_c(){ ... }
trap ctrl_c INT
Cuando pulsas Ctrl+C, el sistema envía al proceso la señal SIGINT (interrupt). trap le dice a bash: “cuando llegue esa señal, en lugar de morir sin más, ejecuta la función ctrl_c”. Aquí la función solo muestra un mensaje y sale con exit 1 (código de error), pero es una buena costumbre en cualquier script con bucles: si algo se queda atascado, sales de forma controlada.
Sacar el nombre del fichero que hay dentro
1
7z l data.gz | tail -n 3 | head -n 1 | awk 'NF{print $NF}'
Esta línea es el corazón del script. Mira la salida de 7z l de arriba: la línea que nos interesa, la que tiene el nombre del fichero, es la tercera empezando por el final.
| Paso | Qué hace |
|---|---|
tail -n 3 | Se queda con las 3 últimas líneas del listado |
head -n 1 | De esas, se queda con la primera: la del fichero |
awk 'NF{print $NF}' | Imprime el último campo de la línea: el nombre |
$NF ya lo conocemos del nivel 10 → 11: el último campo. La novedad es el NF que va antes de las llaves. En awk, lo que va antes de {...} es una condición: el bloque solo se ejecuta si se cumple. NF vale 0 en una línea vacía, y 0 cuenta como falso, así que NF{print $NF} significa “imprime el último campo, pero solo si la línea no está vacía”. Ese detalle es justo lo que hace que el bucle termine, como vamos a ver.
El bucle
1
2
3
while [ $decompressed_file_name ]; do
...
done
[ $variable ] es verdadero mientras la variable tenga contenido. En cada vuelta el script:
- Muestra el nombre del fichero que acaba de aparecer.
- Lo extrae con
7z x, mandando toda la salida a/dev/nullcon&>(stdout y stderr a la vez; en el nivel 6 → 7 solo redirigíamos stderr con2>). - Lista su contenido con
7z lpara sacar el nombre de la siguiente capa.
Cuando llegamos al último fichero, que ya no está comprimido, 7z l falla: no imprime ningún listado, solo un aviso de error al final. Las líneas que llegan a head -n 1 están vacías, awk no imprime nada gracias a la condición NF, la variable se queda vacía y el bucle termina.
Un pequeño detalle de robustez: es buena práctica entrecomillar las variables,
[ -n "$decompressed_file_name" ]y7z x "$decompressed_file_name". Aquí funciona sin comillas porque los nombres no tienen espacios, pero con un nombre comomi archivo.gzel script se rompería.
Ejecutarlo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ bash descomprimir.sh
[+] Nuevo archivo descomprimido: data2.bin
[+] Nuevo archivo descomprimido: data2
[+] Nuevo archivo descomprimido: data4.bin
[+] Nuevo archivo descomprimido: data5.bin
[+] Nuevo archivo descomprimido: data6.bin
[+] Nuevo archivo descomprimido: data6
[+] Nuevo archivo descomprimido: data8.bin
[+] Nuevo archivo descomprimido: data9.bin
Fíjate en data2 y data6, sin extensión: son capas de bzip2, que a diferencia de gzip no guarda el nombre del fichero original, así que 7z lo deduce quitando la extensión.
El último, data9.bin, ya es texto plano:
1
2
$ cat data9.bin
The password is [REDACTED]
Otras formas
1. Traer el fichero con scp en lugar de copiar y pegar
1
$ sshpass -p '[REDACTED]' scp -P 2220 bandit12@bandit.labs.overthewire.org:data.txt hexdump_data
Copiar y pegar un hexdump de 40 líneas funciona, pero basta un carácter mal copiado para que xxd -r genere un fichero corrupto. scp copia ficheros a través de SSH, con la misma conexión que ya usamos para entrar. Ojo a un detalle que confunde a todo el mundo: en ssh el puerto va con -p minúscula, pero en scp es -P mayúscula.
2. Hacerlo todo en el servidor, capa a capa, con file
Bandit tiene xxd, gzip, bzip2 y tar, así que se puede resolver sin salir del servidor. Como en nuestro home no tenemos permiso de escritura, trabajamos en un directorio temporal:
1
2
3
4
bandit12@bandit:~$ cd $(mktemp -d)
bandit12@bandit:/tmp/tmp.XXXXXXXXXX$ cp ~/data.txt .
bandit12@bandit:/tmp/tmp.XXXXXXXXXX$ xxd -r data.txt > data
bandit12@bandit:/tmp/tmp.XXXXXXXXXX$ file data
mktemp -d crea un directorio con un nombre aleatorio en /tmp e imprime su ruta, y $(...) (sustitución de comandos) mete esa ruta directamente como argumento de cd.
A partir de ahí, en cada capa se mira el tipo con file y se usa la herramienta adecuada: gzip -d para gzip, bzip2 -d para bzip2 y tar -xf para tar. Un detalle: gzip -d y bzip2 -d se quejan si el fichero no tiene la extensión esperada, así que antes hay que renombrarlo (mv data data.gz). En este fichero el orden de capas es:
| Capa | Formato |
|---|---|
| 1 | gzip |
| 2 | bzip2 |
| 3 | gzip |
| 4 | tar |
| 5 | tar |
| 6 | bzip2 |
| 7 | tar |
| 8 | gzip |
| 9 | Texto ASCII (la contraseña) |
Hacerlo a mano es la forma más didáctica de entender qué pasa en cada paso, aunque es tediosa.
3. Un script que decide con file
Si no tienes 7z, puedes automatizar el método anterior dejando que file decida qué herramienta usar en cada capa:
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
f="data"
while true; do
tipo=$(file -b "$f")
echo "[+] $f: ${tipo%%,*}"
case "$tipo" in
gzip*) mv "$f" "$f.gz"; gzip -d "$f.gz" ;;
bzip2*) mv "$f" "$f.bz2"; bzip2 -d "$f.bz2" ;;
*tar*) nuevo=$(tar -tf "$f"); tar -xf "$f"; rm "$f"; f="$nuevo" ;;
*) cat "$f"; break ;;
esac
done
file -b(brief) da solo la descripción, sin el nombre del fichero delante.${tipo%%,*}recorta todo lo que hay a partir de la primera coma, para mostrar