Detect Command and Control Configuration Resolution via GitHub/Gist Raw Content Dead Drop in Sumo Logic CSE
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.
MITRE ATT&CK
- Tactic
- Command and Control
Sumo Detection Query
_sourceCategory=dns/infoblox
| parse "query: * IN * " as query_name, query_type nodrop
| parse "client: *#" as client_ip nodrop
| where query_name matches "*raw.githubusercontent.com*" or query_name matches "*gist.githubusercontent.com*" or query_name matches "*gist.github.com*" or query_name matches "*objects.githubusercontent.com*"
| count as QueryCount, min(_messagetime) as FirstSeen, max(_messagetime) as LastSeen by client_ip, query_name
| sort by FirstSeen asc Sumo Logic query over Infoblox DNS query logs, parsing the standard 'query: <name> IN <type>' and 'client: <ip>#<port>' log fields to isolate resolutions of the raw-content GitHub/Gist hosts. Like the QRadar variant, DNS query logs carry no process attribution, so this surfaces which internal client resolved a dead-drop-capable domain without confirming which process did so or whether a new connection followed; pair with a companion query over _sourceCategory=network/paloalto (or your firewall/flow log source) filtering for a new destination from the same client_ip within 15 minutes, and with endpoint process telemetry, before escalating a match.
Data Sources
Required Tables
False Positives & Tuning
- Developer and CI hosts resolving raw-content hosts as part of routine dependency or update checks
- Security automation or IaC tooling pulling configuration from a GitHub-hosted repository on a schedule
- Any resolution with no genuinely new follow-on connection, since this variant does not implement that correlation
- Corporate DNS forwarders/resolvers that log on behalf of many downstream clients under a single client_ip
Other platforms for THREAT-C2-GitHubRawContentDeadDropResolver
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.
- 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.
- 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.
- 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.
References (7)
- https://attack.mitre.org/techniques/T1102/001/
- https://attack.mitre.org/tactics/TA0011/
- https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file
- https://docs.github.com/en/rest/gists/gists
- https://github.com/redcanaryco/atomic-red-team/tree/master/atomics/T1102.001
- https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-devicenetworkevents-table
- https://learn.microsoft.com/en-us/sysinternals/downloads/sysmon
Response Playbook
Triage
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- 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.
- 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
Related Techniques
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.
// 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 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.
// 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 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
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
$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
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.
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
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
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.
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
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
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.