THREAT-C2-GitHubRawContentDeadDropResolver

Command and Control Configuration Resolution via GitHub/Gist Raw Content Dead Drop

Command and Control Last updated:

Adversaries increasingly host a short, tightly-scoped blob of encoded C2 configuration — typically a base64, hex, or single-byte-XOR-obfuscated IP:port or domain string well under a kilobyte — on raw.githubusercontent.com or gist.githubusercontent.com, the plain-text CDN endpoints GitHub serves repository files and gists from with no HTML wrapper. A dropper or stager fetches the raw file directly with a non-browser HTTP client (PowerShell's WebClient/Invoke-WebRequest, Python requests, a compiled .NET or Go HTTP client, certutil, or curl carrying a non-browser user agent), decodes it locally, and then opens a brand-new outbound connection to whatever address the content resolved to. Because raw.githubusercontent.com and gist.githubusercontent.com are legitimate, CDN-fronted GitHub infrastructure that developer tooling, CI runners, package managers, and IDE extensions all depend on, defenders cannot blocklist the hosts outright without breaking normal engineering workflows — the same constraint that makes dead-drop resolvers effective on Pastebin, Twitter, and Google Docs. What is detectable is the behavioral sequence rather than the host itself: legitimate fetches of these endpoints come from identifiable developer tooling (git.exe, VS Code's update/extension-fetch paths, npm/pip package resolution) and are not followed by that same process opening a connection to a destination it has never contacted before, whereas a dead-drop resolver fetch by a scripting engine or unusual binary is, within minutes, followed by exactly that novel outbound connection — the resolver having just told the implant where to go. This record differs from the T1102.001 base detection, which screens a broad, largely static list of dead-drop hosting platforms (Pastebin, GitHub, Twitter, Google Docs, YouTube, TechNet, and others) for non-browser access alone, by narrowing to the specific code-hosting-CDN pattern and adding the resolve-then-connect temporal correlation, which a static domain list has no way to express and which is what actually separates a malicious dead-drop fetch from the very large volume of legitimate raw.githubusercontent.com traffic on any developer-heavy estate.

What is THREAT-C2-GitHubRawContentDeadDropResolver Command and Control Configuration Resolution via GitHub/Gist Raw Content Dead Drop?

Command and Control Configuration Resolution via GitHub/Gist Raw Content Dead Drop (THREAT-C2-GitHubRawContentDeadDropResolver) maps to the Command and Control tactic — the adversary is trying to communicate with compromised systems to control them in MITRE ATT&CK.

This page provides production-ready detection logic for Command and Control Configuration Resolution via GitHub/Gist Raw Content Dead Drop, covering the data sources and telemetry it touches: Network Traffic: Network Connection Creation, Network Traffic: Network Connection Content, Microsoft Defender for Endpoint advanced hunting (DeviceNetworkEvents). The queries below are rated high severity at medium confidence, and ship for 7 SIEM platforms — KQL, SPL, Elastic, QRadar, Sumo, YARA-L, LogScale.

MITRE ATT&CK

Tactic
Command and Control
Microsoft Sentinel / Defender
kusto
let LookbackWindow = 1d;
let PriorContactWindow = 15d;
let CorrelationWindow = 15m;
let DeadDropHosts = dynamic(["raw.githubusercontent.com", "gist.githubusercontent.com", "gist.github.com", "objects.githubusercontent.com"]);
let BrowserProcesses = dynamic(["chrome.exe", "msedge.exe", "firefox.exe", "brave.exe", "opera.exe", "iexplore.exe", "safari.exe"]);
let DevToolProcesses = dynamic(["git.exe", "code.exe", "node.exe", "npm.exe", "pip.exe", "python.exe", "devenv.exe"]);
let ConfigFetch =
    DeviceNetworkEvents
    | where Timestamp > ago(LookbackWindow)
    | where RemoteUrl has_any (DeadDropHosts)
    | where InitiatingProcessFileName !in~ (BrowserProcesses)
    | project FetchTime = Timestamp, DeviceId, DeviceName, FetchProcess = InitiatingProcessFileName,
              FetchCommandLine = InitiatingProcessCommandLine, FetchUrl = RemoteUrl,
              IsDevTool = InitiatingProcessFileName in~ (DevToolProcesses);
let PriorContacts =
    DeviceNetworkEvents
    | where Timestamp between (ago(PriorContactWindow) .. ago(LookbackWindow))
    | where ActionType == "ConnectionSuccess"
    | summarize by DeviceId, RemoteIP;
let NovelConnections =
    DeviceNetworkEvents
    | where Timestamp > ago(LookbackWindow)
    | where ActionType == "ConnectionSuccess"
    | where isnotempty(RemoteIP) and ipv4_is_private(RemoteIP) == false
    | join kind=leftanti (PriorContacts) on DeviceId, RemoteIP
    | project ConnectTime = Timestamp, DeviceId, ConnectProcess = InitiatingProcessFileName, RemoteIP, RemotePort;
ConfigFetch
| join kind=inner (NovelConnections) on DeviceId
| where ConnectTime between (FetchTime .. FetchTime + CorrelationWindow)
| where ConnectProcess =~ FetchProcess
    or ConnectProcess in~ ("powershell.exe", "pwsh.exe", "cmd.exe", "rundll32.exe", "mshta.exe", "wscript.exe", "cscript.exe", "regsvr32.exe")
| extend GapSeconds = datetime_diff('second', ConnectTime, FetchTime)
| project FetchTime, ConnectTime, GapSeconds, DeviceName, DeviceId, FetchProcess, FetchCommandLine, FetchUrl,
          ConnectProcess, RemoteIP, RemotePort, IsDevTool
| sort by FetchTime desc

Detects the resolve-then-connect dead-drop pattern on Microsoft Defender for Endpoint telemetry by correlating two DeviceNetworkEvents arms per device. Arm 1 (ConfigFetch) selects RemoteUrl access to raw.githubusercontent.com, gist.githubusercontent.com, gist.github.com, or objects.githubusercontent.com from a non-browser process. Arm 2 (NovelConnections) computes, per device, outbound TCP connections to a public RemoteIP that a leftanti join shows the device has not successfully connected to in the preceding 15 days — the genuine 'this is a brand-new destination' signal, not just any connection. The two arms are joined on DeviceId with the novel connection required to land within 15 minutes after the fetch, and the connecting process required to match the fetching process or be a common LOLBin/scripting host that a dropper would hand off execution to. ipv4_is_private is used rather than an 'in' list because KQL 'in' performs string equality and cannot evaluate CIDR membership. The IsDevTool flag is carried through rather than used as a hard exclusion, since it is a useful triage signal (git.exe or code.exe matching the pattern is far more likely to be a coincidental developer workflow) without silently dropping a compromised developer tool from view.

high severity medium confidence

Data Sources

Network Traffic: Network Connection Creation Network Traffic: Network Connection Content Microsoft Defender for Endpoint advanced hunting (DeviceNetworkEvents)

Required Tables

DeviceNetworkEvents

False Positives

  • Developer workstations and CI/build agents where git.exe, npm, pip, or an IDE extension fetches raw.githubusercontent.com/gist content as part of normal dependency resolution or update checks, and then separately opens an unrelated new connection within the correlation window purely by coincidence
  • Package managers and dependency-scanning tools that resolve a raw GitHub file as one step of a multi-host installation sequence, followed by a legitimate new connection to a package registry CDN edge
  • Security tooling (SOAR playbooks, threat-intel feed pullers, IaC pipelines) that legitimately fetch raw configuration or IOC lists from a GitHub-hosted repository on a schedule
  • VS Code, GitHub Desktop, or other developer applications' own update-check or telemetry paths reaching these hosts under a non-browser process name

Sigma rule & cross-platform mapping

The detection logic for Command and Control Configuration Resolution via GitHub/Gist Raw Content Dead Drop (THREAT-C2-GitHubRawContentDeadDropResolver) above is provided in a vendor-neutral form so you can deploy it on any SIEM. The same logic is shipped here as native KQL (Microsoft Sentinel / Defender), SPL (Splunk), Elastic (Elastic Security (EQL)), QRadar (IBM QRadar (AQL)), Sumo (Sumo Logic CSE), YARA-L (Google Chronicle / SecOps), LogScale (CrowdStrike LogScale (CQL)) queries. In Sigma terms, this detection targets the following logsource:

logsource:
  category: network_connection
  product: windows

Browse the community-maintained Sigma rules for this technique:


Testing Methodology

Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.

  1. Test 1Fetch Encoded Config from a GitHub Raw Content URL via PowerShell, Then Connect (Windows)

    Expected signal: MDE DeviceNetworkEvents / Sysmon Event ID 22: a DNS/HTTP access to raw.githubusercontent.com initiated by powershell.exe. Within seconds, a second network event (Sysmon Event ID 3 / MDE ConnectionSuccess) shows powershell.exe connecting to the decoded test destination and port.

  2. Test 2Fetch Config from a Gist Raw URL via curl, Then Connect (Linux)

    Expected signal: EDR-for-Linux process and network telemetry: curl execution reaching gist.githubusercontent.com, followed within seconds by a new outbound TCP connection from the same shell session to the decoded test host and port.

  3. Test 3Benign Raw-Content Fetch with No Follow-On Connection, Negative Control (Windows)

    Expected signal: A single DeviceNetworkEvents/Sysmon Event ID 22 record for the raw.githubusercontent.com access from powershell.exe, with no accompanying new outbound connection in the following 15 minutes.


Response Playbook

Triage

  1. Confirm the fetching process is not a recognised developer or CI tool (git, code.exe, npm, pip). A scripting engine (powershell.exe, wscript.exe, mshta.exe) or an unsigned/renamed binary fetching a raw GitHub/Gist URL is a far stronger indicator than the same fetch from an IDE or package manager.
  2. Pull the actual raw content that was fetched, if it is still reachable at the URL or recoverable from proxy/EDR content capture. A short blob (well under 1KB) that decodes as base64/hex to an IP:port or domain string is strong confirmation of a dead-drop resolver rather than legitimate file content.
  3. Check whether the destination reached immediately after the fetch is genuinely novel for that host — a first-ever connection to an address is materially more suspicious than a connection to infrastructure the host talks to routinely.
  4. Review the file/repository/gist itself for edit history if it is still public: dead-drop resolvers are frequently updated (new commit, new gist revision) shortly before or during active operations to rotate the C2 address without touching the malware binary.
  5. Identify the process that made the follow-on connection and whether it is the same process that fetched the content, was spawned by it, or is an injected/hollowed process — this establishes whether the resolver output was consumed directly or handed off.
  6. Search for the same raw-content URL, repository, or gist ID appearing across other hosts in the environment, which would indicate a shared dropper rather than an isolated event.

Containment

  1. Isolate the endpoint via EDR network isolation rather than relying solely on a URL/domain block, since raw.githubusercontent.com and gist.githubusercontent.com cannot be blocklisted wholesale without breaking developer and CI workflows.
  2. Block the specific resolved C2 destination IP/domain at the perimeter and in EDR once identified from the follow-on connection, understanding that the operator may republish an updated dead-drop pointing elsewhere.
  3. Terminate the process that consumed the dead-drop content and any child process it spawned, preserving process memory first where the decoded C2 address may still be resident.
  4. Where the specific gist ID or repository path is identifiable and clearly malicious, report it to GitHub for takedown, and separately hunt for any other host in the estate that has fetched the same URL.

Evidence Collection

  1. The raw content payload itself (captured via proxy, EDR content inspection, or a timely re-fetch), plus the exact URL, gist ID, or repository path and commit/revision hash if recoverable.
  2. Full DeviceNetworkEvents/Sysmon history for the host spanning both the fetch and the follow-on connection, plus at least 15 days prior to establish whether the follow-on destination is genuinely novel.
  3. Process creation and command-line telemetry for the fetching process and any process it spawned or handed off to, including parent-process lineage back to the initial dropper or delivery vector.
  4. Any locally cached or dropped file containing the decoded configuration (temp directories, ADS, registry run keys) that would show how the resolved address was persisted or consumed.

Escalation Criteria

  • ! The fetching process is a scripting engine, LOLBin, or unsigned/renamed binary rather than recognised developer tooling, and it is followed by a genuinely novel outbound connection within minutes.
  • ! The decoded dead-drop content resolves to infrastructure with existing threat-intelligence associations, or the same gist/repository is referenced by known malware samples.
  • ! The same dead-drop URL or resolved destination appears across multiple hosts in a short window, indicating operator-deployed tooling rather than an isolated developer coincidence.
  • ! The activity occurs on a server, domain controller, build/CI system, or other privileged host rather than an ordinary developer workstation.
  • ! Post-resolution host behaviour shows staging, credential access, or collection activity, indicating the dead-drop successfully bootstrapped an active C2 channel.

Investigation Guide

Forensic Artifacts

  • > Proxy or EDR-captured HTTP response body from the raw-content fetch, which contains the encoded configuration and is the single most direct piece of evidence available.
  • > DeviceNetworkEvents (MDE) or Sysmon Event ID 22 + Event ID 3 pairs showing the DNS resolution of the raw-content host followed by the novel outbound connection.
  • > Process creation and command-line history for the fetching process, establishing whether it was an interactive user action, a scheduled task, or a headless dropper.
  • > Browser or application cache/history (if the fetch went through a browser-embedded control) versus the absence of any browser artefact (if it went through a bare HTTP client), which helps distinguish a user click from an automated fetch.
  • > GitHub's public commit/revision history for the repository or gist, when still accessible, showing how and when the dead-drop content was last changed.

Tuning Guidance

Build the developer/CI allowlist first: run the fleet-wide non-browser fetch hunt over 30 days, identify every process name and host role (build agents, IDE update paths, package managers) that legitimately and routinely touches raw.githubusercontent.com or gist.githubusercontent.com, and exclude those specific DeviceId/process-name pairs rather than excluding the hosts entirely, since the same hosts are exactly what a dead-drop resolver depends on. Do not drop the novel-destination requirement (the KQL leftanti join) to simplify the rule — it is what separates 'this process fetched a raw file and also happened to make a connection' from 'this process fetched a raw file and then went somewhere it has never been,' and removing it will produce far more noise on any estate with active development activity. Where a network sensor or proxy captures response body size or content, layer in the short-response hunting query as a corroborating signal before escalating a match that has no independent confirmation. If your environment cannot support the 15-day prior-contact lookback at scale, shorten it rather than removing it — even a 3-to-5-day window still filters out a host's routine, repeatedly-contacted infrastructure and preserves most of the detection's value.


Hunting Queries

Fleet-wide baseline hunt for any non-browser resolution of the raw-content GitHub/Gist hosts over 30 days, independent of any follow-on connection. This is the right starting point for building an allowlist of legitimate developer and CI processes that routinely touch these hosts, against which the correlated detection's remaining hits can be triaged much faster.

Hunting — KQL
kql
// Hunt: any non-browser fetch of a GitHub/Gist raw-content host in the last 30 days, regardless of follow-on activity
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any ("raw.githubusercontent.com", "gist.githubusercontent.com", "gist.github.com", "objects.githubusercontent.com")
| where InitiatingProcessFileName !in~ ("chrome.exe", "msedge.exe", "firefox.exe", "brave.exe", "opera.exe")
| summarize FetchCount = count(), Urls = make_set(RemoteUrl, 20), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName, InitiatingProcessFileName
| sort by FirstSeen asc
Hunting — SPL
spl
index=windows sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=22
| eval QueryLower=lower(coalesce(QueryName, ""))
| eval ImageLower=lower(coalesce(Image, "unknown"))
| where (like(QueryLower, "%raw.githubusercontent.com%") OR like(QueryLower, "%gist.githubusercontent.com%") OR like(QueryLower, "%gist.github.com%"))
| where NOT (like(ImageLower, "%\\chrome.exe") OR like(ImageLower, "%\\msedge.exe") OR like(ImageLower, "%\\firefox.exe"))
| stats count as FetchCount, values(QueryLower) as Queries, min(_time) as FirstSeen, max(_time) as LastSeen by ComputerName, ImageLower
| eval FirstSeen=strftime(FirstSeen, "%Y-%m-%d %H:%M:%S"), LastSeen=strftime(LastSeen, "%Y-%m-%d %H:%M:%S")
| sort FirstSeen

Hunts for unusually small response bodies from raw-content hosts, which is the size signature of a short encoded config blob rather than genuine source code, a README, or a binary release asset. This is a content-shape signal that is independent of the process-identity and timing correlation used in the primary detection and works well as a second, orthogonal pass over the same data source.

Hunting — KQL
kql
// Hunt: short (sub-1KB) responses from raw-content hosts, a size profile consistent with dead-drop config rather than source/binary content
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any ("raw.githubusercontent.com", "gist.githubusercontent.com")
| where isnotempty(ResponseBodySizeInBytes)
| where ResponseBodySizeInBytes < 1024
| summarize HitCount = count(), Urls = make_set(RemoteUrl, 20), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName, InitiatingProcessFileName
| sort by FirstSeen asc
Hunting — SPL
spl
index=proxy ("raw.githubusercontent.com" OR "gist.githubusercontent.com")
| eval resp_bytes=tonumber(sc_bytes)
| where resp_bytes < 1024 AND resp_bytes > 0
| stats count as HitCount, values(cs_uri_stem) as Urls, min(_time) as FirstSeen, max(_time) as LastSeen by c_ip, cs_user_agent
| eval FirstSeen=strftime(FirstSeen, "%Y-%m-%d %H:%M:%S"), LastSeen=strftime(LastSeen, "%Y-%m-%d %H:%M:%S")
| sort FirstSeen

Atomic Red Team Tests

Test 1 Fetch Encoded Config from a GitHub Raw Content URL via PowerShell, Then Connect (Windows)
windows

Simulates the core two-stage pattern: PowerShell fetches a small base64 string from a raw.githubusercontent.com URL using Invoke-WebRequest (a non-browser client), decodes it locally, and immediately opens a new TCP connection to the decoded (attacker-controlled test) address. Use a raw file you control in a scratch repository/gist containing only a harmless base64 string, and point the decoded connection at a benign test listener you control — never at a real third party.

Command

powershell
$raw = Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/<your-test-org>/<your-test-repo>/main/config.txt' -UseBasicParsing
$decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($raw.Content.Trim()))
Write-Host "Decoded dead-drop content: $decoded"
$parts = $decoded -split ':'
Test-NetConnection -ComputerName $parts[0] -Port ([int]$parts[1]) | Out-Null
Write-Host 'GitHub raw dead-drop fetch-then-connect test complete'

Cleanup

powershell
Remove-Variable raw, decoded, parts -ErrorAction SilentlyContinue

Expected Telemetry

MDE DeviceNetworkEvents / Sysmon Event ID 22: a DNS/HTTP access to raw.githubusercontent.com initiated by powershell.exe. Within seconds, a second network event (Sysmon Event ID 3 / MDE ConnectionSuccess) shows powershell.exe connecting to the decoded test destination and port.

Expected Detection

The KQL rule's ConfigFetch arm matches the powershell.exe access to the raw-content URL, and the NovelConnections arm matches the immediate follow-on connection provided the test destination has not been contacted by the host in the prior 15 days — use a fresh, never-before-contacted test listener to guarantee this. The two arms join on DeviceId within the 15-minute correlation window and the rule fires. The SPL transaction-based rule fires equivalently on the DNS-query-then-connect pair.

Test 2 Fetch Config from a Gist Raw URL via curl, Then Connect (Linux)
linux

Simulates the same pattern on Linux using curl (a non-browser HTTP client) against a gist.githubusercontent.com raw URL, followed by a netcat connection to the decoded test address. Use a gist you control containing only a benign hex-encoded string.

Command

bash
raw=$(curl -s 'https://gist.githubusercontent.com/<your-test-user>/<gist-id>/raw/config.txt')
decoded=$(echo "$raw" | xxd -r -p)
host=$(echo "$decoded" | cut -d: -f1)
port=$(echo "$decoded" | cut -d: -f2)
timeout 5 bash -c "cat < /dev/null > /dev/tcp/${host}/${port}" 2>/dev/null || true
echo 'Gist raw dead-drop fetch-then-connect test complete'

Cleanup

bash
unset raw decoded host port

Expected Telemetry

EDR-for-Linux process and network telemetry: curl execution reaching gist.githubusercontent.com, followed within seconds by a new outbound TCP connection from the same shell session to the decoded test host and port.

Expected Detection

The KQL/EQL correlated rules fire on the fetch-then-connect pair from the same DeviceId/host.id within the 15-minute window, provided the test destination is genuinely new to the host. This test also exercises the Elastic EQL sequence rule's dns-then-network correlation.

Test 3 Benign Raw-Content Fetch with No Follow-On Connection, Negative Control (Windows)
windows

Fetches the same raw.githubusercontent.com URL via PowerShell but performs no subsequent network activity, validating that Arm 1 alone (without a correlated novel connection) does not trigger the primary correlated detection and only surfaces in the broader baseline hunting query.

Command

powershell
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/<your-test-org>/<your-test-repo>/main/config.txt' -UseBasicParsing | Out-Null
Start-Sleep -Seconds 60
Write-Host 'Negative control fetch-only test complete'

Cleanup

powershell
None required.

Expected Telemetry

A single DeviceNetworkEvents/Sysmon Event ID 22 record for the raw.githubusercontent.com access from powershell.exe, with no accompanying new outbound connection in the following 15 minutes.

Expected Detection

The correlated KQL/SPL/EQL rules should NOT fire, since the NovelConnections/endswith arm never materialises. The event should appear only in the fleet-wide baseline hunting query, confirming the primary rule's correlation requirement is functioning as a noise-reduction control rather than alerting on the fetch alone.

Related Detections