跳到主要内容

Level 0 - Hello Ethernaut

来源:ethernaut.openzeppelin.com/level/0

Hello Ethernaut 是一道入门关,但它已经在教接下来全程都用得上的一个习惯: 别再相信 UI,打开控制台,看清楚合约真正暴露出了什么。

这一关算不上真正意义上的利用挑战。它更像是 Ethernaut 在教你 —— 它希望你怎么玩这套东西。

准备环境

如果页面以只读模式打开,说明 Ethernaut 还没看到你的钱包。处理方式很直接: 装一个 MetaMask,把它连上,切到 Sepolia 网络,然后给账户充一点测试 ETH。

这里不打算展开钱包的完整教程。这一关需要确保的,只有以下几点:

  • MetaMask 已安装并已解锁
  • 网络已经切到 Sepolia
  • 账户里有足够支付 gas 的测试 ETH

切到 Sepolia 的顺序是: 先把"显示测试网络"打开,再从钱包主界面切换网络。

打开 MetaMask 设置

MetaMask 管理网络

打开 MetaMask 网络选择器

在 MetaMask 中选中 Sepolia

同一个钱包地址在不同网络上保持一致

第一次接触时容易踩到的一个小点: 你的地址在以太坊主网和 Sepolia 上一般是同一个。变的是网络状态,而不是地址本身。每个网络的余额、交易、合约都是各自独立的。

Faucet 备注

大多数 Sepolia 水龙头平时都能用,直到突然不能用。最常见的麻烦来自反滥用策略: 有的水龙头拒绝全新钱包,有的要求登录账号,还有的会检查你主网上是否有一点点 ETH。

实操上的应对很简单:

  • 先试一个正常的水龙头
  • 如果被拒,换一个备选
  • 如果钱包是全新的,基于浏览器的 Sepolia PoW 水龙头 通常是阻力最小的路径

可用资源:

控制台才是真正的操作界面

钱包接上以后,把浏览器控制台打开。从这一刻起,Ethernaut 不再是一个网页,而像一个合约游乐场。

你应该立刻能看到关卡横幅、提示文字,以及当前关卡的合约地址。

Ethernaut 控制台总览

第一个值得敲一下的辅助是 player:

player

它会返回当前连接账户的地址。

Ethernaut player 地址输出

接着确认账户里确实有 gas:

await getBalance(player)

Ethernaut getBalance 输出

最后问问 Ethernaut 都提供了哪些辅助方法:

help()

Ethernaut help 输出

这一条命令几乎把这个平台的工作面都告诉你了: player 地址、当前关卡地址、当前实例、合约包装器,以及几个顺手的工具方法。

真正重要的两个对象

到这一步,真正值得关心的对象只有两个。

ethernaut

它是全局的游戏合约包装器,而不是当前关卡的合约实例。它负责管理所有关卡以及通关校验。

ethernaut
await ethernaut.owner()

Ethernaut owner 调用

这一步主要起到熟悉作用。重点是让自己习惯一件事: 这些"游戏对象"就是普通的合约包装器,而不是某种只存在于 UI 里的、有魔法的抽象。

contract

它是你请求出来的当前关卡实例。真正用来解题的,是这个对象。

通关步骤

1. 连接站点

先在 MetaMask 里通过 Ethernaut 发起的钱包连接请求。

Ethernaut 钱包连接请求

如果 MetaMask 询问网络权限,通过 Sepolia 这一项。

Ethernaut 网络权限请求

2. 请求一个关卡实例

点击 Get New Instance,在 MetaMask 里通过那笔交易。部署完成后,Ethernaut 会把实例地址打印到控制台。

Ethernaut 请求实例输出

3. 顺着合约留的面包屑走

这一关本质上是一次贯穿合约接口的引导式游览。合约会一路告诉你下一步该去哪儿,你要做的只是注意听它说什么。

整条交互链是:

await contract.info()
await contract.info1()
await contract.info2('hello')
await contract.infoNum()
await contract.info42()
await contract.theMethodName()
await contract.method7123949()
await contract.password()
await contract.authenticate('ethernaut0')

Ethernaut 合约 info 链

这里没有什么微妙之处。Ethernaut 是在教你: 读合约,而不是盯着按钮猜。

4. 提交已通关的实例

交互链跑完之后,点 Submit Instance,在 MetaMask 里通过这笔交易。

Ethernaut 实例操作按钮

Ethernaut 在 MetaMask 中提交交易请求

如果一切正常,Ethernaut 会把这一关标记为已通过。

Ethernaut 通关输出

看完源码之后,有些事就很明显了

通关之后,Ethernaut 会把源码公开出来。这才是这一关真正值得回味的地方。

下面是同一份合约,加上了内联注解:

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

contract Instance {
// 声明为 public 意味着 Solidity 会自动生成一个 password() 取值方法。
// 换句话说,这个"秘密"任何人都可以读到。
string public password;

// 这是一条面包屑。值 42 是为了把玩家引向 info42()。
uint8 public infoNum = 42;

// 又一条面包屑。因为它是 public 的,玩家其实也可以直接把它读出来。
string public theMethodName = "The method name is method7123949.";

// 这才是这一关真正的通关状态。
bool private cleared = false;

// 构造函数
// 关卡实例在部署时被赋予一个初始 password。
constructor(string memory _password) {
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.';
}

// 玩家第一次需要传入参数的地方。
// 只有传入 "hello",才能拿到下一条线索。
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.";
}

// infoNum 指向这里。
function info42() public pure returns (string memory) {
return "theMethodName is the name of the next method.";
}

// theMethodName 指向这里。
function method7123949() public pure returns (string memory) {
return "If you know the password,submit it to authenticate().";
}

// 如果传入的值与 password 一致,这一关就算通过。
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;
}
}

整个流程被刻意设计得很简单。一个函数指向下一个,直到合约最终告诉你: 读出 password,然后传进 authenticate()

真正的关键就在这一行:

string public password;

整道题的精髓就在这里。合约把 password 写得像一个秘密,但只要它被声明成 public,Solidity 就会自动暴露出一个 getter。它不是隐藏的,也不是被保护的,更不是任何意义上的 private —— 你可以直接读,然后把它喂回 authenticate()

这也是为什么这道看似简单的题依然值得认真做: 它逼你去注意,UI 里描述的样子,跟合约在链上真正暴露的内容,是两回事。

这一关真正想教的是什么

最显而易见的那句话当然是"public 状态本来就是公开的"。但底下其实还有一层更宽的意思:

  • 把合约接口当成事实来源
  • 不要因为某个变量"听起来像个秘密"就以为它真的是秘密
  • 仔细读输出 —— 合约经常把下一步的提示直接摆在你面前
  • 区分清楚: 什么是前端层隐藏的,什么是链上真正隐藏的

这是真正的智能合约审计习惯,不是游戏化的技巧。

这一关为什么仍然重要

题本身刻意简单,但它教的工作流,恰恰是后面每一关都会反复出现的:

  1. 接钱包
  2. 切到对的网络
  3. 给账户充值
  4. 摸清楚控制台里的辅助方法
  5. 读懂公开接口
  6. 创建实例
  7. 与合约交互
  8. 提交结果

一旦这套循环开始变得自然,后面的关卡也就不再那么生疏。

通关标准

只要做到下面这些,这一关就算过了:

  • 钱包成功连接
  • 切到 Sepolia,并有足够的 gas
  • 成功创建关卡实例
  • 顺着合约公开的提示链走完一遍
  • 提交已解的实例

关键收获

  • 这是一道入门关,而不是漏洞利用题。
  • 浏览器控制台是这场游戏的一部分,不是可选项。
  • public 变量并不是秘密。
  • 读合约,通常比盯着 UI 猜要划算得多。
  • 把钱包、网络与水龙头一次配好,后面省下的时间会非常可观。

参考链接