$cat povratak-nemoguce-jednadzbe.mdMiscHackultet 2026
Povratak Nemoguće Jednadžbe (Return of the Impossible Equation)
A calculator for a Lisp-like language guards its secret by giving back a plain int instead of the special object that holds the flag, which Python's operator-overloading rules turn out to undo in 56 characters.
$man povratak-nemoguce-jednadzbe
What this one teaches:
- Why
int.__add__always returns a plain int, discarding any subclass, even one with extra attributes attached. - Python's
__radd__fallback, called when the left operand's own__add__can't handle the right-hand type. - Reaching a blocked built-in (
type) through an object's own metaclass chain instead of calling it by name.
Task description
A prefix-notation (Polish notation) calculator for the Hy language (a Lisp that compiles to Python). Make the program print a flag stored as an attribute on a custom int.
The name is a sequel-style pun: “nemoguća jednadžba” means “impossible equation,” and “povratak” means “return,” both as in “coming back” and, fittingly for this task, as in the return from a function call.
What the server does
class OOO00OO00O00(int): # a custom int subclass, nothing overridden
None
obj = OOO00OO00O00(0)
obj.flag = os.environ.get('FLAG', '...')
user_input = input('Calculation: ')
if len(user_input) > 58: fail()
if 'flag' in user_input.lower(): fail()
restricted_globals = {
'type': BLOCKED, 'object': BLOCKED, 'fn': BLOCKED,
'defn': BLOCKED, 'defclass': BLOCKED, 'lambda': BLOCKED,
'__builtins__': {'__import__': hy_only_import},
}
result = hy.eval(hy.read(user_input), restricted_globals)
combined = obj + result
print('Result:', combined)
if hasattr(combined, 'flag'):
print(combined.flag)
Constraints: 58 characters maximum, the literal substring flag is banned (case-insensitively), and several names that would make an obvious escape easy are explicitly blocked in the evaluation globals.
Why the straightforward approach fails
obj holds the flag as an attribute, but obj + result is computed with plain Python addition, and int.__add__ always returns a fresh, ordinary int, no matter that obj is actually an instance of a subclass with extra attributes attached. Whatever result evaluates to, the addition itself discards the subclass and its .flag attribute along with it; combined is never going to be obj by this route, however cleverly result is chosen on its own.
Reaching around the blocked names
Two observations about Hy and Python matter here:
fnis a Hy special form, compiled directly to a Python lambda at the AST level by Hy’s own compiler, so puttingfnin the blocked-globals dictionary has no effect: Hy never looks it up as a regular name in the first place. Functions remain fully usable.typeis blocked by name, but reachable through any object’s own metaclass chain. Every object’s__class__is its type, and every type’s own__class__istypeitself (or close enough, for a plain int).0.__class__.__class__istype, retrieved without ever naming the blocked identifier directly.
The actual gap: __radd__
Python’s addition protocol has a fallback: if the left operand’s __add__ returns NotImplemented for the right operand’s type, Python tries the right operand’s own __radd__ instead, and whatever that returns becomes the result of the whole expression. int.__add__ does exactly that when asked to add something it doesn’t recognise as numeric, which clears the way to define a class whose __radd__ just hands back whichever object was on the left of the +, meaning the original obj, flag attribute intact, comes right back out as the result of the addition.
The payload
(((. 0 __class__ __class__) "" #() {"__radd__" (fn [s o] o)}))
56 characters, no banned substring. Reading it piece by piece:
| Fragment | Meaning |
|---|---|
(. 0 __class__ __class__) |
0.__class__.__class__, reaching type without naming it |
type("", #(), {"__radd__": ...}) |
constructing a new class on the fly, with one method |
(fn [s o] o) |
a lambda that ignores self and returns the other operand, o |
the outer (...) |
instantiates that freshly built class |
Why it works
hy.evalreturns an instance of this freshly built class.obj + instance:int.__add__(obj, instance)can’t handle a non-numeric right-hand side and returnsNotImplemented, so Python falls back toinstance.__radd__(obj), which was defined to just returnobj.combinedis thereforeobjitself, not a new plain int, so its.flagattribute survives intact.hasattr(combined, 'flag')is now true, and the server prints it:
CTF2026[532955405732]
What the sandbox actually failed to anticipate
Blocking a handful of named identifiers (type, object, lambda, defn, defclass) assumes those names are the only paths to the capabilities they represent. In a language with a reflective object model, almost every one of those capabilities is also reachable through ordinary attribute access on objects that are never themselves blocked, here, a plain int literal’s own class hierarchy, and a special form (fn) that the sandbox’s name-based filter couldn’t see at all, since Hy had already compiled it away before the filter ever ran.
$cat flag.txt reveal
CTF2026[532955405732]