May 27, 2025
Pwnable.kr - col
This was a fun one. Let’s dive in:
Daddy told me about cool MD5 hash collision today.
I wanna do something like that too!
ssh col@pwnable.kr -p2222 (pw:guest)
I SSH into the server, and find three files to work with:
col@ubuntu:~$ ls -l
total 24
-r-xr-sr-x 1 root col_pwn 15164 Mar 26 13:13 col
-rw-r--r-- 1 root root 589 Mar 26 13:13 col.c
-r--r----- 1 root col_pwn 26 Apr 2 08:58 flag
The setup looks similar to a few other problems I have worked on: col is a SETUID binary that unlocks the flag.
Let’s try running the binary
col@ubuntu:~$ ./col
usage : ./col [passcode]
It appears that the binary is asking for the passcode as an argument. Let’s check out the source code from col.c:
#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
int* ip = (int*)p;
int i;
int res=0;
for(i=0; i<5; i++){
res += ip[i];
}
return res;
}
int main(int argc, char* argv[]){
if(argc<2){
printf("usage : %s [passcode]\n", argv[0]);
return 0;
}
if(strlen(argv[1]) != 20){
printf("passcode length should be 20 bytes\n");
return 0;
}
if(hashcode == check_password( argv[1] )){
setregid(getegid(), getegid());
system("/bin/cat flag");
return 0;
}
else
printf("wrong passcode.\n");
return 0;
}
After examining the source code, it appears that to solve this challenge, I need to find a 20-byte string that collides with the hash code 0x21DD09EC, as processed by the check_password function.
After some back and forth with GPT about how to reverse the math in the check_password function, this script was suggested to find a suitable string. I’ve had mixed results with code generated by AI, but let’s see how it works this time.
def find_passcode():
target = 0x21DD09EC
# Using a simple approach: find integers that sum to the target.
# Splitting into five equal parts
part = target // 5
# Check for potential overflow
if part > 0xFFFFFFFF:
print("Each integer part must be less than 0xFFFFFFFF")
return
# Create 5 integers from the part
# Build the byte representation
integers = [part] * 4
integers.append(target - sum(integers[:4])) # Calculate last integer to fill the sum
passcode_chars = bytearray()
for i in integers:
# Converting each integer into bytes
passcode_chars += i.to_bytes(4, 'little') # Little-endian
# Make sure that the total length is exactly 20 bytes
if len(passcode_chars) != 20:
print("Error: Passcode must be 20 bytes long, got", len(passcode_chars))
return
# Print the characters that make up the passcode
print("Constructed Passcode (bytes):", passcode_chars)
print("Passcode String (not null-terminated):", passcode_chars.decode(errors='ignore'))
find_passcode()
The output for this script:
$ python3 find_col.py
Constructed Passcode (bytes): bytearray(b'\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xcc\xce\xc5\x06')
Passcode String (not null-terminated):
ÈÎÅÈÎÅÈÎÅÈÎÅÌÎÅ
The “Passcode String” looks like some flavor of cursed, so I’m just going to ignore that last part. The bytearray above looks like it could work. I tried using echo to pipe the bytestring to the binary col but it didn’t work.
col@ubuntu:~$ echo -e -n '\xc8\xce\xc5\xc8\xce\xc5\xc8\xce\xc5\xc8\xce\xcd\xc8\xce\xc5' | ./col
usage : ./col [passcode]
Then I tried using printf, with similar results.
col@ubuntu:~$ workprintf '\xc8\xce\xc5\xc8\xce\xc5\xc8\xce\xc5\xc8\xce\xcd\xc8\xce\xc5' | ./col
usage : ./col [passcode]
workprintf: command not found
Looks like piping the string into the binary isn’t going to work. Looking back at the source code reminds me that the string needs to be passed as an argument from the command line. After some more searching and trial and error, I find a working solution.
col@ubuntu:~$ ./col "$(printf '\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xcc\xce\xc5\x06')"
Two_hash_collision_Nicely
This method worked because the 20 bytes were passed directly as argv\[1], matching the expected input of 20 bytes, and allowing the binary to unlock the flag.