Hack The Box: Seal


Jump Ahead: EnumGetting a FootholdUserRootResources

TL;DR;

To solve this machine, we begin by enumerating open ports – finding ports 22, 443, and 8080 open. From port 443, we find the backend webserver is Apache Tomcat based on an error message. On port 8080, we find GitBucket, and find a pair of credentials for Apache Tomcat. Due to protections of the frontend webserver, we are unable to use the credentials to log in. After some research, we are able to evade the frontend protections to gain access to Tomcat. From here, we are able to upload and get a reverse shell as tomcat. After monitoring processes for scheduled jobs, we find the system uses an automation platform. After exploiting the automation platform, we are able to get the SSH private key of luis, as well as read user.txt. After minimal local enumeration as luis, we learn we are able to exploit the automation platform again to get a shell as root – thus granting access to root.txt.

Enumeration

Like all machines, we begin by enumerating open ports using nmap – finding ports 22, 443, and 8080 open.

$ sudo nmap -v- --min-rate 3000 -p- $RHOST
[...]
$ sudo nmap -sV -A -p 22,443,8080 -oA enum/nmap/tcp-scripts $RHOST
# Nmap 7.91 scan initiated Sun Jul 11 22:01:50 2021 as: nmap -sV -A -p 22,443,8080 -oA enum/nmap/tcp-scripts 10.129.182.60
Nmap scan report for 10.129.182.60
Host is up (0.047s latency).

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: Mon, 12 Jul 2021 03:02:03 GMT
|     Set-Cookie: JSESSIONID=node0v5t0xmmz1ocg15f3v0v24qozy3.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: Mon, 12 Jul 2021 03:02:02 GMT
|     Set-Cookie: JSESSIONID=node0ixdhunkvp0jhqhld08ztydqk1.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: Mon, 12 Jul 2021 03:02:02 GMT
|     Set-Cookie: JSESSIONID=node01r1jhx98vfzk71raz8i17rtk882.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=7/11%Time=60EBB0AB%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,F3,"HTTP/1\.1\x20401\x20Unauthorized\r\nDate:\x20Mon,\x2012\x2
SF:0Jul\x202021\x2003:02:02\x20GMT\r\nSet-Cookie:\x20JSESSIONID=node0ixdhu
SF:nkvp0jhqhld08ztydqk1\.node0;\x20Path=/;\x20HttpOnly\r\nExpires:\x20Thu,
SF:\x2001\x20Jan\x201970\x2000:00:00\x20GMT\r\nContent-Type:\x20text/html;
SF:charset=utf-8\r\nContent-Length:\x200\r\n\r\n")%r(HTTPOptions,109,"HTTP
SF:/1\.1\x20200\x20OK\r\nDate:\x20Mon,\x2012\x20Jul\x202021\x2003:02:02\x2
SF:0GMT\r\nSet-Cookie:\x20JSESSIONID=node01r1jhx98vfzk71raz8i17rtk882\.nod
SF:e0;\x20Path=/;\x20HttpOnly\r\nExpires:\x20Thu,\x2001\x20Jan\x201970\x20
SF:00:00:00\x20GMT\r\nContent-Type:\x20text/html;charset=utf-8\r\nAllow:\x
SF:20GET,HEAD,POST,OPTIONS\r\nContent-Length:\x200\r\n\r\n")%r(RTSPRequest
SF:,AD,"HTTP/1\.1\x20505\x20Unknown\x20Version\r\nContent-Type:\x20text/ht
SF:ml;charset=iso-8859-1\r\nContent-Length:\x2058\r\nConnection:\x20close\
SF:r\n\r\n<h1>Bad\x20Message\x20505</h1><pre>reason:\x20Unknown\x20Version
SF:</pre>")%r(FourOhFourRequest,F4,"HTTP/1\.1\x20401\x20Unauthorized\r\nDa
SF:te:\x20Mon,\x2012\x20Jul\x202021\x2003:02:03\x20GMT\r\nSet-Cookie:\x20J
SF:SESSIONID=node0v5t0xmmz1ocg15f3v0v24qozy3\.node0;\x20Path=/;\x20HttpOnl
SF:y\r\nExpires:\x20Thu,\x2001\x20Jan\x201970\x2000:00:00\x20GMT\r\nConten
SF:t-Type:\x20text/html;charset=utf-8\r\nContent-Length:\x200\r\n\r\n")%r(
SF:Socks5,C3,"HTTP/1\.1\x20400\x20Illegal\x20character\x20CNTL=0x5\r\nCont
SF:ent-Type:\x20text/html;charset=iso-8859-1\r\nContent-Length:\x2069\r\nC
SF:onnection:\x20close\r\n\r\n<h1>Bad\x20Message\x20400</h1><pre>reason:\x
SF:20Illegal\x20character\x20CNTL=0x5</pre>")%r(Socks4,C3,"HTTP/1\.1\x2040
SF:0\x20Illegal\x20character\x20CNTL=0x4\r\nContent-Type:\x20text/html;cha
SF:rset=iso-8859-1\r\nContent-Length:\x2069\r\nConnection:\x20close\r\n\r\
SF:n<h1>Bad\x20Message\x20400</h1><pre>reason:\x20Illegal\x20character\x20
SF:CNTL=0x4</pre>")%r(RPCCheck,C7,"HTTP/1\.1\x20400\x20Illegal\x20characte
SF:r\x20OTEXT=0x80\r\nContent-Type:\x20text/html;charset=iso-8859-1\r\nCon
SF:tent-Length:\x2071\r\nConnection:\x20close\r\n\r\n<h1>Bad\x20Message\x2
SF:0400</h1><pre>reason:\x20Illegal\x20character\x20OTEXT=0x80</pre>");
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 4.15 - 5.6 (95%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.3 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 8080/tcp)
HOP RTT      ADDRESS
1   47.12 ms 10.10.14.1
2   47.18 ms 10.129.182.60

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Jul 11 22:02:23 2021 -- 1 IP address (1 host up) scanned in 32.42 seconds

Using nikto to enumerate the webserver on port 443, we find the TLS certificate is configured for the domain name seal.htb (which we add to /etc/hosts). Additionally, we see that Nginx 1.18.0 is the platform used for the webserver.

$ nikto -h https://$RHOST
[...]
- Nikto v2.1.6/2.1.5
+ Target Host: 10.129.182.60
+ Target Port: 443
+ GET The anti-clickjacking X-Frame-Options header is not present.
+ GET The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ GET The site uses SSL and the Strict-Transport-Security HTTP header is not defined.
+ GET The site uses SSL and Expect-CT header is not present.
+ GET The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ GET The Content-Encoding header is set to "deflate" this may mean that the server is vulnerable to the BREACH attack.
+ GET Hostname '10.129.182.60' does not match certificate's names: seal.htb
+ OPTIONS Allowed HTTP Methods: OPTIONS, GET, HEAD, POST 
+ GET /manager/status: Default Tomcat Server Status interface found
+ GET The anti-clickjacking X-Frame-Options header is not present.
+ GET The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ GET The site uses SSL and the Strict-Transport-Security HTTP header is not defined.
+ GET The site uses SSL and Expect-CT header is not present.

Using gobuster to enumerate the webserver, we find a few interesting directories, however, when we attempt to navigate to them, we are redirected to use cleartext http (port 80, which is not open). Going to https://seal.htb in the browser, we are given the same index page as if we accessed the webserver via its IP. This indicates vHosts may not be implemented for the webserver.

While trying to enumerate the types of files in use by the webserver, we get an error page that reveals the webserver is actually Apache Tomcat 9.0.31. This means that the directories we previously found were likely Tomcat related.

Going to https://seal.htb/manager/manager reveals possible Tomcat endpoints.

When we attempt to use BurpSuite to bruteforce the credentials, we are unsuccessful. Lastly, going to the webserver on port 8080 reveals it is running GitBucket. Unfortunately, we aren’t allowed to see anything unless we use a valid account.

Getting a Foothold

Having found GitBucket running on port 8080, we create an account so that we may see if there are any repositories that we may access. After logging in, we are presented with a couple repositories – “seal_market” and “infra”.

Looking around the “seal_market” repo, we learn a few things. From the README.md file, we learn it may contain configuration files for the webserver on port 443, mutual authentication may be enabled for a few areas of the website, the deployed instance of Tomcat may not be up-to-date, and Tomcat manager and host-manager may be enabled.

Looking at the commit history, we see a commit titled “Adding Tomcat configuration”. Next, we select that commit to view the files it added/changed, and find potential Tomcat credentials in the tomcat/tomcat-users.xml file. Looking at the roles, the user should have access to the manager and admin GUIs.

When we try to access the GUIs at https://seal.htb/manager/html and https://seal.htb/host-manager/html, we are prevented by Nginx. Looking at the Nginx configuration file, we see that mutual authentication is the reason we are being blocked from viewing the pages.

As we would need a valid user certificate to access the Tomcat GUIs, we will need to find a way to bypass this. To do this, we start by Googling “tomcat nginx reverse proxy vulnerability”. From this search, we find this BlackHat presentation by Orange Tsai. After reading through it, we learn that we can evade the Nginx mutual authentication check by supplying a ‘;’ and some text in the middle of the URL path. This works because Tomcat and Nginx parse URLs differently. Nginx treats the URL literally, whereas Tomcat treats the ‘;’ similarly to a comment.

When you add ;(ignored) to the URL path, we are able to evade Nginx, and get access to the Tomcat Manager.

As we’ve done in previous labs, we generate a malicious ‘.war’ file (reverse shell), and upload it to Tomcat.

In order to execute the reverse shell, we need to know the file name of the ‘.jsp’ file that contains the reverse shell. To find it, we use unzip to list the contents of the ‘.war’ file, as it is basically a rebranded ZIP archive.

Lastly, we start a netcat listener to handle the connection back to us, and navigate to the ‘.jsp’ page. Once we have it open, we get a connection back as the tomcat user.

Getting User

Now that we have a shell on the machine, we begin our initial enumerations. Looking around for credentials to the database GitBucket uses, we find the database credentials in /home/luis/.gitbucket/database.conf.

Next, we download the GitBucket database at /home/luis/.gitbucket/data.mv.db to our local machine to enumerate it. Using dbeaver, we are able to connect to the database, and find usernames and password hashes in the “Public.Account` table.

We attempt to crack the password hashes with hashcat, however, we are unable to crack them in a reasonable time frame due to the hashing algorithm used. Next, we try to run sudo -l to see if Tomcat can run any commands as other users, however, we need a password. Since these quick checks were unsuccessful, we switch to automated enumeration with LinPEAS. Unfortunately, this does not yield any valuable results, so we upload and run pspy to enumerate potential scheduled tasks. From this, we see that ansible-playbook is being executed regularly.

Next, we look at the run.yml file it executes, and learn it copies files in /var/lib/tomcat9/webapps/ROOT/admin/dashboard (including symbolic links) to /opt/backups/files/. Once files are copied, it creates an archive of them, and stores the archive in /opt/backups/archives/. After the archive is made, it deletes the files in /opt/backups/files/.

Looking at the /var/lib/tomcat9/webapps/ROOT/admin/dashboard directory, we see that it is owned by root, however, the uploads/ directory within it is world-writable. Due to this, we should be able to exploit the ansible-playbook cronjob to extract the contents of a user’s directories. Since ansible-playbook is being run as luis, he will be our target user. Since ansible is configured to also copy symbolic links, we create a link to luis‘s home directory and store it in the uploads/ directory.

Setting up an exploit for ansible-playbook

Next, we wait about a minute for the archive to be created, then we copy it to another directory, and extract it.

Looking into the extracted contents, we do see luis‘s home directory, and within it, we have access to read user.txt.

We see the .ssh/ directory is also present, so we are able to extract his private SSH key, and connect to the machine via SSH.

Getting Root

Having gained a shell on the machine as luis, we begin our initial enumeration. One of the first commands we run is sudo -l. From it, we see that we are able to run ansible-playbook as root.

Checking GTFOBins, we see that we are able to exploit ansible-playbook to get a shell as root. Following the instructions, we get a shell as root, and can now read root.txt.

Thank you for taking the time to read my write-up. I am interested in other ways this machine has been solved. Feel free to reach out to me and we can discuss it. Thanks!

Resources