$ 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.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
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; ...
$ grep --similar