Dojo #46
Challenge "Ghost Wisper" du dojo de YesWeHack (novembre 2025)
Le
Le Dojo de YesWeHack est un espace d’entraînement pratique où les hunters peuvent développer leurs compétences en identifiant et exploitant des failles de sécurité à travers une série de défis dédiés au bug bounty. C'est pourquoi les articles de cette catégorie sont tournés comme des rapports de bug bounty.
📝 Summary
A critical Remote Code Execution (RCE) vulnerability was identified in the application’s command-execution workflow. The issue arises from a flawed Unicode-normalization-based sanitization process combined with unescaped interpolation into a system command executed via ``os.popen()`.
The application attempts to block dangerous characters (notably single quotes ') by replacing them and applying Unicode normalization (unicodedata.normalize("NFKC")). However, Unicode full-width characters, such as Fullwidth Apostrophe (U+FF07), are not filtered before normalization, and normalization maps them back to dangerous ASCII characters. This allows an attacker to inject arbitrary shell command separators such as '; and achieve full command execution.
The injected payload is then propagated into both the command and the Jinja2 template, leading to command execution on the host system.
This report describes the vulnerability, provides technical details and exploit flow, root cause analysis, impact assessment, remediation guidelines, and a Proof-of-Concept (PoC) demonstrating the RCE.
✍ Code
import os, unicodedata
from urllib.parse import unquote
from jinja2 import Environment, FileSystemLoader
template = Environment(
autoescape=True,
loader=FileSystemLoader('/tmp/templates'),
).get_template('index.html')
os.chdir('/tmp')
def main():
whisperMsg = unquote("")
# Normalize dangerous characters
whisperMsg = unicodedata.normalize("NFKC", whisperMsg.replace("'", "_"))
# Run a command and capture its output
with os.popen(f"echo -n '{whisperMsg}' | hexdump") as stream:
hextext = f"{stream.read()} | {whisperMsg}"
print( template.render(msg=whisperMsg, hextext=hextext) )
main()
🔍 Technical Details
VID-01 — Unicode Normalization Bypass Leading to Command Injection
Vulnerability type: Command Injection Affected components: unicodedata.normalize(), shell invocation (os.popen) Severity: Critical
User-controlled input (whisperMsg) is passed through:
whisperMsg = unicodedata.normalize("NFKC", whisperMsg.replace("'", "_"))
The intended mitigation is to remove ASCII ' characters. However, the payload uses Fullwidth Apostrophe U+FF07: ', which:
- Bypasses
.replace("'", "_"), because'≠' - Is normalized by NFKC into
', reintroducing the dangerous character - Gets inserted unescaped into:
os.popen(f"echo -n '{whisperMsg}' | hexdump")
- Breaks out of the quoted shell string and injects arbitrary OS commands.
whisperMsg = unquote("<input>")
whisperMsg = unicodedata.normalize("NFKC", whisperMsg.replace("'", "_"))
with os.popen(f"echo -n '{whisperMsg}' | hexdump") as stream:
hextext = f"{stream.read()} | {whisperMsg}"
print(template.render(msg=whisperMsg, hextext=hextext))
🧠 Root Cause
1. Misunderstanding Unicode normalization
The application incorrectly assumes that replacing ' before NFKC normalization eliminates dangerous metacharacters. NFKC canonicalizes several Unicode punctuation marks into ASCII equivalents, including:
| Unicode | Description | Normalized to |
|---|---|---|
U+FF07 |
Fullwidth Apostrophe ' |
' |
U+FE64 |
Small Less-Than Sign | < |
U+FE65 |
Small Greater-Than Sign | > |
The following python script was used in order to get the unicode caracters that are equal to quote after normalization.
import unicodedata
equivalents_quote = []
for codepoint in range(0x110000):
char = chr(codepoint)
try:
# NFKC Normalization
normalized = unicodedata.normalize("NFKC", char)
if normalized == "'":
equivalents_quote.append((char, f"U+{codepoint:04X}", unicodedata.name(char, "UNKNOWN")))
except Exception:
continue
for char, code, name in equivalents_quote:
print(f"{char} ({code}) -> {name}")
print(f"\nTotal equivalent found : {len(equivalents_quote)}")
Output: 
2. Unescaped embedding into shell commands
User input is placed directly into:
f"echo -n '{whisperMsg}' | hexdump"
which is unsafe unless properly escaped or avoided entirely.
3. Command execution through os.popen()
os.popen() spawns a shell (/bin/sh -c "..."), allowing shell metacharacters to be interpreted.
Combined, these flaws produce a reliable RCE primitive.
💥 Impact
A remote attacker can obtain arbitrary code execution inside the server environment. Potential consequences include:
- Full server compromise
- Reading environment variables (API keys, DB credentials, tokens)
- Stealing application secrets
- Lateral movement inside the internal network
- Deploying reverse shells or persistence implants
- Privilege escalation if the runtime user has elevated permissions
Because the attack occurs before template rendering, WAF or template-sandbox protections do not mitigate the vulnerability.
🛡️ Recommendations
✔ Do not embed user input in shell commands
Replace os.popen with safe APIs:
subprocess.run([...], shell=False)
Or avoid shell execution entirely if not required
✔ Escape or remove dangerous characters
Apply strict allowlisting (e.g., alphanumeric + safe punctuation only). Do not rely on Unicode normalization for security filtering.
✔ Sanitize before normalization
If normalization is required for functional reasons, sanitize both: - Before normalization - After normalization
✔ Enforce strict Content Security and Template boundary controls
Avoid injecting attacker data directly into templates unless properly escaped.
✔ Add unit tests for Unicode security regressions
Specifically test NFKC/NFD/NFKD mappings of problematic characters.
✅ Proof of Concept (PoC)
- Original payload :
'; env ;# ' - After flawed sanitization:
'; env ;# '

How the injection works
- The fullwidth apostrophe
'bypasses filtering. - Normalization converts it into
'. - The final shell command becomes:
echo -n ''; env ;# ' | hexdump
This executes:
env
and leaks full environment variables, potentially including sensitive credentials.
FLAG
FLAG{Gh0s7_S4y_BOOO000oooo}