Featured image of post CRC32 hash collision to defeat a PHP password check - Nullcon Goa 2025

CRC32 hash collision to defeat a PHP password check - Nullcon Goa 2025

Forging a CRC32 hash collision to defeat a PHP password check and leak the flag. Nullcon Goa HackIM 2025 web write-up.

Introduction

Context Explanation

Craphp asks for a password and hands back a flag if you get it right. The source (index.php) is provided, and the whole thing hinges on it using CRC (Cyclic Redundancy Check) hashes in its password check.

Craphp Homepage

Directive

Bypass the password check and read the flag. The catch is that CRC isn’t collision-resistant, so instead of recovering the real password we forge a different one that produces the same CRC16 and CRC8.


Solution

Analyzing Source Code

The check in index.php works like this:

  1. The real password is hardcoded: $MYPASSWORD = "AdM1nP@assW0rd!";.
  2. Our input is rejected unless its length matches $MYPASSWORD.
  3. It computes crc16 and crc8 of both the hardcoded password and our input.
  4. If both CRCs match and our input isn’t literally $MYPASSWORD, it prints $FLAG.

The relevant snippet:

if(isset($_POST['password']) && strlen($MYPASSWORD) == strlen($_POST['password'])) {
    $pwhash1 = crc16($MYPASSWORD);
    $pwhash2 = crc8($MYPASSWORD);

    $password = $_POST['password'];
    $pwhash3 = crc16($password);
    $pwhash4 = crc8($password);

    if($MYPASSWORD == $password) {
        die("oops. Try harder!");
    }
    if($pwhash1 != $pwhash3) {
        die("Oops. Nope. Try harder!");
    }
    if($pwhash2 != $pwhash4) {
        die("OoOps. Not quite. Try harder!");
    }
    $access = true;

    if($access) {
        echo "You win a flag: $FLAG";
    } else {
        echo "Denied! :-(";
    }
} else {
    echo "Try harder!";
}

Observations and Vulnerability Insight

The design looks fine until you notice it’s leaning on CRC for security. CRC16 and CRC8 are error-detection checksums, not cryptographic hashes, and they collide freely: plenty of different inputs share the same CRC value. Stacking two of them (CRC16 + CRC8) narrows the search but doesn’t fix anything, the combined space is only 24 bits, trivially brute-forceable.

So we don’t need the real password, just a collision that satisfies:

"AdM1nP@assW0rd!" != $password
strlen("AdM1nP@assW0rd!") == strlen($password)
crc16("AdM1nP@assW0rd!") == crc16($password)
crc8("AdM1nP@assW0rd!") == crc8($password)

Exploitation

Crafting the Collision

Brute force it: generate random 15-character candidates (the length of $MYPASSWORD) until one matches both CRCs of the hardcoded password. A short PHP script does the job, reusing the challenge’s own crc16/crc8 so the values line up exactly:

<?php

$MYPASSWORD = "AdM1nP@assW0rd!";
$pwhash1 = crc16($MYPASSWORD);
$pwhash2 = crc8($MYPASSWORD);

for ($i = 0; $i < 100000000; $i++) {
    $password = str_random(strlen($MYPASSWORD));
    $pwhash3 = crc16($password);
    $pwhash4 = crc8($password);

    if ($pwhash1 == $pwhash3 && $pwhash2 == $pwhash4) {
        echo "Collision found ! : $password\n";
        break;
    }
}

function str_random($length) {
    $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+';
    $randomString = '';
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, strlen($characters) - 1)];
    }
    return $randomString;
}

function crc16($string) {
  $crc = 0xFFFF;
  for ($x = 0; $x < strlen ($string); $x++) {
    $crc = $crc ^ ord($string[$x]);
    for ($y = 0; $y < 8; $y++) {
      if (($crc & 0x0001) == 0x0001) {
        $crc = (($crc >> 1) ^ 0xA001);
      } else { $crc = $crc >> 1; }
    }
  }
  return $crc;
}


// https://stackoverflow.com/questions/507041/crc8-check-in-php/73305496#73305496
function crc8($input)
{
$crc8Table = [
    0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
    0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
    0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
    0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
    0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
    0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
    0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
    0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
    0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
    0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
    0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
    0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
    0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
    0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
    0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
    0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
    0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
    0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
    0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
    0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
    0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
    0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
    0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
    0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
    0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
    0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
    0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
    0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
    0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
    0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
    0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
    0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
];

    $byteArray = unpack('C*', $input);
    $len = count($byteArray);
    $crc = 0;
    for ($i = 1; $i <= $len; $i++) {
        $crc = $crc8Table[($crc ^ $byteArray[$i]) & 0xff];
    }
    return $crc & 0xff;
}

?>

Accessing the Flag

The script lands a collision pretty quickly:

┌──(kali㉿kali)-[~/Desktop]
└─$ php crc_collision.php  
Collision found ! : Tp#UTARjQMz+6X8

Submitting it clears both CRC checks and the server coughs up the flag:

Craphp flag

You win a flag: ENO{Cr4hP_CRC_Collison_1N_P@ssw0rds!}

Tips & Tricks

  1. CRC is not a hash. CRC16/CRC8 are built for detecting transmission errors, not for comparing secrets. Seeing them in an auth check is a red flag.
  2. Spot small hash spaces. When a challenge validates against a hash, check its output size. 24 bits here is nothing to brute-force.
  3. Reuse the target’s own code. Copying the challenge’s crc16/crc8 into your script guarantees the values match and saves reimplementation bugs.