pwnable.co.il - hash
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 terminalnetcat
or using thesendline
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()