sigbreak


my solutions to ethernaut

2022-06-16

A few weeks ago, I came across a blog post on how to get into smart contract auditing on Twitter. I've had a fair share of experience with cryptocurrencies, from daytrading Ethereum while it was still less than a dollar to mining it in my college dorm. I had experienced enough emotional stress from doing both to warrant a healthy aversion to the topic, and even moreso considering the current state of "Web3." After reading this article, it convinced me enough to try and learn how to contribute to helping to fix the mess from which I had profited before.

I had a bit of difficulty in trying to set up Damn Vulnerable DeFi, so I quickly found a different set of challenges that seemed easier to get started with, Ethernaut.

In this post, I'd like to document my progress in learning about the EVM, Solidity, and discovering security vulnerabilities in smart contracts.

0. hello ethernaut (solved 06-15-2022)

This problem served as a setup guide and introduction to interfacing with an Ethereum smart contract. I didn't have much difficulty in the setup, aside from needing to setup the Rinkeby testnet through this website.

In this challenge, we're given some helpful guidance on interacting with our environment through a series of function calls on our contract instance.

Eventually, I end up awaiting on contract.authenticate('ethernaut0') in order to clear the challenge.

1. fallback (solved 06-15-2022)

This problem has us review a vulnerable contract in which we must take ownership and drain the account funds. We're given the fact that sending ether with and without the contract ABI, converting ether to wei, and the concept of fallback functions will be useful in achieving our goals.

The concept of a contract feels analogous to that of an object in other languages, such as Java or Python, without thinking too deeply on the relational details. It can have state, which is stored on the blockchain, a constructor to initialize default values, and functions that can operate on arguments and the contract's state. What sticks out to me is the use of require() statements, which seem to be analogous to assertions in other languages. I quickly found out that this is to prevent erronous operations from being minted to the blockchain.

There are a few statements in the code that describe how we can snatch this contract:

function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
        owner = msg.sender;
    }
}

The above code lets us take ownership if our contributions to the contract's funds are greater than that of the owner's. However, the constructor sets the owner's contributions to something far greater than what is reasonable in short time:

constructor() public {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
}

We would need to contribute more than 1,000 ether to reclaim the contract. With a require() preventing us from giving more than 0.001 ether per transaction, we would likely be stuck on this problem for a long time.

Another snippet of code shows us an alternative route:

receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
}

This receive() function is a fallback function, which gets called whenever a transaction is sent to the contract with some ether, but no specific function to call. The require() asserts that it would only succeed if we send some ether and have already made previous contributions.

We can simply solve this by contributing a small amount under the 0.001 ether threshold via contribute(), then by sending any amount directly to the contract.

Using the browser console, the following commands let me reclaim ownership and drain the funds from the contract:

await contract.contribute({value: toWei("0.0001")})
sendTransaction({from: player, to: contract.address, value: toWei("0.0001")})
await contract.owner() // to verify ownership
await contract.withdraw()

When using Ethernaut's sendTransaction() function, the MetaMask wallet defaulted to a much lower gas than what's used in calling the contract functions, so I had to set these values to 3 wei.

2. fallout (solved 06-16-2022)

One thing that stuck out to me nearly immediately was the misspelling of this function:

/* constructor */
function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
}

The letter l looks deviously similar to the number 1, and the commented hint tells us that this function is intended to be the constructor for the contract. The last contract had used a specific constructor() keyword, so I'm guessing this typo is the fault.

One thing I found with a bit of research is that the data field in a transaction can encode a specific function to be called with certain arguments. Knowing this, we can directly call the Fal1out() function by encoding it and passing it along with a transaction. An online tool can be used to encode a function, but it can also be done programmatically.

Some code that'll solve this problem:

let _call = web3.eth.abi.encodeFunctionCall({
                name: 'Fal1out',
                type: 'function',
                inputs: []
            }, [])
sendTransaction({from: player, to: contract.address, value: 0, data: _call})