Mimo August 2022 contest - peritoflores's results

Bridging the chasm between the DeFi world and the world of regulated financial institutions.

General Information

Platform: Code4rena

Start Date: 02/08/2022

Pot Size: $50,000 USDC

Total HM: 12

Participants: 69

Period: 5 days

Judge: gzeon

Total Solo HM: 5

Id: 150

League: ETH

Mimo DeFi

Findings Distribution

Researcher Performance

Rank: 15/69

Findings: 2

Award: $840.41

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: horsefacts

Also found by: ayeslick, cccz, peritoflores, teddav, vlad_bochok

Labels

bug
duplicate
2 (Med Risk)
old-submission-method

Awards

224.5009 USDC - $224.50

External Links

Lines of code

https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxy.sol#L29-L33 https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxy.sol#L4

Vulnerability details

Impact

PBR proxy owner change protection can bypassed / DoS

PoC

PRBProxy has a protection to prevent malicious delegatecall to overwrite owner.

function execute(address target, bytes calldata data) public payable override returns (bytes memory response) { ... ... // Check that the owner has not been changed. if (owner_ != owner) { @audit owner change protection revert CustomErrors.OWNER_CHANGED(owner_, owner); } ... }

This is an important protection because delegatecall has the option to set any variable in the contract including the owner.

However, you have made some changes to PRB Proxy adding more storage variables (by inheriting Initializable) that are vulnerable to be changed via a malicious call.

abstract contract Initializable { /** * @dev Indicates that the contract has been initialized. * @custom:oz-retyped-from bool */ uint8 private _initialized; /;

that means that _initialized is part of the storage of MIMO proxy.

So the malicious contract called by execute can

  1. Overwrite _initalized and set to zero in the delegatecall (owner still is the same so execute will finish correctly).
  2. Call initialize() and set a new owner.

I believe that it is better to use as little storage variables as possible. Ideally ZERO.

The solution I have been thinking is

  1. Remove Initializable contract

  2. Remove initialize function

  3. Create a constructor to set owner and mark it as immutable.

  4. Set minGasReserve as constant (I see no point in this variable, worse that is storage, and I believe that here the vulnerability is from PRB proxy). minGasReserve can be used to DoS if set too high because of an underflow ( uint256 stipend = gasleft() - minGasReserve; )

    Maybe you want to come with a different solution. For example, check that at the end of execute the contract is still initialized.

    However, I have been thinking how to fix this and my proposal solves another bug related to multicall that I will be reporting next.

#0 - horsefacts

2022-08-08T22:20:44Z

#1 - RnkSngh

2022-08-10T10:29:44Z

We're picking #161 as the original since that issue was well written, provided an example, and had the correct risk assessment

Findings Information

🌟 Selected for report: peritoflores

Also found by: 8olidity, vlad_bochok

Labels

bug
2 (Med Risk)
disagree with severity
old-submission-method

Awards

615.9148 USDC - $615.91

External Links

Lines of code

https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/libraries/BoringBatchable.sol#L36

Vulnerability details

Impact

msg.value in a loop can be used to drain proxy funds

PoC

While BoringBatchable is out of the scope, this bug affects seriously MIMOProxy as it inherits.

Some time ago I read a report about an auditor called samczsung (https://samczsun.com/two-rights-might-make-a-wrong/). I believe that you are having the same problem here.

I will try to explain it as brief as possible but I can add a PoC in QA stage if required.

First step: Draining ETH

This vulnerability comes from the fact that msg.value and msg.sender are persisted in delegatecall.

It is possible to call execute() (which is payable ) from batch() (which is also payable ) because both are public functions. (For now ignore the fact that execute() has access control).

The attacker would call batch() sending, for example, 1 ETH with an array of 100 equal items that call execute()

This execute() will call and external contract 100 times and in every time it will send 1ETH from proxy funds (not from the attacker).

If the receiving contract stores these value then the proxy wallet will be drained.

Second step: Access control bypass scenario

While this is already a high risk and there should be many attacking scenarios I would like to show you a pretty simple one.

Suppose the owner would like to grant access to a target with a normal function (maybe no even payable).

For example suppose that the owner grant access to the function

function goodFunction() public

This function has the selector 0x0d092393 . However, for some reason. the owner mistyped the selector and grant access to non existing function 0x0d09392.

Then if the target contract has the so common function.

fallback() external payable { }

Then the attacker can drain wallet funds using this selector as I explained above.

The solution is pretty straightforward.

Remove payable from batch() in BoringBatchable.

#0 - horsefacts

2022-08-08T22:34:05Z

Agree this is possible. I would note that there is a big warning at the top of BoringBatchable that links this very blog post.

#1 - RayXpub

2022-08-10T10:32:21Z

We are not modifying any state in the MimoProxy based on msg.value, so this doesn't apply here. Please refer to our test case here

#2 - peritoflores

2022-08-11T14:56:46Z

Hi @RayXpub . I found that there is an error in the test case that you mentioned. The test is passing because the contract has no ETH and you call batch with false parameter.

The second delegatecall is reverting. However, by design delegatecall will not revert the main transaction and instead will return false that is ignored in this case.

To show you this I have created a PoC with a few modification to your original test. I just send ETH before an then compared that the amount deposited was double.

it("PoC: should be able to reuse msg.value for multiple deposits", async () => { const { mimoProxy, vaultActions, vaultsDataProvider, wmatic } = await setup(); //Send ETH to the proxy RECEIVE EXTERNAL PAYABLE const [owner] = await ethers.getSigners(); owner.sendTransaction({ to: mimoProxy.address, value: DEPOSIT_AMOUNT }); await mimoProxy.execute(vaultActions.address, vaultActions.interface.encodeFunctionData("depositETH"), { value: DEPOSIT_AMOUNT, }); const vaultIdBefore = await vaultsDataProvider.vaultId(wmatic.address, mimoProxy.address); const vaultBalanceBefore = await vaultsDataProvider.vaultCollateralBalance(vaultIdBefore); const data = vaultActions.interface.encodeFunctionData("depositETH"); mimoProxy.batch( [ mimoProxy.interface.encodeFunctionData("execute", [vaultActions.address, data]), mimoProxy.interface.encodeFunctionData("execute", [vaultActions.address, data]), ], true, { value: DEPOSIT_AMOUNT }, ); const vaultId = await vaultsDataProvider.vaultId(wmatic.address, mimoProxy.address); const vaultBalanceAfter = await vaultsDataProvider.vaultCollateralBalance(vaultId); expect(vaultBalanceAfter).to.be.equal(vaultBalanceBefore.add(DEPOSIT_AMOUNT).add(DEPOSIT_AMOUNT));} );

#3 - RayXpub

2022-08-12T13:41:31Z

Hi @peritoflores ,

Thanks for providing a PoC. It seems we misunderstood the issue as we were looking at it in the context of the miso platform vulnerability described in the paradigm article where it is our understanding that the issue was a msg.value reliant state update. Here ETH are actually transferred in each call. However, for an attacker to be able to call execute() he would need to have been granted permission, so that would rely an approval made by the MIMOProxy owner.

In the case of the fallback function this would require the owner making a mistake while granting permission by entering an erroneous selector and the target contract would need to have a fallback.

As we do not see any scenario where this issue would work without a user mistake we consider that this should be labeled as medium risk. But we are considering making all the MIMOProxy functions non payable, this is still being discussed.

Btw the PoC provided is missing an await on the owner.sendTransaction line which ends up not really showcasing the issue but we did manage to reproduce the scenario.

#4 - gzeoneth

2022-08-21T14:46:50Z

Agree this is not High Risk due to the requirement of owner privilege.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter