Executive Summary
Microsoft 365 is a trusted part of daily business. A threat actor with a valid OAuth credential can hide command-and-control (C2) traffic inside requests that look like any other Microsoft 365 application talking to the cloud
Rubrik Zero Labs identified and analyzed a new 64-bit Windows backdoor variant in the growing class of malware that abuses Microsoft Graph for C2. Rather than contacting an attacker-owned server, the implant authenticates to Microsoft Graph and reads its tasking from Microsoft 365 objects. We track this malware as GraphFleet. The name reflects its Graph-centric design and the campaign marker gcx-fleet-2026-10 embedded in the binary.
GraphFleet's access depends on an OAuth refresh token, a long-lived credential that lets an application sign in to a Microsoft 365 account without a password. GraphFleet uses it to sign in under the name of Microsoft Teams and keeps the latest token in an encrypted cache tied to the infected system. If the token stops working, it downloads a replacement from a hard-coded SharePoint location and, as a last resort, reads encrypted recovery settings from a BNB Smart Chain (BSC) smart contract.

With that access, GraphFleet gives each infected system a unique ID and exchanges commands and results through the account. Its main channel is hidden data, known as open extensions, attached to a Calendar event, which does not appear in Outlook or Teams. If Calendar fails, GraphFleet falls back to Outlook Drafts, then Microsoft To Do, then a OneDrive file, and periodically checks Calendar to return to it.
Through these channels, the operator gets broad control of the host. GraphFleet can run shell commands, download files, update itself, load payloads directly into memory, run scripts in an internal virtual machine (VM), and set up persistence.
Building a Host Identity
Upon execution, GraphFleet first makes sure only one copy is running. It creates the mutex Global\gc-7f2a91c4 and stops if another process already holds it. It then reads the computer name, converts it to lowercase, and begins building an identifier that separates each infected system from every other one.
The malware reads MachineGuid from HKLM\SOFTWARE\Microsoft\Cryptography, combines it with the campaign salt gcx-fleet-2026-10, and hashes the result. Part of the hash is then written out using only the characters a through p. The result is normally a 16-character host ID. If the system cannot supply the source value, GraphFleet generates a random identifier instead.
The host ID is GraphFleet's identity inside Microsoft 365. The Calendar, Outlook Drafts, and To Do channels use it to name their C2 objects:
| Channel | Command Object | Result Object |
|---|---|---|
| Calendar | Extension com.c2.cmd on the event with subject Sync-<host-id> | Extension com.c2.out.<host-id> on the same event |
| Outlook Drafts | Draft with subject cmd-<host-id> | Draft with subject out-<host-id> |
| Microsoft To Do | Task titled notes-<host-id> (or notes) | Task titled notes-done-<host-id> |
The OneDrive fallback is the exception. It uses a single fixed file, settings.dat, in the account's app folder, and its name does not include the host ID. On the other three channels, host-specific names let an operator manage many infected systems through one authenticated account without mixing up their commands and results.
The Host-Bound DPAPI Cache
GraphFleet saves its working configuration, including the latest OAuth refresh token, in a local cache so it can resume after a restart. The cache is a hidden, DPAPI-encrypted file at %LOCALAPPDATA%\Microsoft\WindowsUpdate\state.dat, a path chosen to look like a Windows component. Each time Microsoft returns a newer refresh token, or GraphFleet recovers a replacement, it saves the new token here.
A companion file, state.dat.m, contains a 16-letter fingerprint of the infected computer, derived from a SHA-256 hash of the campaign salt gcx-fleet-2026-10, the MachineGuid, the computer name, and the volume serial number, written using only the letters a to p. Before loading the cache, GraphFleet recalculates the fingerprint and skips state.dat if it does not match, which ties the cache to one machine.
Refresh Tokens as the Key to Microsoft Graph
GraphFleet's next goal is to get a Graph access token through OAuth. It needs this token to receive commands and send back results through Microsoft Graph. At startup it loads an embedded OAuth refresh token into memory, then reads the DPAPI cache described above. If the cache holds a newer refresh token, that one replaces the embedded token before the first OAuth request. On a newly infected host with no cache, the embedded token is used.
The malware sends a refresh-token grant to Microsoft's sign-in endpoint. The request contains a client ID, a Graph .default scope, the selected refresh token, and grant_type=refresh_token.
A successful response returns two values:
- Access token: short-lived, used to access Microsoft Graph.
- Replacement refresh token: long-lived, saved to the cache and used to get a new access token when the current one expires.
SharePoint: The First Remote Recovery Path
If a token refresh fails, GraphFleet first reloads state.dat and retries if it finds a different token. If that also fails, it downloads a text value from a hard-coded document on the SharePoint tenant heightcontrollawnandlandsca-my[.]sharepoint[.]com, checks that it looks like a refresh token, and retries the OAuth request. If Microsoft accepts it, GraphFleet gets a new access token and saves the returned refresh token to its cache.
During our analysis, the refresh token embedded in the binary was already invalid, but the token downloaded from SharePoint was valid, confirming that this recovery path was active. The response included an access token valid for about two and a half hours, a replacement refresh token, and broad Microsoft Graph permissions.

These cover every channel GraphFleet uses, plus sending email, writing to SharePoint sites, sending Teams messages etc. The access token identifies the requesting application as Microsoft Teams, and the response marks the refresh token as part of Microsoft's "family of client IDs" (FOCI), so the same token could also be used through other Microsoft apps such as Microsoft Office, Azure CLI, and Azure PowerShell.
Calendar Open Extensions: The Primary C2 Channel
Once it has an access token, GraphFleet starts with Calendar, its preferred channel. Each infected system has its own Calendar event, named Sync-<host-id> after its unique host ID. GraphFleet searches the account's calendar for this event with GET /v1.0/me/events?$filter=subject eq 'Sync-<host-id>', asking Graph to return only the event's ID. If no such event exists, GraphFleet creates one.
GraphFleet and its operator exchange data through this event using open extensions, a Microsoft Graph feature that lets applications attach small pieces of hidden data to items such as events, emails, and tasks. GraphFleet uses three extensions, all under /v1.0/me/events/<event-id>/extensions/:
| Open extension | What it holds |
|---|---|
| com.c2.host.<host-id> | The computer's name, which registers the host |
| com.c2.cmd | The operator's command |
| com.c2.out.<host-id> | The command's output |
Once the event is ready, GraphFleet registers the system by sending PATCH /v1.0/me/events/<event-id>/extensions/com.c2.host.<host-id> with the system's name, which tells the operator which system the event belongs to.
On each polling cycle, it then checks for a command with GET /v1.0/me/events/<event-id>/extensions/com.c2.cmd. When the operator has placed a command there, GraphFleet reads it and removes the whole extension with DELETE on the same address before running the command, so the same command is not picked up again. The operator has to create a new command extension for each later task.
GraphFleet then passes the command to its dispatcher. Built-in commands, which start with a colon, are handled by GraphFleet itself. Anything else is run as cmd.exe /c <command> in a hidden window, with its output and errors captured. When the command finishes, GraphFleet saves the result with PATCH /v1.0/me/events/<event-id>/extensions/com.c2.out.<host-id>, creating the extension with a POST if it does not exist yet, where the operator can read it, and the cycle starts over
Rotating Through Fallback Channels
Calendar is GraphFleet's preferred channel, but not its only one. After repeated errors, it moves in a fixed order to Outlook Drafts, then Microsoft To Do, then OneDrive, and it returns to Calendar once that channel works again.
Outlook Drafts
The first fallback uses the account's Outlook Drafts folder. The operator sends a task by saving a draft with the subject cmd-<host-id> and the command in its body.
GraphFleet looks for the command with GET /v1.0/me/mailFolders/drafts/messages?$filter=subject eq 'cmd-<host-id>'. Graph replies with the matching draft's message ID, a unique identifier for that draft in the mailbox, and its body, which holds the command. GraphFleet then deletes the draft with DELETE /v1.0/me/messages/<message-id>, using the ID it just received, and runs the command.
To return the result, GraphFleet creates a new draft with POST /v1.0/me/messages, giving it the subject out-<host-id>and the command output as its body. Graph replies with the new draft's message ID, which GraphFleet keeps. For later results it updates the same draft with PATCH /v1.0/me/messages/<message-id>, so the operator reads the output from that one draft.
Microsoft To Do
The second fallback uses Microsoft To Do. The operator sends a task by creating a To Do task titled notes-<host-id>, or simply notes, with the command in its body.
GraphFleet starts by finding the account’s default list, the built-in Tasks list. It sends GET /v1.0/me/todo/lists, and Graph returns the account’s lists, each with a list ID. GraphFleet picks the one marked as the default and keeps its list ID.
Next, GraphFleet requests that list’s tasks with GET /v1.0/me/todo/lists/<list-id>/tasks. Graph returns a collection of tasks, each with a task ID, title, and body. The request has no filter, so GraphFleet searches the response itself for a task titled notes-<host-id>, falling back to one titled notes. From the matching task it takes the task ID and the body, which holds the command. It deletes the task with DELETE /v1.0/me/todo/lists/<list-id>/tasks/<task-id> and then runs the command.
To return the result, GraphFleet creates a new task with POST /v1.0/me/todo/lists/<list-id>/tasks, titled notes-done-<host-id>, with the command output as its body.
OneDrive App-Folder File
The last fallback relies on a single OneDrive file, settings.dat, in the account’s Graph app folder. The file works like a shared notepad: the operator writes a command into it, and GraphFleet writes the result back to the same file.
GraphFleet first checks that the file exists with GET /v1.0/me/drive/special/approot:/settings.dat, and Graph returns the file’s details, including its item ID. If the file is missing, GraphFleet creates it with the word clean using PUT /v1.0/me/drive/special/approot:/settings.dat:/content. The word clean signals that no task is waiting.
On each cycle, GraphFleet reads the file’s content with GET /v1.0/me/drive/special/approot:/settings.dat:/content. If the content begins with clean, there is nothing to do. Anything else is treated as a command. GraphFleet runs it and saves the result by sending the output to the same address with PUT, which replaces the file’s content for the operator to collect.
Command Processing and Payload Delivery
Whichever channel delivers a command, GraphFleet passes it to a central dispatcher. Commands that begin with a colon (or an exclamation mark) control the implant itself, and are listed in the table below. Any other text is run as a Windows shell command in a hidden cmd.exe window, and the output is sent back through the same Graph channel.
| Command | What it does |
|---|---|
| :exit | Stops the implant |
| :sleep <milliseconds> | Changes how often GraphFleet checks for new commands |
| :pull <cloud-path> <local-path> | Downloads a file from the account's OneDrive to the victim’s machine. |
| :persist | Sets up persistence so GraphFleet starts again when the user logs in |
| :unpersist | Removes that user-level persistence |
| :persist-sys | Sets up persistence through the Windows print system |
| :unpersist-sys | Removes the print-system persistence |
| :status | Reports GraphFleet's current state, such as its persistence and connection status |
| :update <cloud-path> <sha256> | Replaces GraphFleet with a new version downloaded from OneDrive, after checking its hash. Leaves .new, .old, and .pending files next to the executable |
| :load <cloud-path> [sha256] | Downloads a payload, either shellcode or a 64-bit PE, and runs it in memory inside GraphFleet's own process |
| :run <module> [sha256] | Downloads a script and runs it in an internal virtual machine (VM) that can run shell commands and read files |
| Any other text | Runs as a shell command in a hidden cmd.exe window and returns the output |
Persistence
The :persist command gives GraphFleet three ways to start again when the user signs in:
- It writes the executable path to the UserInitMprLogonScript value under HKCU\Environment.
- It adds a OneDriveSyncSetup value under the current user's Run key at HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
- It creates OneDriveSync.cmd in the user's Startup folder at %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup, which launches the executable using start.
A .legs file next to the executable acts as a repair switch. While it exists, GraphFleet regularly checks all three and recreates any that are missing or no longer point to the current executable.
The :persist-sys command uses a more privileged method based on Windows print processors. Unlike the user-level methods, which restart GraphFleet directly, it installs a separate, externally supplied component named mpf64, which was not available for analysis. GraphFleet copies mpf64 into the system print-processor directory, registers it under HKLM\SYSTEM\CurrentControlSet\Control\Print\Environments\Windows x64\Print Processors, and assigns it to a printer. Once registered and assigned to a printer, the component can be loaded by the Windows Print Spooler.
Configuration Recovery Through BSC
If normal token refresh, the local cache, and the built-in SharePoint token drop all fail, GraphFleet tries one final recovery route. It queries the public BNB Smart Chain (BSC) services bsc-dataseed[.]bnbchain[.]org and bsc[.]publicnode[.]com. It makes a read-only eth_call to contract 0x0BF2981759296aC3cf2A4b9d6D8f681C0807432F;
GraphFleet verifies the returned data with HMAC-SHA-256 and decrypts it only if verification succeeds. The decrypted configuration is designed to contain new tenant and client IDs and a token_drop URL. GraphFleet uses that URL to download a replacement refresh token.
Placing GraphFleet in Context
The Microsoft Graph APIs and techniques GraphFleet relies on have been documented in earlier threat research, including the use of Outlook Tasks and calendar events as C2 channels, and the storage of malware configuration in public blockchain smart contracts. GraphFleet stands out in how it combines and extends these ideas.
It hides commands and results in Calendar open extensions instead of event content, falls back across Outlook Drafts, To Do, and OneDrive when Calendar fails, signs in with a user's refresh token as Microsoft Teams instead of with app credentials, and recovers lost access through its local cache, SharePoint, and the blockchain without any change to the malware.
Conclusion
GraphFleet shows how an operator can build durable command and control entirely from trusted cloud services. Microsoft Graph carries the commands and results, SharePoint and a BSC smart contract provide ways to restore lost access, and sign-ins that appear as Microsoft Teams help the traffic blend into normal activity.
For defenders, no single Calendar event, draft, task, or file is enough to raise an alarm. The signal is in the pattern: host-specific object names, Teams sign-ins from unexpected devices or locations, hidden command execution, and persistence changes appearing together. Organizations that find these artifacts should also revoke the refresh tokens of the affected accounts.
Detecting GraphFleet with Rubrik
Rubrik Threat Monitoring scans backups for known threats using file hashes and YARA rules from the Rubrik Zero Labs (RZL) threat intelligence feed. The GraphFleet hashes and the YARA rule below are part of that feed, so Threat Monitoring can alert when a backup contains a match.
File Indicators
| Description | SHA-256 |
|---|---|
| GraphFleet sample | 97072aeeb8774462c5242c042e58416d165e9fd7d92f7e167c9d2ebe8407856a |
| GraphFleet sample | f5050d9dfbec1daefd27c98d9c086153a311cff62b2224236d48d906da60aa75 |
| GraphFleet sample | 44a49ad0108ceec79e8c2db0ab613cc492628c4774dca2fdf285aae227d8d3fe |
| GraphFleet sample | ee422ebf72f92384e3779c946a3832ef2e5a0e215f8b8fc484ee807d1cc73a97 |
| GraphFleet sample | f7d04b537d67c14cf1d37fff34496527050714e21443664f4c43180442d2dd0f |
| GraphFleet sample | 0e05363634035980ddc963debf9d47bb161202b31fa078d865315d613e7cdefc |
Network Indicators
| Description | Indicator |
|---|---|
| SharePoint tenant hosting the refresh-token drop | heightcontrollawnandlandsca-my[.]sharepoint[.]com |
| BSC smart contract storing encrypted recovery configuration | 0x0BF2981759296aC3cf2A4b9d6D8f681C0807432F |
Yara Rule
rule rbrk_GraphFleet_C2
{
meta:
description = "Detects GraphFleet using its encrypted campaign, mutex, and Calendar C2 strings"
author = "Rubrik Zero Labs"
category = "Backdoor"
date = "2026-10-28"
strings:
// Single-byte XOR variants used by most samples
$xor_campaign = "gcx-fleet-2026-10" ascii xor(0x01-0xff)
$xor_mutex = "Global\\gc-7f2a91c4" ascii xor(0x01-0xff)
$xor_c2 = "com.c2.cmd" ascii xor(0x01-0xff)
condition:
uint16(0) == 0x5A4D and
filesize < 256KB and
all of ($xor_*)
}
MITRE ATT&CK Mapping
| Tactic | Technique ID | Technique Name |
|---|---|---|
| Execution | T1059.003 | Command and Scripting Interpreter: Windows Command Shell |
| Persistence | T1547.001 | Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder |
| Persistence | T1037.001 | Boot or Logon Initialization Scripts: Logon Script (Windows) |
| Persistence | T1547.012 | Boot or Logon Autostart Execution: Print Processors |
| Defense Evasion | T1550.001 | Use Alternate Authentication Material: Application Access Token |
| Command and Control | T1102.002 | Web Service: Bidirectional Communication |
| Command and Control | T1102.001 | Web Service: Dead Drop Resolver |
| Command and Control | T1008 | Fallback Channels |
| Command and Control | T1105 | Ingress Tool Transfer |
LATEST BLOGS