Executor Missing Access Control USDC/USDT Drain
2026-4-26 23:59:37 Author: www.darknavy.org(查看原文) 阅读量:0 收藏

On Ethereum mainnet, transaction 0x81f9aeaa697e4a389e7ce442a357e162ada24049c27cb1439f69d2f4fee720f1 succeeded at block 24971842 on 2026-04-27T14:01:11Z. The attacker deployed helper contract 0x2196b3f31a43de49a2951c514488a8dd7c96ad67 and used it to call execute(uint256,address,uint256,bytes) on executor account 0xc851e5a046819b022091b50f05ae3bd052e034a4. Because that executor path accepted an arbitrary caller and arbitrary target calldata, the executor called two funds accounts as a trusted caller and caused them to transfer 224,865 USDC and 183,453.18 USDT. The total on-chain stablecoin loss was approximately $408,318.18, before the attacker’s small gas cost.

Root Cause

Vulnerable Contract

  • Vulnerable executor account: 0xc851e5a046819b022091b50f05ae3bd052e034a4.
  • Proxy status: proxy-like account; the trace shows calls to 0xc851... immediately DELEGATECALL to shared implementation 0x039152a960af4500c0d8a516fe78dbc7a9adf5e5.
  • Execute module: selector 0x44c028fe is routed by module lookup selector 0x5c23bdf5 to 0xb1a4010d6dbe4f72bf82ae5de6a0c714eb1a02c6.
  • Source status: the critical target contracts were not verified. The user-provided Dedaub reconstruction strengthens the root-cause analysis and is preserved as 0xb1a4010d6dbe4f72bf82ae5de6a0c714eb1a02c6/dedaub_recovered_excerpt.sol; the call trace and receipt logs remain the authoritative evidence.

Vulnerable Function

  • Function: execute(uint256,address,uint256,bytes).
  • Selector: 0x44c028fe (cast sig 'execute(uint256,address,uint256,bytes)').
  • Entry observed in trace:
    • Path .2: 0x2196... -> 0xc851..., selector 0x44c028fe, target argument 0x34be478993b60561c7c9f3b8a3851e9a3a15cd53.
    • Path .3: 0x2196... -> 0xc851..., selector 0x44c028fe, target argument 0x2a69893ec6d332101750eed731d52891717af671.

Vulnerable Code

// [Dedaub recovered - approximation]
function _exec(uint256 operation, address to, uint256 value, bytes memory data)
    internal
    returns (bytes memory)
{
    bytes4 sel = data.length >= 4 ? bytes4(data) : bytes4(0);
    if (operation == 0) {
        if (address(this).balance < value) revert InsufficientBalance(address(this).balance, value);
        emit Executed(0, to, sel, value);
        (bool ok, bytes memory ret) = to.call{value: value}(data); // <-- VULNERABILITY
        if (!ok) revert FailedCall();
        return ret;
    }
    // operation 1/2 create, operation 3 staticcall, operation 4 delegatecall omitted for brevity
}

function execute(Message calldata message) external {
    _checkAuth(4);
    // guarded message execution path
}

function b9b8bc50(address token, address to, uint256 value) external nonReentrant {
    _checkAuth(4);
    _safeTransfer(IERC20(token), to, value);
}

function execute(uint256 _operation, address _to, uint256 _value, bytes calldata _data)
    external
    payable
    returns (bytes memory)
{
    return _exec(_operation, _to, _value, _data); // <-- VULNERABILITY: no _checkAuth(4)
}

function executeBatch(
    uint256[] calldata operations,
    address[] calldata tos,
    uint256[] calldata values,
    bytes[] calldata datas
) external payable returns (bytes[] memory results) {
    // also reaches _exec for each item without _checkAuth(4)
}

The important refinement is the contrast inside the recovered code: the contract has _checkAuth(4) and uses it on other sensitive paths, but the overloaded execute(uint256,address,uint256,bytes) directly returns _exec(...) without any authorization check. The behavior represented by _exec(operation == 0) is directly proven by trace paths .2.0.1.0 and .3.0.1.0, where 0xc851... makes external CALLs to attacker-chosen target accounts with attacker-supplied 0xaaa18f86 payloads.

Why It’s Vulnerable

Expected behavior: every external path into _exec should authenticate msg.sender as the account owner/operator, or otherwise enforce a strict target/function allowlist before it performs arbitrary external calls. The recovered code already shows that design intent on neighboring functions: execute(Message calldata), b9b8bc50(...), e0ac316e(...), _3df9c3ea(...), and _8c6dd35e(...) all call _checkAuth(4) before privileged behavior.

Actual behavior: the attacker-controlled helper 0x2196... called the unguarded overload on 0xc851... directly. The recovered code shows execute(uint256,address,uint256,bytes) simply returns _exec(_operation, _to, _value, _data) with no _checkAuth(4), and _exec(operation == 0) performs to.call{value: value}(data). The executor accepted operationType = 0, target = 0x34be.../0x2a69..., value = 0, and a dynamic bytes payload beginning with unresolved funds-action selector 0xaaa18f86, then made the requested external call from 0xc851....

This matters because the funds accounts were holding the token balances and then executed token transfers when called by 0xc851.... The downstream funds-account action path appears to rely on the caller context (the trace shows role-provider and hasRole checks during that path), so direct attacker calls would not have had the same trust context. By abusing the unguarded executor overload, the attacker used 0xc851... as the authority-bearing caller and caused 0x34be... to transfer all of its USDC and 0x2a69... to transfer all of its USDT.

Normal flow should be: authorized owner/operator -> executor -> approved target/action. The attack flow was: arbitrary helper -> executor -> funds account action -> ERC-20 transfer to helper.

Attack Execution

High-Level Flow

  1. The attacker submitted a contract-creation transaction and deployed helper 0x2196....
  2. The helper checked that the attacker EOA initially held zero USDC and zero USDT.
  3. The helper called the exposed executor twice with crafted execute(...) calldata.
  4. The first executor call targeted the USDC funds account and caused it to transfer its USDC balance to the helper.
  5. The second executor call targeted the USDT funds account and caused it to transfer its USDT balance to the helper.
  6. The helper forwarded the received USDC and USDT to the attacker EOA.

Detailed Call Trace

  • Root: CREATE from attacker EOA 0xdb2096ffceef50106c4457b12fc139d89d179cce to helper 0x2196b3f31a43de49a2951c514488a8dd7c96ad67.
  • Path .0: helper STATICCALL USDC balanceOf(address) (0x70a08231) for the attacker EOA; output 0.
  • Path .1: helper STATICCALL USDT balanceOf(address) (0x70a08231) for the attacker EOA; output 0.
  • USDC drain leg:
    • Path .2: helper -> executor 0xc851..., CALL, execute(uint256,address,uint256,bytes) (0x44c028fe). Decoded arguments: operationType = 0, target = 0x34be478993b60561c7c9f3b8a3851e9a3a15cd53, value = 0, payload selector 0xaaa18f86.
    • Path .2.0: executor -> shared implementation 0x039152..., DELEGATECALL, same selector.
    • Path .2.0.0: executor -> module registry 0xc6f93..., STATICCALL 0x5c23bdf5; output resolves the selector/module key to 0xb1a4010d6dbe4f72bf82ae5de6a0c714eb1a02c6.
    • Path .2.0.1: executor -> execute module 0xb1a401..., DELEGATECALL, 0x44c028fe.
    • Path .2.0.1.0: executor -> USDC funds account 0x34be..., CALL, payload selector 0xaaa18f86.
    • Path .2.0.1.0.0.1.10: USDC funds account -> USDC token 0xa0b8..., CALL, transfer(address,uint256) (0xa9059cbb) to helper for raw amount 224865000000 (224,865 USDC).
  • USDT drain leg:
    • Path .3: helper -> executor 0xc851..., CALL, execute(uint256,address,uint256,bytes) (0x44c028fe). Decoded arguments: operationType = 0, target = 0x2a69893ec6d332101750eed731d52891717af671, value = 0, payload selector 0xaaa18f86.
    • Path .3.0: executor -> shared implementation 0x039152..., DELEGATECALL, same selector.
    • Path .3.0.1: executor -> execute module 0xb1a401..., DELEGATECALL, 0x44c028fe.
    • Path .3.0.1.0: executor -> USDT funds account 0x2a69..., CALL, payload selector 0xaaa18f86.
    • Path .3.0.1.0.0.1.10: USDT funds account -> USDT token 0xdac17f..., CALL, transfer(address,uint256) (0xa9059cbb) to helper for raw amount 183453180000 (183,453.18 USDT).
  • Final forwarding:
    • Path .7: helper -> USDC token, transfer(attacker EOA, 224865000000).
    • Path .9: helper -> USDT token, transfer(attacker EOA, 183453180000).

Financial Impact

funds_flow.json records four ERC-20 transfers and the final attacker gains:

TokenSource of lossHelper receiptFinal attacker receiptHuman amount
USDC0x34be478993b60561c7c9f3b8a3851e9a3a15cd53log 294log 297224,865
USDT0x2a69893ec6d332101750eed731d52891717af671log 296log 298183,453.18

The attacker EOA’s net gain was 224,865 USDC plus 183,453.18 USDT, or approximately $408,318.18 at stablecoin face value. The receipt used 1,032,631 gas at an effective gas price of 2.294767559 gwei, for about 0.002369648119217729 ETH in gas cost. No flash loan, AMM swap, or oracle manipulation appears in this transaction; the loss comes from direct token transfers out of the two funds accounts.

Evidence

  • Transaction metadata: tx.to is null, the receipt contractAddress is 0x2196b3f31a43de49a2951c514488a8dd7c96ad67, and receipt status is 0x1.
  • Executor event topic: Executed(uint256,address,uint256,bytes4) hashes to 0x4810874456b8e6487bd861375cf6abd8e1c8bb5858c8ce36a86a04dabfac199e.
  • Receipt log 293: 0xc851... emitted Executed with operationType = 0, target = 0x34be..., value = 0, selector 0xaaa18f86.
  • Receipt log 295: 0xc851... emitted Executed with operationType = 0, target = 0x2a69..., value = 0, selector 0xaaa18f86.
  • Transfer topic: Transfer(address,address,uint256) hashes to 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.
  • Receipt log 294: USDC transfer from 0x34be... to helper 0x2196..., amount 224865000000.
  • Receipt log 296: USDT transfer from 0x2a69... to helper 0x2196..., amount 183453180000.
  • Receipt logs 297 and 298: helper forwarded the same USDC and USDT amounts to attacker EOA 0xdb2096....
  • Selector verification: execute(uint256,address,uint256,bytes) = 0x44c028fe, transfer(address,uint256) = 0xa9059cbb, balanceOf(address) = 0x70a08231, and hasRole(bytes32,address) = 0x91d14854.
  • Source caveat: 0xaaa18f86 remains unresolved because the funds-account action module source was not verified. Its effect is nevertheless clear from the trace: it led to ERC-20 transfer calls from the funds accounts to the attacker helper.
  • Recovered-code caveat: the user-provided Dedaub reconstruction is useful for the missing _checkAuth(4) on the vulnerable overload, but its recovered Executed event declaration/order does not match receipt topic 0x48108744...; receipt logs remain authoritative for event ABI/order.

文章来源: https://www.darknavy.org/web3/exploits/executor-missing-access-control-usdc-usdt-drain/
如有侵权请联系:admin#unsafe.sh