-
Notifications
You must be signed in to change notification settings - Fork 60
Device Codes
One of the key features of GraphSpy is the ability to generate and poll the status of multiple device codes at once.
If you are not familiar with Device Code authentication and phishing, it is explained in more detail below.
A device code can easily be generated by selecting a Resource and Client ID, and clicking on the submit button. For convenience, a list of commonly used Resources and Client IDs will be shown, although you are completely free to specify a custom value as well.
As explained below, the Client ID is important when performing a device code phishing attack, as the friendly name linked to the selected Client ID will be shown to the victim after they fill in the user code. (The Client ID may also define which scopes will be present in the tokens you obtain, although if you are using a FOCI application, you don't need to worry about that too much as you can easily refresh your tokens with a different Resource and Client ID afterwards)
The "user code" (i.e. the 9-character code which needs to be filled in to the https://microsoft.com/devicelogin page) can be copied to your clipboard using the copy icon. Alternatively, click the delete icon if you want to delete the entry from the database (this will also stop polling it if it has not expired yet).
The "device code" itself is what is used in the background to poll its status. In most cases, you will not need this, although you can view it if needed using the drop-down button.
If the device code flow was successful, GraphSpy will automatically store the access and refresh token in its database. The tokens can be viewed from the Access Tokens
and Refresh Tokens
pages respectively. The description of these tokens will also note from which specific device code they were obtained.
Note: While GraphSpy allows you to create multiple device codes and poll them all concurrently without any limit, I have noticed that Microsoft does start throttling you if you generate and poll an excessive amount of device codes from the same public IP address. From testing, I've found you can easily poll about 50 device codes at once without getting rate limited, but if you want to go a lot higher, you will most likely have to proxy your requests through different public IP addresses.
When a new device code is generated, all relevant information is stored in the database and GraphSpy will start a different polling thread in the background (if it is not running already). This polling thread will automatically poll all active device codes stored in the database every 5 seconds, and update its status if required.
The device codes page refreshes the table with the latest information every 5 seconds. However, polling itself happens on the GraphSpy server, so you can safely navigate to a different page or close the browser without impacting the device code polling.
The polling thread quits when there are no active device codes anymore to poll. However, generating a new device code will obviously cause the polling thread to start again.
Note: If you stop and restart GraphSpy process in the middle of polling, this will obviously kill the polling thread as well. Since the polling thread does not automatically start, you can use the Restart Polling
button to start the polling process again in this case.
This section will provide a brief summary of device code authentication. For more information, please refer to the Microsoft documentation.
Device code authentication is intended to allow a user to log in to an application on one device (Device A
), by signing in to their account on a different device (Device B
).
This is particularly useful when Device A
is an input-constrained device (e.g. a smart TV, printer, IoT device, ...) and Device B
is a device where you may or may not be already signed in with your account.
One more common example where device code authentication is used is in the Azure CLI. As described in the Azure CLI documentation, the device code flow is used when the Azure CLI can not automatically open a web browser when using the az login
command.
So how does it actually work when a user wants to sign in to Device A
from Device B
using the device code flow?
In practice, the flow is initiated by Device A
by sending a request to Microsoft to generate a device code for a specific Resource and Client ID. You can think of the resource as the actual (cloud) application you want to be signed in to (e.g. SharePoint, Microsoft Teams, Azure AD, Outlook, Microsoft Graph API, ...) and the Client ID as the client application on your device from which you access that resource (e.g. The Microsoft Teams program on your Windows PC, or the Outlook app on your mobile phone).
When a device code is requested, Microsoft will generate two types of codes and return these in the response: a Device Code and a User Code.
The Device Code is used by Device A
to poll the status of the authentication attempt. The status should be polled every 5 seconds (as implied by the interval
parameter which Microsoft returns and is always set to 5.) The Device Code itself is not very user-friendly as it is relatively long. During a normal device code flow, this code is not shown to the user as it is not really relevant to them.
In contrast, the User Code is a fairly short 9-character alphanumeric code. This code will be displayed by Device A
together with instructions on how to use it.
At this point, the user is expected to go to Device B
and fill in this User Code on a special Microsoft login page:
For easier access to this page without having to type the full URL, the following two legitimate Microsoft URLs will also take you to this website:
After the user enters the code on the previous page, they will see a sign-in screen with a message. We will come back to this message later on, but let's first discuss the actual sign-in options a user has at this point.
If the user is logging in from a browser/device where they do not have an active session already, they will be prompted to enter their email address and password. The user might also be prompted for MFA if that is required by the organization.
The second scenario, which is the most common in a corporate environment, is that the user is already logged in to their account. In this case, the user can simply select their account from the list. Depending on how long ago the last authentication was, the user might still be prompted for their password or to complete MFA. However, the fact that their account is already prefilled also tends to lower the barrier to continuing faster without paying too much attention to the message above it. (More on that later in the Device Code Phishing section)
Remember, this is all happening on Device B
while Device A
is just still polling the status every 5 seconds.
At some point, Microsoft decided to add an additional verification prompt after the authentication. This is the last warning provided to the user stating that one should only continue if they trust the source from which they received the user code.
After approving the last prompt, the user has completed the device code flow on Device B
. With this process, you have proven to Microsoft that you indeed have access to this account as you have filled in the code on a device where you were able to sign in.
When Microsoft now receives the next poll from Device A
for the previous Device Code, it will return an access token and refresh token for the user who signed in on Device B
. If you are unfamiliar with OAuth and JWT, the following provides a simplified summary :
- An access token can be used to authenticate as a user to a specific resource (i.e. application/website/...) for a limited amount of time. (Defaults to about 1 hour for most Microsoft applications)
- A refresh token is valid for a much longer period (typically 90 days, but can be indefinitely refreshed) and can not be used to authenticate to an application directly, but they are used to request new access tokens.
- Refresh tokens are automatically revoked in certain use cases, for example when the user resets their password.
- While refresh tokens should by nature only allow you to obtain access tokens for one specific resource, due to how Microsoft implemented OAuth, certain refresh tokens can be used to obtain access tokens for a group of applications. (Family of Client IDs - FOCI)
So with the obtained access and refresh token, Device A
will now be able to access the resource with the privileges of the signed-in user.
After reading the Device Code Authentication section, you might already have identified that this technique can be fairly powerful in social engineering attacks for several reasons:
- If an attacker can trick a victim user into filling in a user code created by the attacker, they would obtain persistent access to the account of the user.
- The victim never fills in their credentials on a malicious page, and only ever interacts with legitimate Microsoft websites. This has the added benefit that the user might already be signed in to their account, or that their password manager will automatically fill in their credentials on the sign-in page.
- An attacker does not have to go through the hassle of setting up any public infrastructure, such as obtaining a hostname, creating a website with a fake login page, gaining a positive reputation to bypass detections, ...
However, there are still a couple of limitations that we will need to take into consideration. The main limiting factor is that device codes are only valid for exactly 15 minutes (900 seconds) after being generated. This means that in order to utilize this successfully, an attacker needs to be confident that the user will receive, follow, and complete its instructions within 15 minutes of generating the code.
However, with some creativity and a good pretext, this is still a very powerful technique with the potential for a very high success rate.
The second issue that you might need to overcome is the warning messages provided by Microsoft when the user goes through the device code flow. Although this might not be that big of a deal in practice. Let's look at these a bit more closely.
Let's look back at the first warning message on the sign-in page after the user has just filled in the user code:
You're signing in to {application_name} on another device located in {country}. If it's not you, close this page.
The two key parts of this message which are emphasized in bold are the name of the application and the country. Naturally, this will probably be the only information that a user will read on this page, which is exactly what we can take advantage of.
While the message contains all the information to be able to understand what will happen, a regular user will usually not take the time to fully read and process what it is actually saying. In fact, the most important words in this sentence are "another device". In my opinion, these should have been the words in bold. Since the device code flow is not well known by most regular non-technical users, it will not cross their minds that signing in to their own device on a legitimate Microsoft website will result in actually signing in on a different device of an attacker.
Going back to the information that is actually emphasized in the message, the application name and the country can both be partly controlled by the attacker.
The country shown is derived by Microsoft from the location of the public IP address from which the device code was generated. This can easily be controlled by generating the device code from the same country as your target. A VPN or virtual machine in a different cloud region can be used if your target is from a different country.
Secondly, the application name shown is the friendly name of the client ID specified during the device code generation process. (Interestingly, the resource to which we are authenticating is not mentioned anywhere, and only the client ID is used for this.) As an attacker, an interesting resource where you may want to get access to is the Microsoft Graph API (https://graph.microsoft.com
). However, since this is a Family of Client ID (FOCI) application, it is possible to select any FOCI client ID that would suit our pretext. (This is a good resource listing known FOCI applications)
For example, if in the context of the pretext you want your target to believe that they need to enter a "Meeting Code" on the Microsoft website in order to join your Microsoft Teams meeting, then you can use the client ID 1fec8e78-bce4-4aaf-ab1b-5451cc387264
when generating the device code, which translates to the application name "Microsoft Teams".
In this example, let's say your target lives in Belgium and you want them to join your last-minute "Teams Meeting", the following message might not seem that suspicious anymore for the user:
You're signing in to Microsoft Teams on another device located in Belgium. If it's not you, close this page.
If your victim is struggling to quickly join what they believe to be a Microsoft Teams Meeting, the only information they will process while selecting their account on this page is "signing in", "Microsoft Teams" and "Belgium".
In a different pretext, you might want a user to believe that the code provides them temporary access to a file on SharePoint. In this case, it might make more sense to use the Client IDs for SharePoint or Microsoft Office.
The additional verification prompt added after the sign-in page itself seems to be even less of an issue. In this case, the warning itself is once again hidden in the default font and will not be read in %95 of the cases: "Only continue if you download the app from a store or website that you trust."
However, the only message that the user will focus on due to the contrast of the size is:
Are you trying to sign in to <application_name>?
If you crafted your pretext well, you want to make your victim believe that they are trying to sign in to that application. In the example of the Microsoft Teams meeting, the user is already late for the meeting at this point and actually thinks they are indeed signing in to Microsoft Teams themselves, so they don't have time for silly confirmation prompts asking them if they really want to sign in to Microsoft Teams or not.
If the user reaches this final page, that means that you have now successfully gained access to the account of the user. If you are using GraphSpy, you will see that the status of your device code will be updated to SUCCESS
, and you will have received an access and refresh token for your victim.
If you utilized a FOCI client ID, your refresh token will allow you to obtain access tokens for any other FOCI application. This is explained in more detail on the Refresh Token page of this wiki. These access tokens can be used in GraphSpy for various post-exploitation enumeration methods.
Ok, so you got access, although there is one more thing you might need to take care of at this point. Depending on what you promised in your pretext, the user might expect to actually have access to a Teams Meeting or a document on SharePoint right? But instead, they see this vague message stating that they have signed in to the application on their device and that the window may be closed. However, since they were expecting something at this stage, this might be the only message they will fully read.
Luckily, the message can easily be interpreted as that they signed in to the application on the device they are currently using: "on your device". Unlike previously, the message does not refer to it anymore with "another device in ...".
From experience, I've noticed that users usually conclude that an error occurred which prevents them from gaining access to what they were trying to access, instead of suspecting that their account has been compromised at this point. This is good because it will not trigger the user to reset their credentials. However, they might instead reach out saying that "They can't join the meeting" or "download the file". From this point, all you need to do is to close the conversation or stall to buy time without raising additional suspicion. For instance, you might postpone the meeting to next week or state that you will deliver the "important document" through a different method.
But what if the user does get suspicious and resets their credentials? Well, then all the refresh tokens obtained before the password reset will be invalidated and will not allow new access tokens to be generated. However, access tokens can't be revoked by default (except when CAE is used). If the user resets their passwords 15 minutes after you obtained several access tokens with a validity of about 60 minutes, you will still be able to use those access tokens for the next 45 minutes until they actually expire! Better make these minutes count!