Over the past couple of months, I’ve been intermittently working on a new tool named GraphSpy.

At Spotit, we’ve already been using GraphSpy fairly successfully during penetration tests and red team assessments. The plan was always to release it to the public once it was at a point where I was satisfied with it, although I always saw something extra that could be added or improved, which kept delaying the actual release.

A couple of weeks ago, I decided to publish it on GitHub and share it with an initial test group to collect some feedback without making any public announcements about it yet. I got a lot of positive feedback here, and after making some last changes, I decided it was finally time to share it with a bigger audience while I continue to work on improving it simultaneously, instead of indefinitely delaying its release for specific features I still have in mind.

Feel free to jump directly to the GraphSpy Features section if you’re already familiar with device code phishing and are only interested in a quick overview of the features GraphSpy has to offer.

Device Code Phishing Recap – The king of all Phishing Tactics?

During red team assessments and social engineering campaigns, our offensive security team usually has a lot of success with device code phishing.

This is not surprising if you look at how powerful device code phishing is as a tool in highly targeted spear-phishing attacks due to its nature. The process is simple: Generate a 9-character code as an attacker, and convince your target to fill it in on a Microsoft website.

The most important feature of device code phishing is the fact that it only utilizes completely legitimate Microsoft websites. This not only means that no security product will be able to block it, but also ensures that the victim will be less suspicious since they can validate it is a legitimate Microsoft website.

Additionally, in a corporate environment, users are usually already logged in to their accounts on their company devices. Depending on how long ago they authenticated and the company policy, it might not even ask them to reauthenticate with their password and/or MFA. And even if they are not already signed in to their account, their password manager will automatically fill in their credentials on a legitimate Microsoft domain, further establishing that this is a legitimate Microsoft website instead of a phishing page.

Why GraphSpy?

Traditionally, these device code phishing attacks were performed using CLI-based tools such as TokenTactics. This worked great for us, although we quickly stumbled upon some limitations during actual assessments:

  • It was only possible to generate and poll one device code at a time. (I initially resolved this with my multi-threaded token generation pull request to TokenTactics, which is still open 2 years later)
  • It only allows you to request access tokens for a specific predefined set of resource/client id combinations

But most importantly, post-compromise enumeration options were very limited with the tools that were available. Especially when having compromised a normal employee account, using the obtained access to its fullest would mean having easy access to Office applications such as Microsoft Teams, Outlook, OneDrive, SharePoint, …

Back then, AADInternals had some nice functions to list or send Microsoft Teams messages one command at a time, and TokenTactics came with a command that allowed to dump all emails from the mailbox of the user in a JSON format.

Aside from that, we mainly used some custom-developed PowerShell scripts in which we basically implemented whatever features we needed. We used this to interact with OneDrive, SharePoint, and Outlook using the access tokens obtained through device code phishing. (A snippet of one of these functions we created is shown below.)

However, no matter how hard we tried to make it as convenient as possible, I was never fully satisfied with the command line tools due to the amount of time it took to perform basic enumeration activities.

Especially for file browsing, you would need to go through multiple steps:

  1. List all sites on SharePoint
  2. List all drives of a specific SharePoint site using the site ID from the previous output
  3. List the contents of a specific drive using the drive ID of the previous output
  4. To list the contents in a folder on a drive you would need to type out the folder name.
  5. If you then find an interesting file, you need to download it using its file ID

All of these functionalities were implemented in different functions which work very well on their own, although constantly changing the parameters of these functions during a painstaking enumeration process is very time-consuming. And if you just got access to an account of your target, without knowing at which point your access might be lost, you need to ensure that you get the most out of it as quickly as possible while avoiding being too noisy (just blindly recursively downloading everything is a no-go).

This is exactly why GraphSpy was created, seeking to solve most of the issues we had by defining the following requirements:

  • Basic enumeration steps should be simple and painless
  • Multiple device codes can be generated and polled from a single location
  • The user should have complete customization options to generate access tokens with any resource and client ID
  • Where possible, everything within the tool should easily integrate with the other functionality, making it possible to instantly pivot from initial access to post-compromise enumeration.
  • A single location to persistently store all access and refresh tokens, allowing convenient switching between accounts or even different projects.
  • Added bonus if it works cross-platform on multiple operating systems.

For a better end-user experience during the enumeration, the design choice was made to build GraphSpy as a local web application.

At its core, GraphSpy is a Python Flask application that can be accessed through a web browser on Both the default interface and port can be changed using the -i and -p arguments respectively. (For a quick start reference and detailed information about the available options, please refer to the wiki.)

Using Python allows it to be cross-platform (mainly tested for Linux and Windows). (Side note: The name of the tool is also a reference to it being written in Python:

GraphSpy Features

Alright, now that we know the requirements we took into consideration, let’s highlight some of the key features of GraphSpy.

Device Codes

The device codes page contains all details from all your device codes in one single location. GraphSpy comes with a predefined list of common Resources and client IDs which can be quickly selected when generating a new device code, although you are completely free to specify any custom values as well.

Once a code is generated, it will be added to the database and GraphSpy will automatically start polling its status every 5 seconds until the device code authentication succeeds or expires. Multiple device codes can be polled concurrently without any issues, and polling even continues in the background if a different page is opened or the browser is completely closed (as long as GraphSpy itself is still running in the background of course).

A convenient copy button ensures that you do not waste any time copying the user code when you need to share it through email for instance.

If the device code flow is 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.

For more information on the Device Code page, Device Code Authentication, or Device Code Phishing, check out the GraphSpy wiki!

Access & Refresh Tokens

The Access Tokens page is where you can view, import, or create access tokens within GraphSpy. Every access token you want to use in GraphSpy is stored in the database. This allows easy switching between different access tokens with different identities or resources.

Access tokens generated by GraphSpy through successful device code phishing attacks are automatically stored in the database with a useful description. However, Access Tokens obtained through other means can also be manually imported. Similarly, access tokens from GraphSpy can be easily exported to use them with other tools by clicking the copy button to copy the token to your clipboard. Even if no other features of GraphSpy are used, GraphSpy can still work as a central location to keep track of all your access tokens. No more fiddling around with access and refresh tokens of different users which start to accumulate in bigger projects spanning multiple days!

The top right section of the page is where you can use refresh tokens stored in GraphSpy, to obtain new access tokens. If it is a family of client IDs (FOCI) refresh token, you can even use it to request access tokens for other FOCI resources.

You can also view the full decoded details of each access token when needed.

A similar page is available for Refresh Tokens where you can view a summary of all the refresh tokens stored in the database, switch active refresh tokens, or import refresh tokens obtained through other means. (More information here)

To use access and refresh tokens within GraphSpy itself, they need to be set as the active access or refresh token respectively. While this can be done from the Access Tokens or Refresh Tokens pages themselves, you can also always view a quick summary of the currently active access and refresh token from any page using the Token Options sidebar. When the current access token is expired, its background will be red (as shown in the following screenshot), while it would be green otherwise.

The Token Options sidebar also allows easy switching between tokens, or even refreshing the current or a completely different access token.

OneDrive & SharePoint

As previously mentioned, one of the biggest frustrations we had previously was how cumbersome it was to enumerate through directories and files on OneDrive and SharePoint. This is the part where GraphSpy’s web interface shines.

With a valid access token for the MSGraph API selected, we can easily browse through all content in the user’s OneDrive using a file explorer-like interface. A folder can be opened by clicking on the folder icon, while files can be downloaded using the download icon. Simple as that.

Future updates will also include file upload and delete functionality.

Recently accessed files and files shared with the current user can also be easily accessed.

Working with SharePoint in GraphSpy is just as easy.

The SharePoint Sites page will usually be the first page you will want to visit, as it allows you to view all SharePoint Sites or search for a specific one using the filter. A SharePoint site can be opened using the link icon.

Selecting a SharePoint Site brings you to the SharePoint Drives page which will show all drives that are linked to that specific SharePoint Site. (The Site ID will be automatically filled in from the previous step, although you are also free to specify a custom Site ID if you would have enumerated that through any other means.)

To view the files and folders inside of a Drive, simply click the link icon in the table, which will open the SharePoint Files page in a new tab and show its content.

The SharePoint Files page works similarly to the OneDrive Files page. Open folders or download files from the file explorer, or specify a custom path at the top.

Once again, a custom Drive ID can be entered if that would have been enumerated through some other means. although it will be automatically populated if you clicked through from the SharePoint Drives page.


The initial plan for the Outlook module was to create a web-based email client similar to Outlook on the Web (OWA) where emails could be read and answered. However, why would we do that if we can just access the real Outlook web client using just an access token?

To use the Outlook module, simply select a valid access token with the resource (or manually paste an access token in the text box), and click on the “Open outlook” button.

If the provided access token is valid, this will open a browser tab in which you have complete access to the user’s mailbox. From here you can interact with emails, view the user’s calendar, create email rules, …

Note that this technique currently only works for Outlook, as we have not found any way to replicate this for any other applications (such as SharePoint, Microsoft Teams, Azure Portal, …)

(A similar technique was previously built into TokenTactics, however this stopped working about two years ago. The technique used by GraphSpy, which is still working today, was discovered by Lares.)

Generic MSGraph Searching

The search functionality of the Microsoft Graph API is very powerful as it allows you to query data from various Microsoft 365 applications through one endpoint. GraphSpy includes a basic wrapper around the Microsoft Search API to make it very easy to search for anything you want and view the results in a generic table.

Any search query supported by the search API will work. The simplest type of query is to just enter a keyword you want to search for (e.g. password).

More advanced search queries can be constructed using built-in operators and KQL syntax. (e.g. (password OR login) AND (filetype:xlsx)).

GraphSpy will try its best to show an overview of the most relevant information in the columns, although, all entity types return different attributes, making it hard to implement a generic solution. Therefore, the dropdown icon can be used to view the full details of the entity.

The summary column displays the context returned by Microsoft on which it hit this search request. Any specific keywords that were searched for are highlighted in bold.

The action icon will change depending on the type of entity queried. For instance, a driveItem will show a download icon, while a site will have a link icon to view its drives.

Custom API Requests & Templates

I am still constantly adding new functionality to GraphSpy, and have a lot of nice ideas in my backlog. However, because this is currently a one-man project, finding the time to implement everything I want next to other responsibilities is sometimes challenging.

Additionally, during assessments, we sometimes have a very specific requirement where we would need to utilize some specific API calls. Simply implementing every single API call of every Microsoft application within GraphSpy is just not feasible, although I still wanted a way to facilitate this need and make it as convenient as possible to call any single API endpoint using access tokens stored within GraphSpy, without having to switch between 5 different tools or build custom scripts.

For this, a powerful custom request builder was created in GraphSpy.

As an example, if we want to view the default properties of the current user, we can use the Microsoft Graph API as described in Microsoft’s documentation. GraphSpy will automatically use the active access token from its database for authentication towards the endpoint.

For basic API requests, as shown above, this is fairly straightforward.

However, for some more complex APIs, GraphSpy supports the use of custom variables which will be substituted within the request URI, the request body, and even headers.

An example of this is shown in the next image where the content of a blob in an Azure Storage Account is retrieved.

So now you are probably thinking: “Why would you use variables instead of just modifying the values in the URL itself?”

Well, what you can now do is save the complete request template (including variables) to the database for later use.

At any time, you can then load any of the request templates that you have previously stored in the database. So if there is a specific API call you utilize a lot, you likely want to store it as a request template so you don’t have to start from scratch every time.

Any variables defined in the request template when it was saved will also be stored in the database. The values can be left empty, or they can be filled in to store a default value for the variable.

Multiple Databases

All information that needs to be stored persistently between runs (such as access and refresh tokens, device code information, request templates, …) is stored in an SQLite3 database.

GraphSpy has the ability to create as many databases as you want and switch between them at any time, allowing you to work on multiple projects at once and keep all information nicely separated.

How you use this is completely up to you of course. Whether you want to separate information from different organizations, have a different database for each compromised user account, or just use a single database for everything, the choice is yours.

A different database can be selected/created on launch using the -d <database_name> command line argument, although you can still create or switch databases from the Settings page within GraphSpy itself. (More information here)

Future & Conclusion

As mentioned previously, the current version of GraphSpy is by no means close to the final state I have in mind, so expect a lot of new features and updates coming up soon!

Features that are currently in the backlog include additional authentication options (Password, ESTSAuth cookies, PRT, Certificate Based Authentication, …), new modules (Microsoft Teams, Entra ID, Azure, …), and a lot of overall improvements/additions to the current modules to allow more customization and easier usage. I am also always open to suggestions and feedback, so feel free to open an issue on GitHub or reach out (Linkedin, Twitter)!

The Wiki is also something that is still work in progress, and there are some nice features that are not documented yet. For instance, we’ve used GraphSpy for some fairly advanced dynamic device code phishing in the past. The setup for this is a bit more advanced, although it is very powerful once it is set up, allowing dynamically created device codes to be automatically stored and polled in GraphSpy, and storing access and refresh tokens on success without any interaction. This is most likely something that we will go over in a dedicated blog post in the future, so stay tuned for that!