GRIDTIDE’s C2 Is a Google Sheet You Don’t Own. Your Workspace Audit Log Is Blind to It
A Linux server in a telecom or government network polls sheets.googleapis.com once a second, reads cell A1 of a spreadsheet for its next shell command, writes the output back into the rows underneath, and wipes the first thousand rows at the start of every session so the operator’s history never piles up. That spreadsheet does not live in your Google Workspace tenant. The attacker owns it — an ordinary Drive object reached through their own service account and Google Cloud project, never provisioned in your directory — and GRIDTIDE authenticates with credentials that aren’t in the binary at all: it decrypts them from configuration stored in the attacker’s Google Drive, using a 16-byte key it expects to find in a separate file on the host. So the Workspace admin audit log, the first place your instinct sends you, shows nothing at all. That gap is where GRIDTIDE lives.
TL;DR
- GRIDTIDE (UNC2814, suspected PRC-nexus) runs a Linux ELF backdoor that uses an attacker-owned Google Sheet as C2 — reading commands from cell A1 and writing output below — hitting 53 confirmed victims across 42 countries, primarily telecommunications providers and government organizations.
- Because the spreadsheet is attacker-owned — reached through their own service account and Cloud project, never in your directory — your Workspace admin, Drive, and Alert Center audit logs show nothing; pulling those logs is the wrong first move and yields false confidence.
- Detection has to come from what you control: egress (a non-browser server process hitting sheets.googleapis.com with a Java-client user-agent, plus batchClear on a1:z1000 and valueRenderOption=FORMULA) and the host (a short-named /var/tmp binary spawning sh -c id and a systemd unit at /etc/systemd/system/xapt.service).
- Tune network detections by scoping to server tiers that never touch Sheets and allowlisting the Python UA of legitimate data-eng pipelines; also hunt the SoftEther VPN (hamcore.se2) — a parallel channel that touches Google not at all, infrastructure UNC2814 has run since 2018.
- There’s no patch — the API works as designed — so the real control is default-deny egress (SC-7/CM-7) from server VLANs, which removes the channel outright rather than monitoring for it after the fact.
Google’s Threat Intelligence Group and Mandiant disrupted the campaign in February 2026, attributing it to UNC2814, a suspected PRC-nexus espionage group they’ve tracked since 2017. The confirmed count was 53 victims across 42 countries — primarily telecommunications providers and government organizations, some compromised for years, with suspected activity in 20 more. GTIG is explicit that this is not a Google product vulnerability — GRIDTIDE abuses the Sheets API exactly as documented. That’s the uncomfortable part. There is no patch for “the API works as designed.”
The channel
Strip away the espionage framing and the mechanism is almost boring, which is why it’s effective. GRIDTIDE is a C-based ELF backdoor. It doesn’t carry its credentials in the binary: it expects a 16-byte key in a separate file on the host, uses that key to decrypt configuration data it pulls from the attacker’s Google Drive with AES-128 in CBC mode, and that configuration is what holds the service account, its private key, and the target spreadsheet ID. With those in hand it authenticates to the Sheets API and treats one spreadsheet as a duplex socket.
The layout is fixed. Cell A1 carries the command and the malware’s status reply. Cells A2 downward move bulk data — command output, file chunks in 45-KB fragments. Cell V1 holds the base64’d host metadata dropped on first run: hostname, OS, local IP, working directory, timezone. Commands follow a type-id-arg-arg grammar; a shell command arrives base64-encoded in A1, the output lands in the rows below, and the malware answers with a terse status string.
The API calls are the tell, if you can see them:
GET /v4/spreadsheets/<id>/values/A1?valueRenderOption=FORMULA
POST /v4/spreadsheets/<id>/values:batchClear {"ranges":["a1:z1000"]}
POST /v4/spreadsheets/<id>/values:batchUpdate
Two things there don’t look like normal Sheets traffic. valueRenderOption=FORMULA is an unusual read mode — most legitimate readers pull FORMATTED_VALUE or UNFORMATTED_VALUE; FORMULA is chosen here to preserve raw cell content byte-for-byte. And a batchClear of a1:z1000 on every session start is the row-wipe — a range-clear that passive BI and reporting readers essentially never emit, and much higher signal the moment it’s paired with a server source, a Java-client UA, and the FORMULA render. The polling cadence is a second at a time while it waits for work, then after roughly 120 empty polls it backs off to a random 5-to-10-minute interval to go quiet. One second of jitter-free polling is loud; the backoff is the part that lets it sit unnoticed for months once the operator steps away.
Persistence and lateral movement are old-fashioned by comparison. A systemd unit at /etc/systemd/system/xapt.service respawns the binary from /usr/sbin/xapt — xapt masquerading as a Debian package tool. Reconnaissance is the usual /bin/sh -c id off a dropper in /var/tmp. As a parallel channel that doesn’t touch Google at all, UNC2814 stands up a SoftEther VPN bridge for encrypted egress — VPN metadata suggests they’ve run this specific infrastructure since July 2018, so it isn’t a contingency they spin up when Sheets gets burned; it’s a second path running alongside. If Sheets visibility is where the hunt starts, the SoftEther path is the one you still have to rule out.
Why your first instinct is the wrong log
Here is the thing most teams get wrong in the first hour: they pull the Google Workspace audit logs, filter for Sheets and Drive API activity, and find nothing suspicious, and they conclude the host is clean.
The sheet isn’t in your tenant. It’s an attacker-owned Drive document, reached through the attacker’s own service account and Google Cloud project. Your Workspace admin console, your Drive audit log, your Alert Center — none of them have visibility into a document you don’t own and a service account that was never provisioned in your directory. The Google-side telemetry that mattered belonged to Google, which is precisely why the disruption came from GTIG terminating the attacker’s Cloud projects and revoking the API access rather than from a victim SOC catching it in Workspace logs.
So the detection has to come from the two places you actually control: what leaves your network, and what runs on the host. Everything else is a dead end.
What the detection looks like
On egress, you need hostname-level visibility, which in practice means a forward proxy or TLS-inspecting gateway — Palo Alto with decryption, Zscaler, Netskope, the Elastic/Suricata equivalent if you’re rolling your own. The signal is a non-browser process on a server making Sheets API calls. GRIDTIDE carries the Google API Java client’s user-agent on the wire, so it reads Google-HTTP-Java-Client/1.42.3 (or a Directory API Google-API-Java-Client/2.0.0 variant), never a real Chrome or Edge string. In Splunk against a Palo Alto threat/url feed, the starting shape is something like:
index=proxy dest_host="sheets.googleapis.com"
http_user_agent="*Google-HTTP-Java-Client*"
src_ip IN (server_subnets)
| stats count min(_time) max(_time) dc(dest_host) by src_ip, http_user_agent
If you have full URL visibility, add uri_path="*batchClear*" OR uri_query="*valueRenderOption=FORMULA*" and your false-positive rate collapses toward zero, because almost nothing legitimate combines a server source, a Java client UA, and a formula-render read against Sheets.
Without decryption you’re working blind on the path, and you fall back to periodicity. During active operator interaction the cadence is one request per second — a loud, near-jitter-free beacon you can find in NetFlow if you can pin it to a hostname — but after roughly 120 empty polls GRIDTIDE drops to a random 5-to-10-minute sleep, so that periodicity is really only there while someone is driving it. The catch: sheets.googleapis.com resolves into Google’s shared IP ranges, which every browser in the building also hits, so a naive destination-IP beacon detector (RITA-style low-jitter scoring) drowns in Google noise. You need SNI granularity plus a source filter down to server tiers, and you need to accept that the backoff window is deliberately built to defeat exactly this analysis. TLS fingerprinting helps here — a JA4 hash for a Java client is not the JA4 you get from Chrome, and that survives even without decrypting payloads.
On the host is the higher-fidelity path, and honestly where I’d anchor primary detection. Linux EDR with process-and-network correlation — Falcon for Linux, SentinelOne, or auditd/Wazuh if that’s what you’ve got — catches the behavior that infrastructure rotation can’t hide. A short-named executable in /var/tmp spawning sh -c id. A new unit file dropped in /etc/systemd/system/ pointing at a binary in /usr/sbin. GTIG’s own detection that first surfaced this was named “Suspicious Shell Execution From Var Directory,” which tells you the endpoint tell was cleaner than the network one. GTIG also published a YARA rule keyed on the JWT header string and the {"ranges":["a1:z1000"]} literal baked into the binary; run it across your Linux estate, but treat it as a floor, not a ceiling — the C source recompiles and those strings shift, the systemd-plus-/var/tmp behavior doesn’t.
Don’t sleep on the SoftEther bridge. Hunt for the hamcore.se2 component and for unexpected VPN processes on servers that have no business running a VPN client. If the Sheets channel is what you found, the VPN is what you missed.
Where the false positives come from
The non-browser-hits-Sheets rule floods on day one in any shop with a data engineering team, and this is the tuning nobody warns you about. Every gspread job, every google-api-python-client pipeline pushing metrics into a spreadsheet, every Looker Studio connector and backup-to-Sheets cron is a non-browser process talking to the Sheets API. In a shop with an analytics function, expect the raw rule to fire dozens of times a day, almost all of it your own automation.
Three things separate the wheat from the chaff. The user-agent is the cheapest cut: Python automation reports google-api-python-client/x.y.z, not the Java Google-HTTP-Java-Client string GRIDTIDE carries, so a UA allowlist knocks out most of your own pipelines immediately. One caveat so you don’t over-trust it: a Java Google-API-client UA is not malicious on its own — legitimate Java or Scala enterprise jobs use the same client family — so it earns its keep only when it comes from a server tier with no Sheets business case and lines up with GRIDTIDE’s API paths. Subnet scoping is the second: the rule should never look at your data-eng hosts or CI runners in the first place — it should watch the tiers that have no earthly reason to reach Google Sheets. A telecom HLR, a RADIUS box, a billing database, a DMZ edge server. When one of those beacons to sheets.googleapis.com, there is no benign story. Third is the FORMULA render option and the a1:z1000 clear, which legitimate readers essentially never emit together. Scope by where the traffic originates first, filter by UA second, confirm on the API verbs.
The residual false positive after all that is usually a rogue admin who wired up a quick Sheets export from a server they shouldn’t have. Which is its own finding.
Egress is the real control
Detection is the consolation prize here. The reason GRIDTIDE — and the Google Sheets C2 pattern generally, which predates it — works for years is flat outbound: server tiers that can reach the entire internet over 443 because nobody ever wrote the deny rule.
A telecom core server does not need to reach Google Sheets. It probably doesn’t need general internet egress at all. Default-deny outbound from your server VLANs with an explicit allowlist for the handful of destinations those hosts legitimately require kills this channel outright, no signature needed. This is CM-7 least functionality and SC-7 boundary protection doing the work that SI-4 monitoring gets stuck cleaning up after when the egress door was left open. The gap between “we monitor for it” and “it can’t happen” is the gap between finding GRIDTIDE in year three and never giving it a channel in year one.
Map it out and the control story is unambiguous:
| Control | Family | What it does against GRIDTIDE |
|---|---|---|
| SC-7 / SC-7(4) | System & Communications Protection | Default-deny egress from server tiers severs the Sheets and SoftEther channels |
| CM-7 | Configuration Management | Least functionality — the box has no business reaching Google at all |
| SI-4 | System & Information Integrity | Non-browser-to-Sheets and /var/tmp shell-spawn detections |
| AU-6 | Audit & Accountability | Proxy/EDR review anchored on server-tier egress, not Workspace logs |
| IA-5 / AC-6 | Identification & Authentication / Access Control | Service-account inventory and least privilege; the attacker’s embedded creds never touched your directory, but your own should be scoped so a stolen one can’t do this |
The lesson worth carrying out of this one: when C2 rides a service your users legitimately reach every day, the audit trail you reach for first belongs to the wrong owner. Watch the egress from the tiers that should be silent, and take the door away from the servers that never needed it.
Sources
- Disrupting the GRIDTIDE global espionage campaign (Google Cloud / GTIG)
- Google disrupts UNC2814 GRIDTIDE campaign after 53 breaches across 42 countries (The Hacker News)
- China-linked hackers used Google Sheets to spy on telecoms and governments across 42 countries (CSO Online)
- China-linked hackers breached dozens of telecoms, government agencies (Cybersecurity Dive)
- Google disrupts Chinese cyberespionage campaign targeting telecoms, governments (SecurityWeek)
- Google and friends disrupt suspected Beijing espionage op (The Register)
This post was engineered and validated through a multi-agent AI workflow — drafted, adversarially reviewed by several independent models, checked against primary sources, and given a human review before publishing. See an inaccuracy, or found this useful? Leave a comment below — corrections and feedback are read and shape what comes next.