Introduction

Context Explanation

Numberizer is a seemingly simple web-based CTF challenge, where users are required to submit five numbers in an HTML form such that the sum of all the numbers is negative. However, the challenge implements input validation and sanitization mechanisms to prevent trivial solutions like using negative numbers.

Our goal is to bypass these controls and successfully achieve the required negative sum to retrieve the flag.

Numberizer Home


Solution

Analyzing the Code and Validation Mechanisms

The provided PHP source code implements a number of validation checks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
ini_set("error_reporting", 0);

if(isset($_GET['source'])) {
    highlight_file(__FILE__);
}

include "flag.php";

$MAX_NUMS = 5;

if(isset($_POST['numbers']) && is_array($_POST['numbers'])) {

    $numbers = array();
    $sum = 0;
    for($i = 0; $i < $MAX_NUMS; $i++) {
        if(!isset($_POST['numbers'][$i]) || strlen($_POST['numbers'][$i])>4 || !is_numeric($_POST['numbers'][$i])) {
            continue;
        }
        $the_number = intval($_POST['numbers'][$i]);
        if($the_number < 0) {
            continue;
        }
        $numbers[] = $the_number;
    }
    $sum = intval(array_sum($numbers));


    if($sum < 0) {
        echo "You win a flag: $FLAG";
    } else {
        echo "You win nothing with number $sum ! :-(";
    }
}
?>

<html>
    <head>
        <title>Numberizer</title>
    </head>
    <body>
        <h1>Numberizer</h1>
        <form action="/" method="post">
            <label for="numbers">Give me at most 10 numbers to sum!</label><br>
            <?php
            for($i = 0; $i < $MAX_NUMS; $i++) {
                echo '<input type="text" name="numbers[]"><br>';
            }
            ?>
            <button type="submit">Submit</button>
        </form>
        <p>To view the source code, <a href="/?source">click here.</a>
    </body>
</html>

The key authentication logic imposes validation rules through several mechanisms:


Input Validation Rules:

  1. Input must exist:
    Each user-provided field is confirmed to have been submitted using isset($_POST['numbers'][$i]).

  2. Input character length limit (≤4):
    The input must not exceed 4 characters in length, as verified by the strlen($_POST['numbers'][$i]) check.

  3. Input must be numeric:
    Inputs must pass is_numeric(), ensuring they are numbers or valid numeric representations.

  4. Negative numbers are rejected:
    Even if a negative number is submitted and passes the above checks, the condition if ($the_number < 0) will exclude it from further processing.

  5. Fixed number of inputs:
    Only the first $MAX_NUMS = 5 inputs are processed; additional inputs are ignored.

  6. Summation and validation:
    The sum of valid inputs is calculated using array_sum() and cast back to an integer using intval(). The challenge requires this sum to be negative in order to retrieve the flag:

    1
    2
    3
    
    if ($sum < 0) {
        echo "You win a flag: $FLAG";
    }
    

Observations and Vulnerability Analysis

While these constraints appear robust, the root vulnerability lies in how PHP handles numeric input in scientific notation (e.g., 9e99) and its interaction with the intval() function.


Behavior of intval() with Large Numbers

PHP’s intval() function behaves inconsistently depending on the provided input:

  • Small and normal-sized numbers or valid numeric strings ('42', '+42') are converted directly to integers.
  • Numbers in scientific notation (e.g., 9e99) are treated as floating-point doubles.
    • If these floating-point values exceed the maximum range of integers on a 64-bit system (9223372036854775807 for signed integers), integer overflow occurs, converting the number into either the maximum or minimum range value.

To demonstrate:

1
2
3
<?php
echo intval('9e99');        // Outputs: 9223372036854775807 on most 64-bit systems
echo intval(9e99 + 1);      // Outputs: -9223372036854775808 due to integer overflow

Key Observations:

  1. A single input of 9e99 results in the maximum integer value (9223372036854775807).
  2. Adding 1 to 9e99 causes an integer overflow, wrapping the sum to -9223372036854775808 on a 64-bit system.

This overflow behavior serves as the core of our exploit.


Exploitation

Crafting the Payload

To bypass validation and generate a negative output, we supply the following numbers:

1
9e99, 1, 0, 0, 0

Here’s how this works step-by-step:

  1. 9e99 appears as a valid numeric input, as is_numeric('9e99') evaluates to true.
  2. The length of 9e99 is only 4 characters, passing the length check (strlen('9e99') <= 4).
  3. intval('9e99') produces 9223372036854775807, the maximum integer value for signed 64-bit integers.
  4. Adding 1 to 9e99 causes integer overflow in PHP, and intval() then interprets the sum as a negative number (-9223372036854775808), due to the signed two’s complement representation used for integers.
  5. The remaining values (0, 0, 0) do not affect the total.

Calculation:

1
2
3
intval(9e99) = 9223372036854775807
intval(9e99 + 1) = -9223372036854775808
Array Sum: (-9223372036854775808) + 0 + 0 + 0 = -9223372036854775808

Numberizer with 9e99 only

Numberizer flag 9e99 and 1

Since the sum is now negative, the challenge validation is bypassed, and the flag is revealed.


Accessing the Flag

Submitting the payload 9e99, 1, 0, 0, 0 through the form results in the following:

1
You win a flag: ENO{INTVAL_IS_NOT_ALW4S_P0S1TiV3!}

Technical Insights

  1. Scientific Notation and Integer Overflow:
    Numbers in scientific notation (9e99) appear deceptively small when measured in characters but represent astronomically large values. When processed through intval(), these large values often exceed the signed integer range and result in overflow.

  2. Behavior of intval() on Overflow:

    • The function returns the maximum signed integer value (9223372036854775807) on overflow.
    • Adding any small integer (+1) causes wrapping into the negative range, producing -9223372036854775808, as PHP uses two’s complement arithmetic for signed integers.
  3. Validation Oversights:
    The challenge failed to account for edge cases involving scientific notation, where large values still satisfy length and numeric checks but lead to unintended behavior like integer overflow.

  4. Impact of Signed Integers:
    The root cause of this vulnerability lies in the interpretation of integers as signed 64-bit, which allows wrapping into negative values when handling excessively large numbers.