All incidents

BSC Callback Auth Drain

Share
Sep 29, 2023 08:46 UTCAttackLoss: 20 BNBPending manual check2 exploit txWindow: 3m 12s
Estimated Impact
20 BNB
Label
Attack
Exploit Tx
2
Addresses
1
Attack Window
3m 12s
Sep 29, 2023 08:46 UTC → Sep 29, 2023 08:49 UTC

Exploit Transactions

TX 1BSC
0x59aec14f53bb3e07f5b6861aa76eeecced9f9390d361b8666097fa8b66af7c45
Sep 29, 2023 08:46 UTCExplorer
TX 2BSC
0xf77c5904da98d3d4a6e651d0846d35545ef5ca0b969132ae81a9c63e1efc2113
Sep 29, 2023 08:49 UTCExplorer

Victim Addresses

0x1f7cf218b46e613d1ba54cac11dc1b5368d94fb7BSC

Loss Breakdown

20BNB

Similar Incidents

Root Cause Analysis

BSC Callback Auth Drain

1. Incident Overview TL;DR

On BNB Chain, transaction 0xf77c5904da98d3d4a6e651d0846d35545ef5ca0b969132ae81a9c63e1efc2113 drained the full 20 BNB balance from unverified contract 0x1f7cF218B46e613D1BA54CaC11dC1b5368d94fb7. The attacking EOA 0x09039e2082a0a815908e68bd52b86f96573768e8 first deployed helper contract 0x0F41f9146dE354e5Ac6Bb3996e2E319Dc8a3Bb7f, then called the helper's go(address) entrypoint to drive the exploit.

The root cause is a callback-based authorization bypass. The victim delegated privilege checks to an external contract and treated any nonzero return value from selector 0xe44a73b7 as authorization. An attacker-deployed helper with a fallback that always returns ABI-encoded 1 can therefore pass the check, rewrite privileged configuration slots, and trigger the victim's native-value transfer path.

2. Key Background

The victim contract is unverified, so the analysis relies on deterministic on-chain artifacts: the exploit transaction metadata, its full execution trace, and the native balance diff. Those artifacts are sufficient because the exploit is simple and fully contained in one attacker-crafted call chain.

Before the exploit transaction, the victim held exactly 20 BNB and storage slots 1 through 4 still pointed to legacy external addresses:

slot 1 -> 0xa025f4e855ac654d8e80a31ce09c7d7df502ea1d
slot 2 -> 0x10ed43c718714eb63d5aa57b78b54704e256024e
slot 3 -> 0x0000000000004946c0e9f43f4dee607b0ef1fa1c
slot 4 -> 0xa2ed9fbd90c518dd1a89ecb2edc497f1de5f6be3

The attacker helper was deployed 64 blocks earlier in transaction 0x59aec14f53bb3e07f5b6861aa76eeecced9f9390d361b8666097fa8b66af7c45. The auditor's analysis and PoC both describe the helper runtime as minimal: an owner-gated go(address) function plus a permissive fallback that returns 1 for arbitrary selectors. That behavior is exactly what the victim's authorization design mistakenly trusted.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK-class ACT opportunity caused by broken authorization, not by a pricing condition or a privileged-key compromise. The victim exposed privileged routines behind selectors 0xe52809f8 (update(address,address,address,address)) and 0x2a011594 (functionCallWithValue(address,bytes,uint256)).

The unsafe design is that the victim did not authenticate privileged callers from internal state. Instead, it built calldata for selector 0xe44a73b7, passed the current caller as an argument, staticcalled an external address loaded from caller-controlled context, and accepted any nonzero 32-byte return value as proof of authorization.

That breaks the intended invariant: only a legitimately authorized administrator or trusted module should be able to rewrite privileged configuration and instruct the contract to move its own assets. Once an attacker can choose the contract that answers the authorization query, the check becomes self-attestation rather than access control. The concrete breakpoint is the victim's authorization routine accepting the helper's 0x...01 return value before executing update(...) and functionCallWithValue(...).

4. Detailed Root Cause Analysis

The exploit path is directly visible in the seed trace. The helper calls the victim's update(...) function with its own address repeated four times. Inside that call, the victim performs the authorization callback to the helper and receives a nonzero return value:

0x1f7cF218...::update(0x0F41f914..., 0x0F41f914..., 0x0F41f914..., 0x0F41f914...)
  0x0F41f914...::e44a73b7(0x0000000000000000000000000f41f9146de354e5ac6bb3996e2e319dc8a3bb7f) [staticcall]
    ← 0x0000000000000000000000000000000000000000000000000000000000000001

After accepting that return value, the victim rewrites storage slots 1 through 4 to the helper address. The trace records the exact slot mutations:

@ 1: 0x...a025f4e855ac654d8e80a31ce09c7d7df502ea1d -> 0x...0f41f9146de354e5ac6bb3996e2e319dc8a3bb7f
@ 2: 0x...10ed43c718714eb63d5aa57b78b54704e256024e -> 0x...0f41f9146de354e5ac6bb3996e2e319dc8a3bb7f
@ 3: 0x...0000000000004946c0e9f43f4dee607b0ef1fa1c -> 0x...0f41f9146de354e5ac6bb3996e2e319dc8a3bb7f
@ 4: 0x...a2ed9fbd90c518dd1a89ecb2edc497f1de5f6be3 -> 0x...0f41f9146de354e5ac6bb3996e2e319dc8a3bb7f

The helper then invokes the victim's value-moving primitive:

0x1f7cF218...::functionCallWithValue(0x0F41f914..., 0x0dbe671f, 20000000000000000000)
  0x0F41f914...::a{value: 20000000000000000000}()
  0x0F41f914...::e44a73b7(...) [staticcall]
    ← 0x...01

That call drains the victim's entire 20 BNB balance to the helper, and the helper immediately forwards the received native currency to the attacker EOA. Because all required ingredients are public and deployable by any unprivileged user, the exploit is permissionless and reproducible from canonical chain state at block 32161325.

5. Adversary Flow Analysis

The end-to-end adversary flow has two transactions.

First, the attacker EOA 0x09039e2082a0a815908e68bd52b86f96573768e8 deploys helper contract 0x0F41f9146dE354e5Ac6Bb3996e2E319Dc8a3Bb7f in block 32161262. This establishes attacker-controlled runtime code that can both answer authorization callbacks with 1 and receive BNB.

Second, in block 32161326, the same EOA sends the exploit transaction to the helper. The helper calls go(0x1f7cF218B46e613D1BA54CaC11dC1b5368d94fb7), which performs three steps in sequence:

  1. Call update(helper, helper, helper, helper) on the victim.
  2. Let the victim authorize that call by staticcalling the helper's always-true callback.
  3. Call functionCallWithValue(helper, 0x0dbe671f, 20 ether) so the victim sends its 20 BNB balance to the helper, then forward that BNB to the attacker EOA.

The trace summary captures the exploit precisely:

0x0F41f914...::go(0x1f7cF218...)
  0x1f7cF218...::update(...)
  0x1f7cF218...::functionCallWithValue(0x0F41f914..., 0x0dbe671f, 20000000000000000000)
  0x09039E20...::fallback{value: 20000000000000000000}()

No private orderflow, governance action, compromised key, or prior privileged role was required. The only attacker-side prerequisite was deploying a helper contract whose callback semantics matched the victim's broken trust model.

6. Impact & Losses

The measurable on-chain loss is exactly 20 BNB from victim contract 0x1f7cF218B46e613D1BA54CaC11dC1b5368d94fb7. The balance-diff artifact records:

{
  "victim_before_wei": "20000000000000000000",
  "victim_after_wei": "0",
  "attacker_delta_wei": "19999795700700000000"
}

The attacker EOA's net gain is 19.9997957007 BNB after transaction gas, while the victim loses its entire native balance. No ERC-20 deltas were involved in the exploit transaction.

7. References

  1. Exploit transaction: 0xf77c5904da98d3d4a6e651d0846d35545ef5ca0b969132ae81a9c63e1efc2113
  2. Helper deployment transaction: 0x59aec14f53bb3e07f5b6861aa76eeecced9f9390d361b8666097fa8b66af7c45
  3. Victim contract: 0x1f7cF218B46e613D1BA54CaC11dC1b5368d94fb7
  4. Attacker EOA: 0x09039e2082a0a815908e68bd52b86f96573768e8
  5. Attacker helper contract: 0x0F41f9146dE354e5Ac6Bb3996e2E319Dc8a3Bb7f
  6. Seed execution trace and storage changes for the exploit transaction
  7. Seed native balance diff confirming 20 BNB loss and attacker profit