root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat seal.md
// writeups

Seal

10 Jul 2021 · 20 min read · root access
Seal - maquina de Hack The Box

Executive summary — Seal combines open self-registration on an internal GitBucket instance (the Scala-based GitHub clone that explains the Jetty banner on port 8080) with a Tomcat credential that stayed alive in git history despite being "reverted" from the file. That credential only becomes useful thanks to a path normalization mismatch between nginx and Tomcat (the ; character is interpreted differently by each), which breaks a TLS client-certificate restriction and opens the door to Tomcat Manager for a WAR-deployment RCE. The jump to the second account abuses an automated Ansible backup (synchronize + copy_links: yes) over a world-writable directory, planting a symlink that leaks luis's SSH key. The final root step needs no exploit at all: a wildcard in a sudo rule for ansible-playbook.

PlatformHack The Box
Operating systemLinux
DifficultyMedium
StatusRetired
Target IP10.129.95.190

Attack map

[8080] Jetty -> 401 with no WWW-Authenticate
   │  fuzzing reveals /register and /signin -> GitBucket (the real backend)
   ▼
[GitBucket] self-service registration -> repos root/infra, root/seal_market
   │  git log -p on tomcat-users.xml: reverted credential, never rotated
   ▼
[nginx vs Tomcat] ';' in the path -> different normalization -> bypasses
                  ssl_verify_client on /manager/html
   ▼
[Tomcat Manager] tomcat:<leaked credential> -> WAR deploy -> RCE
   ▼
[RCE] tomcat
   │  Ansible backup (synchronize, copy_links=yes) over a world-writable
   │  directory -> symlink to luis's ~/.ssh
   ▼
[SSH] luis  (user.txt)
   │  sudo NOPASSWD /usr/bin/ansible-playbook *  (wildcard)
   ▼
[ROOT] arbitrary playbook executed as root  (root.txt)

1. Reconnaissance and enumeration

Port scan

nmap -p- --min-rate 5000 -T4 -oN nmap_all.txt 10.129.95.190

Three open ports: 22 (SSH), 443 (nginx, over TLS) and 8080 (HTTP, Jetty banner).

PORT     STATE SERVICE
22/tcp   open  ssh
443/tcp  open  https
8080/tcp open  http-alt

On 8080, a plain request to the root path returns 401 with no WWW-Authenticate header — an unusual detail suggesting the app handles its own authentication instead of delegating to the server's Basic/Digest.

TLS certificate and domain

The certificate served on 443 reveals the real domain and organization:

openssl s_client -connect 10.129.95.190:443 -servername seal.htb </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer
subject=CN = seal.htb, O = "Seal Pvt Ltd"
echo "10.129.95.190 seal.htb" | sudo tee -a /etc/hosts

https://seal.htb serves the "Seal Market" corporate site, with no apparent functionality beyond static content.

Port 8080 — GitBucket via fuzzing

With the generic 401 as the only clue, routes on 8080 are fuzzed, filtering out anything that still returns 401:

ffuf -u http://10.129.95.190:8080/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt -fc 401
/register              [Status: 200]
/signin                [Status: 200]

Everything else still returns 401 — only registration and login are open. The /signin HTML identifies the product: GitBucket, a GitHub clone written in Scala and deployed on Jetty — which explains both the port's banner and the 401 with no WWW-Authenticate (GitBucket handles its own form-based login, not HTTP Basic).

Registration and repository enumeration

The registration form asks for userName, password, fullName, mailAddress, description and fileId. That's enough to create an account and authenticate, keeping the session in a cookie jar:

curl -s -c gb.txt -d 'userName=pentest01&password=Passw0rd!23&fullName=Pentest+User&mailAddress=pentest01@seal.htb&description=&fileId=' http://10.129.95.190:8080/register

curl -s -b gb.txt -c gb.txt -i -d 'userName=pentest01&password=Passw0rd!23' http://10.129.95.190:8080/signin
HTTP/1.1 302 Found
Location: /

The 302 to / confirms an authenticated session. With a valid cookie, a simple href grep over the home page lists users and repositories visible to any freshly created account:

curl -s -b gb.txt http://10.129.95.190:8080/ | grep -oE 'href="/[a-zA-Z0-9_.-]+(/[a-zA-Z0-9_.-]+)?"' | sort -u
/alex
/luis
/root
/root/infra
/root/seal_market

Three users (alex, luis, root) and two infrastructure repos under root: infra and seal_market. Both get cloned with the freshly created account (self-signed certificate, hence sslVerify=false):

git -c http.sslVerify=false clone http://pentest01:Passw0rd!23@10.129.95.190:8080/root/infra.git
git -c http.sslVerify=false clone http://pentest01:Passw0rd!23@10.129.95.190:8080/root/seal_market.git

Git history — leaked Tomcat credential

The seal_market history (git log --oneline) includes, among others, two telling commits in chronological order: Adding tomcat configuration followed later by Updating tomcat configuration — the classic "add something sensitive, then revert it" pattern, which always justifies checking a file's full history rather than just its current state:

git log -p --all -- tomcat/tomcat-users.xml
-  <user username="tomcat" password="42MrHBf*z8{Z%" roles="manager-gui,admin-gui"/>
+  <user username="tomcat" password="<must-be-changed>" roles="manager-gui,admin-gui"/>

The current file only has the <must-be-changed> placeholder, but git never forgets: the reverted diff is still in the history. Transcription note: the attribute captured in the diff ends in % (...z8{Z%); the credential that actually authenticates against Tomcat is tomcat:42MrHBf*z8{Z} (likely a copy artifact on the final character — the original capture couldn't be recovered to confirm it 100%) — this second form is used for the rest of the write-up since it's the one that actually works.

nginx configuration — the bypass surface

The same infra repository includes the nginx sites-enabled config for the 443 vhost: it asks for a TLS client certificate optionally, and only enforces it for Tomcat's administrative paths — everything else reaches the app unrestricted:

server {
    listen 443 ssl;
    ssl_verify_client optional;
    ...
    location /manager/html {
        if ($ssl_client_verify != SUCCESS) { return 403; }
        proxy_pass http://localhost:8000;
    }
    location /admin/dashboard {
        if ($ssl_client_verify != SUCCESS) { return 403; }
        proxy_pass http://localhost:8000;
    }
    location /host-manager/html {
        if ($ssl_client_verify != SUCCESS) { return 403; }
        proxy_pass http://localhost:8000;
    }
    location / {
        proxy_pass http://localhost:8000;
    }
}

The direct block is confirmed:

curl -sk -o /dev/null -w "%{http_code}\n" https://seal.htb/manager/html
# 403

nginx demands a client certificate we don't have. Without the Tomcat credential and without breaking that restriction, the Manager is unreachable — until the next phase.

🔒 Clearance required

This content is Root Access only. Everything else on the site — the free tier, the whole public library — stays open.

See Root Access plans

No account? Create one free then upgrade from your console.