A research report published Thursday describes four weeks of active exploitation of JFrog Artifactory, the artifact registry that sits in front of most Java and DevOps build pipelines. Attackers chain two patched CVEs to turn a single unauthenticated request into an admin-scoped token, and the tell is uncomfortable: every request they make afterward shows up in your logs as token:anonymous, an actor name that looks exactly like background noise. CISA has all three CVEs on the Known Exploited Vulnerabilities catalog, and the federal remediation deadline for two of them is September 25.
I write about MCP and tooling security here, and before trusting any report I cross-check it against primary records. The three NVD records and the KEV feed hold up: the privilege escalation is scored 8.1 by the vendor and 8.8 by NVD, the token exposure 7.5, the default-config admin bypass 9.8. What the records do not give you is an audit plan, so I built one from the Wiz IOC table and the version ranges. Three checks, about thirty minutes. One honesty note before we start: I derived these from the published IOCs and ranges, I have not run them against a live instance, so treat every log path here as a starting point to adapt.
First, the version. Everything else depends on it:
curl -s -H "Authorization: Bearer $ARTIFACTORY_TOKEN" \
"https://artifactory.internal.example/artifactory/api/system/version"
Between August 15 and September 8, Wiz observed multiple actors chaining two CVEs against self-hosted Artifactory instances. The chain has three moves:
POST /access/api/v1/aws/token/, with a trailing slash. The bare path rejects callers with a 401. The trailing-slash variant returned HTTP 200 with a JWT for the internal anonymous user, even when anonymous access was disabled. That is CVE-2026-42018.POST /access/api/v1/tokens. The actor exchanges that JWT for an admin-scoped token. The flaw here is scope validation: the token's signature and issuer are checked, its intended scope is not. That is CVE-2026-42016.PUT /api/security/users/<username> (or the matching UI endpoint), answered with a 201. A persistent admin account now exists. In some cases the whole sequence, first request to created admin account, took under five minutes.
The report is explicit that neither CVE grants admin alone. The chain is the exploit. There is also a separate door: POST /access/api/v1/registry/join returned HTTP 200 or 201 with an admin-scoped token in the response body under default configuration. That is CVE-2026-82329, scored 9.8, KEV-listed on September 2, and exploited by several actors between September 1 and September 8.
Here is the detail that elevates this from "another CVE post" for me. The escalated token kept the anonymous username but carried admin authority, so every later request appears with an actor of token:anonymous. Wiz's wording is precise and worth keeping: the attacker did not steal an identity, they attached authority to one you already have.
When I wrote about the MCP 2026-07-28 spec going stateless, my conclusion was that a planted prompt becomes a valid credential the moment state moves somewhere an attacker can write to (the article is here). This report is the same lesson approached from the other side. The credential is real and admin-scoped, but its identity is decoration. Correlation is not authorization, and a principal name that every instance already carries is the perfect place to hide authority.
If your SIEM alerts on "anonymous principal performed an admin action" at all, you are ahead of most setups I have seen. If your logs do not distinguish the built-in anonymous user from an admin-scoped token wearing its name, the attacker inherits that blind spot for free.
Same argument I made about MCP demos: a happy path proves almost nothing, so run the boring checks before you trust the component (I made that case here). All three checks below come from the Wiz IOC table and the NVD ranges.
Take the version string from the call above and turn it into a tuple:
def vulnerable(v):
hits = []
if v < (7, 133, 11):
hits.append("CVE-2026-42016 priv-esc (CNA range: all < 7.133.11)")
if (v < (7, 111, 20)
or (7, 117, 0) <= v < (7, 117, 27)
or (7, 125, 0) <= v < (7, 125, 19)
or (7, 133, 0) <= v < (7, 133, 28)
or (7, 146, 0) <= v < (7, 146, 8)):
hits.append("CVE-2026-42018 anonymous-token exposure")
if ((7, 111, 4) <= v < (7, 111, 21)
or (7, 117, 0) <= v < (7, 117, 28)
or (7, 125, 0) <= v < (7, 125, 20)
or (7, 133, 0) <= v < (7, 133, 29)
or (7, 146, 0) <= v < (7, 146, 38)
or (7, 161, 0) <= v < (7, 161, 20)):
hits.append("CVE-2026-82329 default-config admin bypass")
return hits
print(vulnerable((7, 125, 18)))
A build on 7.125.18 sits inside all three ranges. One trap before you relax on the older branches: the Wiz report lists fixed versions per release branch (7.111.21, 7.117.28, 7.125.20, 7.133.29, 7.146.38, 7.161.20 or later), but the 42016 record says everything before 7.133.11 is affected. Those two statements do not obviously reconcile for the oldest branches, and I could not settle which is authoritative from public records alone. Treat the newest version your upgrade path allows as the target, and check JFrog's advisory for your specific branch before you close the ticket.
The endpoint strings below are transcribed verbatim from the Wiz IOC table. The request-log location varies by install; the path shown is the container default.
REQ="$JFROG_HOME/artifactory/var/log/artifactory-request.log"
grep -F "POST /access/api/v1/aws/token" "$REQ" # bare path 401s and slash variant 200s
grep -F "POST /access/api/v1/tokens" "$REQ" # token minting
grep -F "POST /artifactory/api/security/token" "$REQ" # legacy token endpoint
grep -F "PUT /api/security/users/" "$REQ" # new accounts
grep -F "POST /access/api/v1/registry/join" "$REQ" # the 82329 door
grep -F "GET /api/system/configuration" "$REQ" # config exfil pattern
The highest-confidence signature in the report is behavioral: a 401 on the bare path followed by a 200 on a trailing-slash variant, from the same client, inside a short window. That is an operator confirming the vulnerable variant before relying on it, and normal clients do not produce that pattern. The other tells are identity mismatches: a low-privilege or anonymous identity minting tokens, enumerating users, or reading and writing /artifactory/api/plugins. A lone 200 on the join endpoint is not proof of anything, so correlate it with account creations or configuration reads before you escalate.
Wiz lists the account names threat actors created, and they are designed to pass a casual scroll.
import json, re
users = json.load(open("artifactory_users.json")) # export from Administration > Identity and Access > Users
NAMED = {"jfrog-distribution", "backup-service", "repo-service",
"jfrog-insight", "jfrog-mission-control", "jfrog-pipeline",
"migration-tool", "ldap_admin", "ldap_administrator", "0xterror"}
PATTERNS = [r"svc_[a-zA-Z0-9]{8}$", r"Nxploited_[a-zA-Z0-9]{3}$",
r"labadmin_[a-zA-Z0-9]{10}$"]
for user in users:
name = user["name"]
hit = name in NAMED or any(re.search(p, name) for p in PATTERNS)
if hit:
print("REVIEW:", name, "| admin:", user.get("admin"),
"| created:", user.get("created_at"))
One honest caveat: depending on your integrations, some of those vendor-looking names might collide with legitimate service accounts, so do not mass-delete on a name match. Confirm the admin flag, the creation timestamp against your change log, and any tokens you cannot account for. Wiz classifies them as malicious admin accounts created by threat actors; your job is proving the creation date was not you.
Admin on a registry is not the end of the incident, it is the start of a worse one. Wiz observed malicious Groovy plugins installed through Artifactory's native plugin framework, which turns a legitimate extensibility feature into the implant , with follow-up commands running through /api/plugins/execute/. Droppers fetched binaries over HTTP into world-writable paths like /dev/shm and /tmp, and a custom Rust backdoor with C2 capabilities appeared in multiple cases. One webshell was uploaded into a repository path, meaning the registry itself stored the implant. The 82329 pattern adds configuration exfiltration, join-key theft, and attackers attaching their own SSH keys to the accounts they created.
I keep coming back to why this beats a normal app RCE. A web server gets patched and rebuilt. A registry is upstream of every build: whichever artifact your CI pulls next was assembled by the thing the attacker now administers. I learned the sink-mismatch version of this lesson on my own scanner, when the sink I guarded turned out not to be the sink that got used (written up here). Most teams guard the application tier and mentally file the registry under infrastructure. This report is four weeks of someone else exploiting exactly that filing mistake.
The remediation numbers are the part I would put in front of a manager. Sixty-seven percent of organizations running Artifactory had at least one instance vulnerable to 42016 when it was disclosed on July 27. Six weeks later, 59% still did. 42018 crawled from 69% to 62% over four weeks. 82329, the only one rated critical, dropped from 67% to 49% within two weeks, and Wiz attributes that speed to the severity rating. All of this happened while every one of the three sat on the KEV catalog, and the 82329 federal deadline of September 5 has already passed.
Here is the thing I cannot stop thinking about. 42016 scores 8.1 from the vendor and 8.8 from NVD, a high, not a critical. On paper it lost the prioritization race to the 9.8. In practice it combined with a 7.5 into unauthenticated admin in two requests, and it shed eight points in six weeks while the critical shed eighteen in two. Attackers compose; scores do not.
So the question for your team: does triage sort by the number, or by whether the bug connects to a neighbor? Would an "actively exploited, chainable to admin" flag outrank a 9.8 with no known exploitation in your queue? I would like to claim mine does, but the honest answer is I had never written that rule down until this report forced me to.
Run check one today. If you are below the fixed version for your branch, patch through your normal emergency path: registry downtime is recoverable, artifact poisoning may not be. If the log sweep or the account sweep turns anything up, treat everything the registry can sign, mint, or store as compromised, deploy tokens, join keys, signing keys included, and rotate. Then re-read JFrog's advisory for the per-branch fix on 42016 before closing anything, because that is the one place where the public records disagree with each other.
And if you run the version sweep, tell me what it turned up in the comments. I am genuinely curious whether 59% is still the right number this week.