Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[H1] An overflow can be used to drain ETH in VirtualAccount #687

Closed
c4-submissions opened this issue Oct 6, 2023 · 7 comments
Closed

[H1] An overflow can be used to drain ETH in VirtualAccount #687

c4-submissions opened this issue Oct 6, 2023 · 7 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-885 edited-by-warden partial-25 Incomplete articulation of vulnerability; eligible for partial credit only (25%) sufficient quality report This report is of sufficient quality

Comments

@c4-submissions
Copy link
Contributor

c4-submissions commented Oct 6, 2023

Lines of code

https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/VirtualAccount.sol#L86-L112

Vulnerability details

Impact

Loss of funds

Analysis of the vulnerability

There are three vulnerabilities in the payableCall() function that enable attackers to steal user funds.

/// @inheritdoc IVirtualAccount
 function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) {
    uint256 valAccumulator;
    uint256 length = calls.length;
    returnData = new bytes[](length);
    PayableCall calldata _call;
    for (uint256 i = 0; i < length;) {
        _call = calls[i];
        uint256 val = _call.value;
        // Humanity will be a Type V Kardashev Civilization before this overflows - andreas
        // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256
        unchecked {
            valAccumulator += val;
        }
        bool success;
        if (isContract(_call.target)) (success, returnData[i]) = _call.target.call{value: val}(_call.callData);
        if (!success) revert CallFailed();
        unchecked {
            ++i;
        }
    }

    // Finally, make sure the msg.value = SUM(call[0...i].value)
    if (msg.value != valAccumulator) revert CallFailed();
}

First vulnerability. The payableCall is unprotected

function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) {

​ This vulnerability enables calling external contracts on behalf of Virtual Account.

This can be use to steal ERC20 tokens for example.

Second vulnerability An overflow can be exploited .

The _call.value are sent as a parameter, so it is possible to overflow this.

uint256 val = _call.value;
    // Humanity will be a Type V Kardashev Civilization before this overflows - andreas
    // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256
    unchecked {
        valAccumulator += val;
    }

Third vulnerability EOA are ignored in the batch call and the succeed depends on the previous call.

If you send an EOA in the first call, the transaction will revert, but in the second call, it will succeed.

 bool success;
    if (isContract(_call.target)) (success, returnData[i]) =     _call.target.call{value: val}(_call.callData);        if (!success) revert CallFailed();

Proof of Concept

The attacker will construct the following malicious batch call

uint balanceToSteal =  address(VirtualAccount).balance;  // We will steal all the ETH
address attackerContract = '0x...'

//This call is just to set the succeed variable to TRUE for the next call
PayableCall p1 =  new PayableCall(attackerContract,"", 0)      

// This call is just ignored but helps to overflow
PayableCall p2 = new PayableCall(anyEOA,bytes(0),MAX_UINT-balanceToSteal+1)   

// Here we drain the contract
PayableCall p3 = new   PayableCall(attackerContract,"",balanceToSteal)   /// This call 

PayableCalls[] attack = [p1,p2,p3];

The attacker will call `payableCall(attack)` without any ether.

The transaction will succeed

Recommended

​ You need to fix this

  1. Add a modifier requiresApprovedCaller() to protect payableCall()

  2. Set succeed to false if the call is an EOA

 if (isContract(call.target)) 
      (success, returnData[i]) =     _call.target.call{value: val}(call.callData);  
 else
 	success = false;

Assessed type

Access Control

@c4-submissions c4-submissions added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Oct 6, 2023
c4-submissions added a commit that referenced this issue Oct 6, 2023
@code4rena-admin code4rena-admin changed the title ## [H1] An overflow can be used to drain ETH in VirtualAccount [H1] An overflow can be used to drain ETH in VirtualAccount Oct 6, 2023
@c4-pre-sort
Copy link

0xA5DF marked the issue as duplicate of #888

@c4-pre-sort c4-pre-sort added the sufficient quality report This report is of sufficient quality label Oct 9, 2023
@c4-pre-sort
Copy link

0xA5DF marked the issue as sufficient quality report

@alcueca
Copy link

alcueca commented Oct 26, 2023

One of those rare cases where an AI report that was not checked actually is right.

@c4-judge c4-judge added the partial-25 Incomplete articulation of vulnerability; eligible for partial credit only (25%) label Oct 26, 2023
@c4-judge
Copy link
Contributor

alcueca marked the issue as partial-25

@peritoflores
Copy link

Hi, guys...... I know you are working hard and you marked this as dup. However, in this submission I am explaining how to drain ETH which is not trivial. The submission you marked as principal only shows how to steal tokens. To drain ETH you need to exploit a vulnerability in the function that is already there.

@stalinMacias
Copy link

stalinMacias commented Nov 9, 2023

I'll try to help you a little bit here, I think you miss a key point.

The moment the batch call either fails the external call or the target destination is not a contract, the whole tx will be reverted.

From your report:

If you send an EOA in the first call, the transaction will revert, but in the second call, it will succeed.

You are already acknowledging that the first call will revert the tx, so, if the tx is reverted, the execution is halted and all the changes are wiped out, the "attacker" would basically be wasting gas

the succeed depends on the previous call.

No, it does not depend on the previous call, if you take a closer look at the code, before each cal, the success bool variable is declared, so, it's default value is false, it doesn't matter if the previous call was true, on each new call the variable is defined, and as such, it's value is reset to the default, which is false

I hope this makes sense for you and helps you to understand what did you miss 🫡

@peritoflores
Copy link

peritoflores commented Nov 10, 2023

Hi @stalinMacias ... Sorry but isContract is where you introduced the bug.... you will pass to the next call....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-885 edited-by-warden partial-25 Incomplete articulation of vulnerability; eligible for partial credit only (25%) sufficient quality report This report is of sufficient quality
Projects
None yet
Development

No branches or pull requests

7 participants