Wayback
Wayback
Platform: HackTheBox | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-02-09 | Status: Solved Techniques: aes_cbc_decryption, integer_overflow_arithmetic, prng_seed_bruteforce, static_reverse_engineering
Summary
Two files are provided: - V1 — ELF 64-bit LSB PIE executable (password generator, compiled from Wayback.cpp with GCC 14.2.1, not stripped) - decrypt.py — Python script for AES-CBC decryption with user-provided key
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
HackTheBox| ID:20260209_hackthebox_wayback - Tags: aes, elf, prng, srand, seed_prediction, timestamp_bruteforce, glibc
- Indicators: srand() with time-based seed, rand() for password generation, known date range for seed, AES-CBC encrypted blob, PKCS7 padding
- Source:
20260209_hackthebox_wayback.md
Foothold
Vulnerability / Misconfiguration
- Aes_cbc_decryption
- Integer_overflow_arithmetic
- Prng_seed_bruteforce
- Static_reverse_engineering
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- aes_cbc_decryption
- integer_overflow_arithmetic
- prng_seed_bruteforce
- static_reverse_engineering
- Tags: aes, elf, prng, srand, seed_prediction, timestamp_bruteforce, glibc
Original Writeup
<details><summary>Click to expand original content</summary>Description
A man named Michael Tanz bought 30 bitcoin in 2013 and stored it in his hardware wallet. He set the password for his hardware wallet through a password generator named "V1". He remembers that his password is 20 characters long, and consisted of only alphanumeric characters and symbols. Michael however is not exactly sure of the date he generated the password - he knows it was between the 10th and the 11th of December 2013. Can you crack the password and help him recover his bitcoin?
Two files are provided:
V1— ELF 64-bit LSB PIE executable (password generator, compiled from Wayback.cpp with GCC 14.2.1, not stripped)decrypt.py— Python script for AES-CBC decryption with user-provided key
Analysis
decrypt.py
The script performs AES-256-CBC decryption:
- Key — user input, left-padded with null bytes to 32 bytes
- IV — first 16 bytes of the encrypted blob
- Ciphertext — remaining part after IV
- PKCS7 padding
- Encrypted hex:
ad24426047b0ffb03b679773664838462a6f00bdcaf0589dd1748e9ed5c568601edc87d974894f9dd9b98cc35535145c494eb0af84c8f78d440a033c91c7de62d506d8cabdc2a10138b95139bbe60e89
Reversing the V1 binary
Static analysis of the ELF binary revealed a password generator with the following algorithm:
Function: generate_password(int length, bool include_sym, bool include_num)
Character set (72 characters):
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*_+0123456789
Formed by concatenation: lowercase + uppercase + symbols (if enabled) + digits (if enabled).
Seed calculation — from time(NULL) → localtime(), then a custom formula with 32-bit arithmetic and overflow:
struct tm* tm = localtime(&t);
int seed = tm->tm_mday * 1000000
+ tm->tm_min * 100
+ tm->tm_sec
+ tm->tm_hour * 10000
+ (tm->tm_year + 1900) * 0x540BE400 // year * 1,410,065,408
+ (tm->tm_mon + 1) * 0x5F5E100; // month(1-12) * 100,000,000
srand((unsigned)seed);
Password generation loop:
for (int i = 0; i < length; i++) {
int r = rand();
password[i] = charset[r % charset_len]; // charset_len = 72
}
Vulnerability
The PRNG seed is fully deterministic based on timestamp. Knowing the approximate date range (December 10-11, 2013), we get only 172,800 possible seeds (2 days × 24 hours × 60 minutes × 60 seconds).
Critical nuance: rand() in glibc and macOS are implemented differently. Bruteforce MUST be executed on Linux (glibc), otherwise the generated passwords will be incorrect.
Solution
Step 1: Bruteforce all possible seeds
C program to generate all candidates, compiled and run in a Docker container with glibc:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*_+0123456789";
int main() {
int charset_len = strlen(charset); // 72
int year = 2013;
int month = 12;
for (int day = 10; day <= 11; day++) {
for (int hour = 0; hour < 24; hour++) {
for (int min = 0; min < 60; min++) {
for (int sec = 0; sec < 60; sec++) {
unsigned int seed = (unsigned int)(
day * 1000000 + min * 100 + sec +
hour * 10000 +
year * (unsigned int)0x540BE400 +
month * (unsigned int)0x5F5E100
);
srand(seed);
char password[21];
for (int i = 0; i < 20; i++) {
password[i] = charset[rand() % charset_len];
}
password[20] = '\0';
printf("%s\n", password);
}
}
}
}
return 0;
}
Compilation and execution via Docker:
docker run --rm -v $(pwd):/work -w /work gcc:14 bash -c \
"gcc -O2 -o bruteforce bruteforce.c && ./bruteforce > passwords.txt"
Result: 172,800 candidate passwords.
Step 2: Decrypt with each candidate
Python script iterates through all passwords, attempting AES-CBC decryption with PKCS7 unpadding as an oracle:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
encrypted_message = bytes.fromhex(
'ad24426047b0ffb03b679773664838462a6f00bdcaf0589dd1748e9ed5c568601edc87d974894f9dd9b98cc35535145c494eb0af84c8f78d440a033c91c7de62d506d8cabdc2a10138b95139bbe60e89'
)
with open("passwords.txt", "r") as f:
for i, line in enumerate(f):
password = line.strip()
try:
key = password.encode().ljust(32, b'\x00')
iv = encrypted_message[:16]
ciphertext = encrypted_message[16:]
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
decrypted_padded = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
decrypted = unpadder.update(decrypted_padded) + unpadder.finalize()
print(f"FOUND! Password: {password}")
print(f"Decrypted: {decrypted.decode()}")
break
except Exception:
continue
Result
At line 133,286:
- Password:
eWXtk*Oe%j5cof7Od08G - Decrypted message:
d 30 Bitcoins! , HTB{REDACTED} - Timestamp: 1386784885 = Dec 11, 2013 13:01:25 UTC
Lessons
- glibc vs macOS rand() — critical difference. Same seed produces different sequences. Always use Docker with Linux to reproduce glibc
rand()behavior - 32-bit overflow in seed — the formula uses multiplication by large constants (0x540BE400, 0x5F5E100), causing integer overflow. Need to exactly reproduce the arithmetic with
unsigned int - PKCS7 padding as oracle — with wrong key, AES-CBC decryption produces garbage, and PKCS7 unpadding fails with an error. This allows automatic filtering of incorrect passwords
- 172K search space is trivial — even in Python, bruteforce takes seconds. Time-based seeds from dates give a very small search space
- Static reversing is sufficient — no need to run the binary, just understand the seed algorithm and generation through Ghidra/radare2
Alternative Approaches
- Running V1 with time() substitution — via
LD_PRELOADyou can substitutetime()and make the binary generate passwords for specific timestamps - Angr/symbolic execution — automatic analysis of the seed formula
- Hashcat/John — if there was a password hash instead of AES encryption
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR