pwnable.co.il - hash

3 minute read

Challenge description:

I heard it takes months to find an MD5 collision…

// hash.c
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <openssl/md5.h>

char flag_str[0x100];

void init_buffering() {
    setvbuf(stdin, NULL, 2, 0);
    setvbuf(stdout, NULL, 2, 0);
    setvbuf(stderr, NULL, 2, 0);
    alarm(60);
}

int main() {
    init_buffering();
    unsigned char flag_hash[MD5_DIGEST_LENGTH];
    MD5_CTX flag;
    MD5_Init(&flag);
    int fd = open("flag", O_RDONLY);
    int bytes = read(fd, &flag_str, 0x100);
    close(fd);
    MD5_Update(&flag, flag_str, bytes);
    MD5_Final(flag_hash, &flag);
    puts("Flag MD5: ");
    for(int i = 0; i < MD5_DIGEST_LENGTH; i++) printf("%02x", flag_hash[i]);
    puts("");

    printf("Enter your guess: ");
    char guess_hash[MD5_DIGEST_LENGTH];
    char* guess = malloc(bytes+1);
    bytes = read(0, guess, bytes);
    MD5_CTX guess_ctx;
    MD5_Init(&guess_ctx);
    MD5_Update(&guess_ctx, guess, bytes);
    MD5_Final(guess_hash, &guess_ctx);
    if (!strcmp(flag_hash, guess_hash)) {
        puts("Congrats!!!");
        puts(flag_str);
    } else {
        puts("Wrong!!");
    }
    return 1;
}

In shortly, this code reads the contents of the flag file, hashes it using MD5, and stores the result in a local variable. It then takes user input, hashes it and compares it with the flag’s hash. If the hashes match, the flag is revealed; otherwise, a failure message is displayed.

Now, let’s dig into the code and look for vulns in the hashing process. The most interesting part is this snippet:

printf("Enter your guess: ");
char guess_hash[MD5_DIGEST_LENGTH];
char* guess = malloc(bytes+1);
bytes = read(0, guess, bytes);
MD5_CTX guess_ctx;
MD5_Init(&guess_ctx);
MD5_Update(&guess_ctx, guess, bytes);
MD5_Final(guess_hash, &guess_ctx);
if (!strcmp(flag_hash, guess_hash)) {
    puts("Congrats!!!");
    puts(flag_str);
} else {
    puts("Wrong!!");
}

Here, we see how user input is processed and hashed. A small issue lies in using strcmp to compare the hashes since strcmp stops at the first null byte (\0), So we can use it to exploit this by brute-forcing strings until we find a hash with the same first bytes as the flag hash!

Server response:

$ nc pwnable.co.il 9006
Flag MD5: 
537500469ddfc5b29e9379cdcc2f3c86
Enter your guess:

The flag hash is 537500469ddfc5b29e9379cdcc2f3c86, and the hash ends with a null byte after 5375. Therefore, we need to find another string whose hash starts with 537500.

  • Note: The server will also receive the \n character in both cases, whether sending the string via terminal netcat or using the sendline function from pwntools, so I must add the \n character to each string in the brute force.

Brute-force script:

import hashlib
import itertools
import string

# Function to convert string to md5
def get_md5_hash(s):
    return hashlib.md5(s.encode('utf-8')).hexdigest()

def brute_force_flag():
    # Define the possible characters that might appear in the flag (lowercase letters and numbers)
    characters = string.ascii_lowercase + string.digits

    # We will brute-force the flag bytes with length from 1 to 10 characters
    for length in range(1, 11): 
        for guess in itertools.product(characters, repeat=length):
            guess_str = ''.join(guess) + "\n"
            hash_guess = get_md5_hash(guess_str)
            
            # Print the guess and its corresponding hash
            print(f"Trying: {guess_str} --> MD5: {hash_guess}")
            
            if hash_guess.startswith("537500"):
                print(f"Found matching str: {guess_str}")
                return guess_str

    print("Flag not found within specified length range.")
    return None

if __name__ == "__main__":
    brute_force_flag()

Script results:

Trying: g4nw4
 --> MD5: 537500598c2101141d3d9f25fb41f9e6
Found matching str: g4nw4

Now lets send it to the server!

$ nc pwnable.co.il 9006
Flag MD5: 
537500469ddfc5b29e9379cdcc2f3c86
Enter your guess: g4nw4
Congrats!!!
PWNIL{How_the_hell_did_you_find_this_collision?30105270}

payload

from pwn import *

def main():
	r = remote("pwnable.co.il", 9006)
	r.sendline("g4nw4") # "g4nw4\n" = 537500598c2101141d3d9f25fb41f9e6
	print(r.recvall().decode())
	
if __name__ == "__main__":
	main()