Road Closed [Easy] Walkthrough

“We keep out the wrong people – by letting anyone in.”

Road Closed [Easy] Walkthrough

Road Closed is a Challenge created by QuillAudits Team to learn about Access Control Vulnerability in smart contracts/solidity.

The Objective of CTF:

  1. Become the owner of the contract.

  2. Change the value of hacked to true.

You can find the contract on the Goerli Test network.

1. Let's study the Contract of this Challenge First.

here is the Full code:

// 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;
    }
}

Let's Dive into the code...


1.1. The contract Set-Up.

contract RoadClosed {

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

This Solidity contract is called "RoadClosed". The contract has four variables:

  • "hacked" is a boolean variable that can be set to true or false.

  • "owner" is an address variable that stores the Ethereum address of the owner of the contract.

  • "pwner" is an address variable that stores the Ethereum address of a specific user who is allowed to call the "pwn" function.

  • "whitelistedMinters" is a mapping (a type of data structure in Solidity) that maps Ethereum addresses to boolean values. It is used to store a whitelist of addresses that are allowed to perform certain actions on the contract. The boolean value associated with each address indicates whether or not the address is on the whitelist.

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

The "isContract" function takes in an Ethereum address as an input and returns a boolean value indicating whether or not the address corresponds to a contract.

This function uses the "extcodesize" assembly function to get the size of the code stored at the given address. If the size is greater than 0, this means that there is code stored at the address, and therefore it is a contract. If the size is 0, this means that there is no code stored at the address, and therefore it is not a contract.

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

The "isOwner" function returns a boolean value indicating whether or not the caller of the function is the owner of the contract. It does this by checking if the caller's address (stored in the "msg.sender" variable) is equal to the contract's owner address. If they are equal, the function returns true, otherwise, it returns false.

    constructor() {
        owner = msg.sender;
    }

It's a constructor function for the "RoadClosed" Solidity contract. The constructor function is a special function that is called when the contract is first deployed to the Ethereum blockchain. It is used to initialize the contract and set its initial state.

This constructor is setting the contract's "owner" variable to the Ethereum address of the user who deployed the contract. This is done using the "msg.sender" variable, which stores the address of the user who called the function (in this case, the user deploying the contract).

The constructor function does not return any value. It is only called once, when the contract is deployed, and cannot be called again after that.


1.2. Add to whitelist function.

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

The "addToWhitelist" function in this contract allows the owner of the contract to add a given address to the contract's whitelist. The function takes in a single input, an address called "addr", which is the address to be added to the whitelist.

The function has a "require" statement at the beginning that checks if the given address is a contract or not, using the "isContract" function. If the address is a contract, the function will throw an error message saying "Contracts are not allowed". If the address is not a contract, the function will continue executing.

The function then adds the given address to the whitelist by setting the value in the "whitelistedMinters" mapping associated with the address to "true". This means that the address is now on the whitelist and is allowed to perform certain actions on the contract.

This function is marked as "public", which means that it can be called by anyone, not just the owner of the contract. However, only the owner of the contract is allowed to add addresses to the whitelist, because only the owner is allowed to call the "require" statement at the beginning of the function.


1.3. Change owner function.

    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;
    }

The "changeOwner" function allows the owner of the contract to transfer ownership of the contract to a different address. The function takes in a single input, an Ethereum address called "addr", which is the address to be set as the new owner of the contract.

The function has three "require" statements at the beginning that check the following conditions:

  • The given address is on the whitelist. If it is not, the function will throw an error message saying "You are not whitelisted".

  • The caller of the function (stored in the "msg.sender" variable) is the same as the given address. If they are not, the function will throw an error message saying "address must be msg.sender".

  • The given address is not the zero address (0x0). If it is, the function will throw an error message saying "Zero address".

If all of these conditions are met, the function will set the contract's "owner" variable to the given address. This transfer of ownership will take effect immediately.

This function is marked as "public", which means that it can be called by anyone. However, only the owner of the contract is allowed to transfer ownership of the contract, because only the owner is allowed to call the "require" statements at the beginning of the function.


1.4. PWNs functions.

    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;
    }

The "pwn" function in this Solidity contract allows the owner of the contract or a specific user-defined as "pwner" to set the contract's "hacked" boolean variable to true.

There are two versions of the "pwn" function in this contract. The first version takes in a single input, an Ethereum address called "addr", and can only be called by the owner of the contract. It has three "require" statements at the beginning that check the following conditions:

  • The caller of the function (stored in the "msg.sender" variable) is not a contract. If it is, the function will throw an error message saying "Contracts are not allowed".

  • The caller of the function (stored in the "msg.sender" variable) is the same as the given address. If they are not, the function will throw an error message saying "address must be msg.sender".

  • The caller of the function (stored in the "msg.sender" variable) is the owner of the contract. If they are not, the function will throw an error message saying "Must be owner".

If all of these conditions are met, the function will set the contract's "hacked" boolean to true.

The second version of the "pwn" function does not take any inputs and can only be called by the user-defined as "pwner". It has a single "require" statement at the beginning that checks if the caller of the function (stored in the "msg.sender" variable) is the same as the "pwner" variable. If they are not, the function will throw an error. If they are, the function will set the contract's "hacked" boolean to true.

Both versions of the "pwn" function are marked as "external payable", which means that they can be called from external contracts and can receive Ether payments.


1.5. Is Hacked function?

    function isHacked() public view returns(bool) {
        return hacked;
    }

The "isHacked" function allows anyone to check the value of the contract's "hacked" boolean variable.

The function has no inputs and simply returns the value of the "hacked" boolean. It is marked as "public" and "view", which means that it can be called by anyone and it does not modify the contract's state.

The function is also marked as "returns(bool)", which means that it returns a boolean value. In this case, it returns the value of the "hacked" boolean. This function can be used by anyone to check if the contract has been hacked or not.


2. Let's Hack this contract...

After analyzing a RoadClosed contract I found It has an Access Control Vulnerability.., Access control vulnerabilities occur when an application or system does not properly enforce access restrictions. In this specific code, the contract has a mapping of whitelisted addresses that are allowed to perform certain actions, such as changing the owner of the contract or adding addresses to the whitelist.


2.1. The Idea to Attack this Contract.

  1. The addToWhitelist function allows anyone to add an address to the whitelist, without any restrictions. This could potentially allow an attacker to add their own address to the whitelist, and then change the owner of the contract using the changeOwner function.

  2. The changeOwner function allows the caller to specify any address as the new owner, as long as they are already on the whitelist. This means that an attacker could potentially add their own address to the whitelist, and then use the changeOwner function to become the owner of the contract.

  3. The pwn function allows the owner of the contract to set the hacked flag to true. However, this function can also be called by anyone who is able to send a transaction from the pwner address, even if they are not the owner of the contract.

    And by doing this, we will be achieved the Objective of this CTF.


2.2. Attack steps.

1- Call addToWhitelist function to be in whitelistedMinters.

2- Call changeOwner function to be the owner.

3- Call pwn function to Change the value of hacked to true.


2.3. Simulate the Attack.

We are able to simulate this attack in several methods using tools like Foundry/Hardhat/Brownie/Truffle or using Remix IDE online by writing a suitable script for each one to execute the attack for this contract, and there is also another easy way to do this attack by interacting with the contract directly from etherscan.

2.3.1. Using the Foundry framework.

I used the foundry framework to submit my solution to Quill Audit Team as they asked.., "Note: You can create POC using Foundry/Hardhat. Without proper POC, your submissions will not be accepted."

You have to learn how to use foundry, they have an awesome book for guidance, I think if you wanna use it you much go deeper to write your test code and just craft the attack steps to your test code.

    function hack() public {

        // Call the addToWhitelist function to whitelist your attacker address
        roadClosedContract.addToWhitelist(attacker);

        // Call the changeOwner function with the same attacking address to change the owner to the whitelisted address
        roadClosedContract.changeOwner(attacker);

        // Call the pwn function for changing the value of hacked to true
        roadClosedContract.pwn(attacker);
    }

If you are able to create these attack steps:

  • Call addToWhitelist function as an attacker to whitelist your attacker address.

  • Call changeOwner function with the same attacking address to change the owner to the whitelisted address.

  • Call pwn function for changing the value of hacked to true.

In your test code, you will simulate the attack and get ownership of the contract, and then change the value of hacked to true.


2.3.2. Interacting directly with the contract from Etherscan.

I will show you this very easy method of how I will take ownership of the challenge, and then change the value of hacked to true.

1- Go visit the base link of the contract and then go to Write Contract.

2- Be sure that you are connected to MetaMask.

3- Copy your attacking address, and Call (Write) addToWhitelist function as an attacker.

4- Call (Write) changeOwner function with the same attacking address to change the owner to the whitelisted address.

And now, I'm the Owner...

5- Call (Write) pwn function for changing the value of hacked to true.

And go check the isHacked function from Read Contract you will see it's True.


Thanks To Quill-Audit Team for their efforts...