A phishing technique for compromising Office 365 / Azure AD accounts

Published on: Jan 16 2023 by Nicolas

During our missions at CALYPT, we are regularly brought to perform phishing campaigns targeting our clients. We specifically target their Microsoft 365 installations because they bundle many services e.g. Microsoft Teams, SharePoint, Exchange, Outlook, One Drive, etc. Moreover, thanks to Azure integration with Microsoft 365, it is possible to access the active directory resources from the accounts compromised by the phishing campaign.

There are many phishing techniques. Here we will focus on the Azure AD device code authentication flow. I refer you to this excellent article by Dr. Nestori Syynimaa – Introducing a new phishing technique for compromising Office 365 accounts – in which he presents this attack, its advantages and how to protect yourself from it. For more information you can also consult The Art of the Device Code Phish.

I worked on a fork of the AADInternals tool to add several features. AAD Internals is a PowerShell script module that interacts with Azure AD and Microsoft 365 APIs. It comes with several functionalities like the kill chains that provide tools for recon to improve targeting the organisation depending on the user’s role.

I was first tasked with implementing the download of files shared in Teams conversations to meet a need during a Red Team audit. Indeed, one of the flags we had to retrieve was a file shared in a Team conversation. Meanwhile I tried to improve the token refresh functionality in order to facilitate the interaction with the different Microsoft services. I then worked on exporting the data into a Neo4j database in order to be able to work directly on the interactions between the different objects of the tenant. You can find the project’s fork at nclv/AADInternals. We are open to pull requests !!

Neo4j database schema
Neo4j database schema

Before diving into the project a little background on authentication to Microsoft services.

Access and refresh tokens

Disclaimer : Most of the information below comes from Microsoft documentation.

Clients use access tokens to access a protected resource. The default lifetime of an access token is variable. When issued, an access token’s default lifetime is assigned a random value ranging between 60-90 minutes (75 minutes on average). The variation improves service resilience by spreading access token demand over a period of 60 to 90 minutes, which prevents hourly spikes in traffic to Azure AD.

An access token can be used only for a specific combination of user, client, and resource. Access tokens cannot be revoked and are valid until their expiry. A malicious actor that has obtained an access token can use it for extent of its lifetime.

When a client acquires an access token to access a protected resource, the client also receives a refresh token. The refresh token is used to obtain new access/refresh token pairs when the current access token expires. Refresh tokens are also used to acquire extra access tokens for other resources. Refresh tokens are bound to a combination of user and client, but aren’t tied to a resource or tenant. As such, a client can use a refresh token to acquire access tokens across any combination of resource and tenant where it has permission to do so.

Refresh tokens have a longer lifetime than access tokens. The default lifetime for the refresh tokens is 90 days in our case. Refresh tokens replace themselves with a fresh token upon every use. The Microsoft identity platform doesn’t revoke old refresh tokens when used to fetch new access tokens.

Access token workflow with AADInternals

An access token contains different types of metadata. For instance scope lists the scopes the access token is valid for. It is not related to the user’s permissions. Any user (guest, member or admin) has the same scope for a given resource. The Microsoft Graph permissions reference lists all the existing scopes.

Access token permissions

Disclaimer : Most of the information below comes from Microsoft documentation Scopes and Permissions.

Effective permissions are the permissions that your app has when it makes requests to the target resource. It’s important to understand the difference between the delegated permissions and application permissions that your token is granted, and the effective permissions your token is granted when it makes calls to the target resource. In our case, we use an access token to interact with the Microsoft API so the scopes of our tokens are part of the delegated permissions.

The effective permissions of your token are the least-privileged intersection of the delegated permissions (scopes) the token has been granted (by consent) and the privileges of the currently signed-in user. You can never have more privileges than the signed-in user.
Within organizations, the privileges of the signed-in user can be determined by policy or by membership in one or more administrator roles.
For example, assume your token has been granted the scope Files.Read.All delegated permission like in the figure above. This permission nominally grants permission to read all files the signed-in user can access. If the signed-in user is a global administrator, you can read the files of every user in the organization. However, if the signed-in user doesn’t have an administrator role, you can only read the files of the signed-in user. We can’t read the files of other users in the organization because the user that we have permission to act on behalf of doesn’t have those privileges.

One resource that can be useful when auditing the current user permissions is the Least privileged roles by task in Azure Active Directory.

A phishing attack using a device code

On figure Access token workflow with AADInternals we illustrate the first steps of a phishing attack on Microsoft 365 on the side of the attacker.

We request a token for the Azure Resource Manager resource. The client identifier d3590ed6-52b3-4102-aeff-aad2292ab01c is a common application (Microsoft Office) from Microsoft for every tenant (see Application IDs for commonly used Microsoft applications).
We must use a client identifier belonging to an application registered in the tenant. To avoid that our interactions appear as malicious in the logs, we use one of the identifiers of the applications already present in a tenant at its creation. We could also create an application and register it to the tenant but we will be more likely to be detected.
It is now necessary to send our phishing mail containing the code DJJ9K3FWV to a user who will have to enter this code while being previously connected to his Microsoft account. We have a very short window of 15 minutes to perform this operation as the code expire after this time limit.

After receiving the access token and refresh token for Azure Resource Manager, we request a new access token to interact with Microsoft Graph API using the refresh token we just received. We’ll now be able to use the PowerShell functions I added to AADInternals.

Microsoft Graph API calls

AADInternals already contains some functions using the Microsoft Graph API in MSGraphAPI.ps1. However, there are many missing. For instance you can’t get the files present in the group drives.

I implemented about 30 calls to the Microsoft Graph API to retrieve all the interesting information about the authenticated user and the tenant users. For instance the function Get-MSGraphFiles allows you to download in parallel all the files accessible to our authenticated user.

PS C:>$AccessToken = Get-AADIntAccessTokenFromCache -Resource "https://graph.microsoft.com" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"
PS C:>Get-AADIntMSGraphFiles -AccessToken $AccessToken -Destination .\Files\

I also took the opportunity to make the access token and refresh token management more accessible. Indeed, the token manipulation functions were not made public in the PowerShell module. So the functions Get-AccessToken and Get-AccessTokenWithRefreshToken from AccessToken.ps1 were not accessible by the end user. Once made public, it is possible to choose the identifier of the application (ClientId) you want to use to make your request.
I added the function Get-AccessTokenFromCacheRefreshToken that tries to request an access token using a cached refresh token. This new function is very useful in all the workflows I developed because it allows after a single command to make calls on another API as seen on the figure Access token workflow with AADInternals.

Data exports

As I developed this tool, I thought it would be interesting to be able to visualize the content of the tenant using a graph. In fact, there are several relationships between users, files, groups, roles, etc.
This visualization is rather useful if you want to continue phishing by targeting people who know each other (member of the same team, editors of the same file, …), moreover we are able to audit visually the current user permissions across the whole tenant.

The next step is to use some graph algorithms to find the most important or influential nodes (users, groups, files, etc.) in the network or evaluate how close a node is to all the other nodes in the network. We could also find disconnected components or islands within the network.

Exporting data in CSV format and then importing them into a Neo4j database is quite tedious. Indeed, we must first create a data schema for each node of our graph, then create the PowerShell conversion functions that will generate the CSV files that will then be imported into Neo4j with the keyword LOAD CSV. Some node can have quite a lot of fields (see for instance the users import).

To facilitate the export of data in CSV format, I created a function that exports all accessible data in CSV files. If the user does not have the permissions to retrieve certain data then an informative message is displayed on the terminal.

CSV export

To import the CSV files into Neo4j, I created an import function per CSV file and a script to import everything in a single operation. Of course I took care to create the appropriate constraints and indexes to optimize the database searches.

I ended up with about thirty .cypher import files. Although this may seem like a lot, it allows us to choose which data we want to import into the database. Indeed it is totally possible to import only the CSV files concerning the users or only those concerning the authenticated user to facilitate the analysis.

Finally I wrote some cypher queries to return relevant information. For example display all the files of the tenant accessible by our authenticated user.

Returns all the HAS_FILE relationships
Some useful cypher queries

The project is available on Github at nclv/AADInternals. You will find a list of features that have been added, sample workflows for extracting data from Azure AD and detailed cypher queries for interacting with the exported data.

And of course we are grateful to Nestori Syynimaa for AADInternals and his work on Azure AD.

Filed under: Headlines

Leave a Reply