Created
August 17, 2025 18:50
-
-
Save lawbyte/bf32a4882e9365b7c38c1489adaab250 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| pcap_flag_solver.py | |
| ------------------- | |
| Extracts and decrypts AES/HMAC-protected C2 messages from a PCAP/PCAPNG and prints any CTF-like flags found. | |
| Assumptions (derived from embedded PowerShell in the PCAP): | |
| - $sharedHex is present in the capture and provides key material (32 bytes as hex). | |
| - AES-128-CBC for confidentiality. | |
| - HMAC-SHA256 over (IV || CT) for integrity. | |
| - Wire format per line: Base64( IV(16) || CT || HMAC(32) ). | |
| Usage: | |
| python3 pcap_flag_solver.py /path/to/network-log.pcapng [-o decrypted.txt] | |
| Requires: | |
| - Python 3.8+ | |
| - pycryptodome (preferred) or cryptography | |
| pip install pycryptodome | |
| """ | |
| import argparse, base64, hashlib, hmac, os, re, sys | |
| from typing import Iterable, List, Optional, Tuple | |
| # --- Crypto helpers (prefer pycryptodome) --- | |
| _UNPAD_ERR = "Invalid PKCS#7 padding" | |
| def pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes: | |
| if not data or len(data) % block_size != 0: | |
| raise ValueError(_UNPAD_ERR) | |
| pad = data[-1] | |
| if pad == 0 or pad > block_size or data[-pad:] != bytes([pad])*pad: | |
| raise ValueError(_UNPAD_ERR) | |
| return data[:-pad] | |
| try: | |
| from Crypto.Cipher import AES as _AES # type: ignore | |
| def aes_cbc_decrypt(key: bytes, iv: bytes, ct: bytes) -> bytes: | |
| return _AES.new(key, _AES.MODE_CBC, iv).decrypt(ct) | |
| _CRYPTO_BACKEND = "pycryptodome" | |
| except Exception: | |
| try: | |
| from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes # type: ignore | |
| from cryptography.hazmat.backends import default_backend # type: ignore | |
| def aes_cbc_decrypt(key: bytes, iv: bytes, ct: bytes) -> bytes: | |
| cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) | |
| decryptor = cipher.decryptor() | |
| return decryptor.update(ct) + decryptor.finalize() | |
| _CRYPTO_BACKEND = "cryptography" | |
| except Exception: | |
| print("[!] Install pycryptodome or cryptography (pip install pycryptodome)", file=sys.stderr) | |
| sys.exit(2) | |
| def find_shared_hex(blob: bytes) -> Optional[str]: | |
| """ | |
| Extract $sharedHex = \"...hex...\" from the PCAP bytes. | |
| Returns the hex string without spaces if found. | |
| """ | |
| m = re.search(br"\$sharedHex\s*=\s*\"([0-9a-fA-F\s]+)\"", blob) | |
| if not m: | |
| return None | |
| hex_s = re.sub(br"\s+", b"", m.group(1)) | |
| try: | |
| _ = bytes.fromhex(hex_s.decode()) | |
| except Exception: | |
| return None | |
| return hex_s.decode() | |
| def iter_b64_tokens(blob: bytes) -> Iterable[str]: | |
| """ | |
| Iterate possible base64 tokens embedded in printable regions of the blob. | |
| """ | |
| text = blob.decode("latin-1", errors="ignore") | |
| pattern = re.compile(r"(?:[A-Za-z0-9+/]{24,}={0,2})") | |
| for m in pattern.finditer(text): | |
| yield m.group(0) | |
| def decrypt_line(token: str, aes_key: bytes, hmac_key: bytes) -> Optional[str]: | |
| """ | |
| Validate HMAC and AES-decrypt a single Base64 token line. | |
| Returns plaintext string if valid, else None. | |
| """ | |
| try: | |
| raw = base64.b64decode(token) | |
| except Exception: | |
| return None | |
| if len(raw) < 16 + 32: | |
| return None | |
| iv, tag = raw[:16], raw[-32:] | |
| ct = raw[16:-32] | |
| if len(ct) % 16 != 0: | |
| return None | |
| calc = hmac.new(hmac_key, iv + ct, hashlib.sha256).digest() | |
| if calc != tag: | |
| return None | |
| try: | |
| pt = aes_cbc_decrypt(aes_key, iv, ct) | |
| pt = pkcs7_unpad(pt, 16) | |
| s = pt.decode("utf-8", errors="ignore") | |
| return s | |
| except Exception: | |
| return None | |
| def find_flags(text: str) -> List[str]: | |
| patterns = [ | |
| r"CBD\{[^\}\n]{4,200}\}", # challenge-specific prefix | |
| r"[A-Z]{2,20}\{[^\}\n]{4,200}\}", # generic | |
| r"flag\{[^\}\n]{4,200}\}", r"FLAG\{[^\}\n]{4,200}\}", | |
| ] | |
| out = set() | |
| for p in patterns: | |
| for m in re.findall(p, text): | |
| out.add(m) | |
| return sorted(out) | |
| def main(): | |
| ap = argparse.ArgumentParser(description="Decrypt C2 messages in PCAP and extract flag") | |
| ap.add_argument("pcap", help="Path to pcap/pcapng") | |
| ap.add_argument("-o", "--out", help="Write decrypted messages to this file") | |
| args = ap.parse_args() | |
| if not os.path.exists(args.pcap): | |
| print(f"[!] File not found: {args.pcap}", file=sys.stderr) | |
| sys.exit(1) | |
| blob = open(args.pcap, "rb").read() | |
| shared_hex = find_shared_hex(blob) | |
| if not shared_hex: | |
| print("[!] Could not locate $sharedHex in capture. Aborting.", file=sys.stderr) | |
| sys.exit(1) | |
| key_bytes = bytes.fromhex(shared_hex) | |
| if len(key_bytes) < 32: | |
| print("[!] $sharedHex must be at least 32 bytes (64 hex chars). Found:", len(key_bytes), "bytes", file=sys.stderr) | |
| sys.exit(1) | |
| aes_key = key_bytes[:16] | |
| hmac_key = key_bytes[16:32] | |
| print(f"[*] Crypto backend: {_CRYPTO_BACKEND}") | |
| print(f"[*] Extracted sharedHex ({len(key_bytes)} bytes). Using AES key (16) + HMAC key (16).") | |
| decrypted = [] | |
| flags = [] | |
| seen = set() | |
| for tok in iter_b64_tokens(blob): | |
| if tok in seen: | |
| continue | |
| seen.add(tok) | |
| msg = decrypt_line(tok, aes_key, hmac_key) | |
| if msg: | |
| decrypted.append(msg) | |
| flags.extend(find_flags(msg)) | |
| if args.out: | |
| with open(args.out, "w", encoding="utf-8") as f: | |
| f.write("=== Decrypted C2 Messages ===\n") | |
| for i, m in enumerate(decrypted, 1): | |
| f.write(f"[{i}] {m.strip()}\n") | |
| f.write("\n=== Extracted Flags ===\n") | |
| for fl in sorted(set(flags)): | |
| f.write(fl + "\n") | |
| print(f"[*] Wrote decrypted transcript to {args.out}") | |
| if flags: | |
| print("[+] Flags found:") | |
| for fl in sorted(set(flags)): | |
| print(" ", fl) | |
| sys.exit(0) | |
| else: | |
| print("[-] No flags found. Decrypted messages:", len(decrypted)) | |
| sys.exit(3) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment