Résumé
This write-up covers the discovery of a hidden vhost (grafana.planning.htb), exploitation of a Grafana vulnerability (CVE-2024-9264), obtaining a reverse shell, credential recovery, SSH connection to enzo, and then escalation to root via a vulnerable cron.
Recon — nmap
We do an nmap to scan service on 10.10.11.68
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
Add the output on /etc/hosts
10.10.11.68 planning.htb
Discovering a hidden vhost with ffuf
We do a FUZZ to looking for another DNS
┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -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/bitquark-subdomains-top100000.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: 68ms]
Explanation:
The -fs 178 option filters responses of size 178 (default page returned for non-existent hosts).
The server responds differently for the “grafana” subdomain.
Result:
An active vhost has been discovered: grafana.planning.htb
The HTTP response has a 302 code (redirection), which indicates the presence of an accessible service behind this vhost
grafana.planning.htb (302) — add on /etc/hosts.
Exploitation — Grafana (CVE-2024-9264)
Check the exposed web page.
Identify the version of Grafana and search for any known vulnerabilities (LFI, RCE, default credentials).
We find a public exploit for CVE-2024-9264 allowing RCE via a crafted payload.
We make a git clone of the git project on our attacker machine kali
Then we create shell.sh and mount a python http.server 8000
Then we create a listening port on port 4444 and then we launch the exploit
Rev shell
Then we try to found creds or sensitive information
After a large énumeration i found creds of enzo on /var/lib/grafana env
# Example location found:
/var/lib/grafana/.env
# read
cat /var/lib/grafana/.env
SSH → enzo
Then we connect on ssh enzo
And found the user flag
? How it’s work — CVE-2024-9264 (Grafana)
Grafana's experimental SQL Expressions feature allows the evaluation of duckdb queries containing user input.
These queries are not sufficiently sanitized before being passed to duckdb, resulting in command injection and a local file inclusion vulnerability. Any user with VIEWER privileges or higher is capable of executing this attack.
The duckdb binary must be present in Grafana's $PATH for this attack to work; by default, this binary is not installed in Grafana distributions.
Le PoC abuse de la source de données « Expression » de Grafana (DuckDB dans le backend) : il envoie une expression SQL (via l'API POST /api/ds/query?ds_type=__expr__&expression=true) que Grafana évalue avec DuckDB.
Deux mécanismes sont utilisés :
- read_blob(path) (DuckDB + extension) pour lire un fichier distant et renvoyer son contenu.
- installation et chargement de l’extension
shellfspuis usage deread_csv(“... |”)pour exécuter une commande et rediriger sa sortie vers un fichier temporaire, que le PoC relit ensuite viaread_blob
So the exploit flow: inject a malicious DuckDB expression → DuckDB (on the server) performs operations on the FS / loads extension → the attacker reads the result via the JSON response.
Sources: Grafana advisory, NVD, public proof of concept, and analyses
Escalation → root via Cron
Now we do a privilege escalation on root user
Then After a large énumeration i found password on /otp/crontabs/crontab.db
I also found a open port on 127.0.0.1:8000
I am doing port forwarding.
Then i connect on it http://127:0.0.1:8000 with creds wich i found before
Now I am on the last step, which is to create a new cronjob to escalate to the root user.
The job added in the Crontab interface executes the following command:
cp /bin/bash /tmp/bash && chmod u+s /tmp/bash
This command copies the bash executable to the /tmp directory, then applies the SUID bit to this binary. Since the cron job runs with root privileges, the new /tmp/bash file belongs to root and is marked SUID
Consequence: any user can run /tmp/bash -p and obtain a shell with root privileges, allowing complete privilege escalation on the system
Then i run it
And do ./bash -p to get root bash and found root flag
Conclusion & recommandations
Update Grafana and apply patches related to CVE-2024-9264.
Affected versions:
- Grafana OSS and Enterprise versions 11.0.0 - 11.0.5, 11.1.0 - 11.1.6, and 11.2.0 - 11.2.1.
Corrected versions:
- 11.0.5+security-01 and higher
Grafana has released special versions to fix this vulnerability. To analyze the patch, the following commands can be used to compare the changes:
git checkout v11.0.5+security-01git diff 0421a8911cfc05a46c516fd9d033a51e52e51afe 70316b3e1418c9054017047e63c1c96abb26f495
Cela révèle que la fonctionnalité Expressions SQL a simplement été supprimée des versions vulnérables.
+++ b/pkg/expr/sql/db.go
@@ -0,0 +1,26 @@
+package sql
+
+import (
+ "errors"
+
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+)
+
+type DB struct {
+}
+
+func (db *DB) TablesList(rawSQL string) ([]string, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (db *DB) RunCommands(commands []string) (string, error) {
+ return "", errors.New("not implemented")
+}
+
+func (db *DB) QueryFramesInto(name string, query string, frames []*data.Frame, f *data.Frame) error {
+ return errors.New("not implemented")
+}
+
+func NewInMemoryDB() *DB {
+ return &DB{}
+}
@@ -85,7 +84,7 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V
rsp := mathexp.Results{}
- duckDB := duck.NewInMemoryDB()
+ duckDB := sql.NewInMemoryDB()
var frame = &data.Frame{}
err := duckDB.QueryFramesInto(gr.refID, gr.query, allFrames, frame);
if err != nil {
The patch completely removes SQL expressions, preventing any possibility of exploitation.