Entrada

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.

Bandit: nivel 12 → 13

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:

ColumnaEjemploQué es
Desplazamiento (offset)00000000:Posición del primer byte de la línea, en hexadecimal
Bytes en hexadecimal1f8b 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:

  • 1f8b al 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 como file sabe 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.

PasoQué hace
tail -n 3Se queda con las 3 últimas líneas del listado
head -n 1De 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:

  1. Muestra el nombre del fichero que acaba de aparecer.
  2. Lo extrae con 7z x, mandando toda la salida a /dev/null con &> (stdout y stderr a la vez; en el nivel 6 → 7 solo redirigíamos stderr con 2>).
  3. Lista su contenido con 7z l para 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" ] y 7z x "$decompressed_file_name". Aquí funciona sin comillas porque los nombres no tienen espacios, pero con un nombre como mi archivo.gz el 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:

CapaFormato
1gzip
2bzip2
3gzip
4tar
5tar
6bzip2
7tar
8gzip
9Texto 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
Esta entrada está licenciada bajo CC BY 4.0 por el autor.