CVE-2026-0300

Catalpa 网络安全爱好者

本文对 PaloAlto PAN-OS 系统 Authentication Portal 服务中的一个缓冲区越界漏洞 CVE-2026-0300 进行简要分析。

由于目前还未发布补丁,以下分析可能存在错误。

该漏洞已存在在野利用情况,受影响的用户应该立即应用 PaloAlto 提供的漏洞缓解措施。

漏洞信息

漏洞公告地址:https://security.paloaltonetworks.com/CVE-2026-0300

漏洞标题:CVE-2026-0300 PAN-OS: Unauthenticated user initiated Buffer Overflow Vulnerability in User-ID™ Authentication Portal

漏洞描述:A buffer overflow vulnerability in the User-ID™ Authentication Portal (aka Captive Portal) service of Palo Alto Networks PAN-OS software allows an unauthenticated attacker to execute arbitrary code with root privileges on the PA-Series and VM-Series firewalls by sending specially crafted packets.

漏洞类型:CWE-787: Out-of-bounds Write,CAPEC-100 Overflow Buffers

所在组件:PAN-OS 防火墙中的 Authentication Portal 组件

在野利用信息:PaloAlto 官方发布了关于该漏洞在野利用的情况:https://unit42.paloaltonetworks.com/captive-portal-zero-day/ 其中明确提到了 Upon successful exploitation, the attacker was able to inject shellcode into an nginx worker process.

环境部署

PAN-OS 没有直接提供 root 权限,可以通过挂载磁盘修改 /etc/passwd 中管理员用户 SHELL 的方式来获取 root 权限。具体过程在这里不做过多阐述。该漏洞是位于 Authentication Portal 服务中,该服务的配置比较复杂,可以参考官方文档:https://docs.paloaltonetworks.com/ngfw/administration/user-id/map-ip-addresses-to-users/map-ip-addresses-to-usernames-using-captive-portal/configure-captive-portal

也可以参考视频教程:https://www.youtube.com/watch?v=xp4Kh9ux5CM

PAN-OS 系统中自带 gdb 调试器,可以手动导入 GEF 来辅助分析。

威胁检测规则 🤖

目前还没有针对该漏洞的修复补丁,但官方提供了一个威胁检测规则,PaloAlto 防火墙的威胁检测规则是以 Threat ID 形式发布的,只有订阅了高级威胁检测服务时才能下载。官方还提供离线更新包,下载之后可以在防火墙管理后台进行导入。

更新包是经过加密处理的,查看其内容可以发现一系列 Salted__ 字符串,这是 AES 加密的特征之一,需要研究当导入更新包时系统是如何进行解密的。我们采用 AI 进行自动分析,将测试环境的 SHELL 权限接入 AI,并要求其找到更新包的解密方法,以下信息仅作为参考

规则包结构

规则包使用 PAN-OS 专有的 PanImage 格式(Format 3.0):

1
2
3
4
偏移 0x000:  Sig1 (256B)     — RSA-SHA256 签名,覆盖 0x100 到文件尾
偏移 0x100: Meta (3584B) — Python 字典格式的元数据
偏移 0xF00: Sig0 (256B) — RSA-SHA256 签名,覆盖解密后的原始文件
偏移 0x1000: 加密载荷 — OpenSSL AES-256-CBC 加密的 RPM 包

元数据中确认包类型为 contents,版本 9097-10022

解密方法

PAN-OS 系统中 cryptod 守护进程管理密钥。解密需要:

1
2
3
4
5
6
7
8
9
10
11
# 1. 从 cryptod 提取 AES 口令
python3 -c "import cryptod; cd=cryptod.cryptod(); cd.ks_get_to_file_direct('pancontent-8.0.pass', '/tmp/passphrase')"

# 2. 提取加密载荷(跳过 4096 字节头部)
dd if=panupv2-all-contents-9097-10022 bs=1 skip=4096 of=encrypted.bin

# 3. OpenSSL 解密
openssl enc -d -aes-256-cbc -md sha256 -in encrypted.bin -out decrypted.rpm -pass file:/tmp/passphrase

# 4. 解压 RPM
rpm2cpio decrypted.rpm | cpio -idm

Thread ID 510019

解压后的 global/ 目录包含全局规则元数据:

1
grep -r "510019" opt/content/update/all-threat-content/global/

命中三个文件:

文件 内容
global/global_threat.xml 规则元数据(名称、类别、严重性、默认动作)
global/global_new_threat.xml 同上(新威胁列表)
global/global_threat_v2.xml 同上(v2 格式)

三个文件中 510019 的条目完全相同:

1
2
3
4
5
6
7
8
9
<entry id="510019" name="Palo Alto Networks PAN-OS Out-of-Bounds Read Vulnerability">
<category>dos</category>
<severity>medium</severity>
<engine-version min="11.1"/>
<affected-host>
<server>yes</server>
</affected-host>
<default-action>alert</default-action>
</entry>

关键信息: 规则名称是 “Palo Alto Networks PAN-OS Out-of-Bounds Read Vulnerability”,需要 PAN-OS 11.1+ 引擎版本支持。XML 文件只包含规则的元信息(名称、类别、默认动作等),实际的检测签名不在这里。签名被加密打包在 .fnc 文件中。

fnc 文件解密

规则包中所有检测签名都存储在 .fnc 文件中,这些文件经过两层处理:

  1. AES 加密: 使用 tdb_encrypt 工具解密(-d -f 参数启用 AES-256-FIPS 模式)
  2. zlib 压缩: 解密后是 zlib 压缩数据,需要再解压才是可读的 XML 文本
1
2
3
4
5
# 解密
tdb_encrypt -d -f -r all.fnc -w all.dec

# 解压 zlib
python3 -c "import zlib; open('all.txt','wb').write(zlib.decompress(open('all.dec','rb').read()))"

提取检测签名

从解密后的 http15/all.fnc 中提取完整条目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<entry id="510019" name="Palo Alto Networks PAN-OS Out-of-Bounds Read Vulnerability">
<single-event-attack>
<engine-version min="11.1"/>
<version>1</version>
<release-date>0000-00-00 00:00:00</release-date>
<severity>medium</severity>
<category>dos</category>
<direction>client2server</direction>
<vulnerability/>
<reference>
<member>https://cwe.mitre.org/data/definitions/788.html</member>
</reference>
<affected-system>
<application>
<member>http</member>
</application>
<host>
<member>server</member>
</host>
</affected-system>
<signature>
<exploit>GENERIC</exploit>
<scope>protocol-data-unit</scope>
<and>
<entry>
<equal-to>
<field>http-req-cookie-client-info-overflow</field>
<value>1</value>
</equal-to>
</entry>
</and>
</signature>
<action>
<alert/>
</action>
</single-event-attack>
</entry>

核心检测条件: http-req-cookie-client-info-overflow == 1

检测签名引用了字段 http-req-cookie-client-info-overflow,在 req_cookie_client_info_decode_filter_state 状态中设置该字段。

在 HTTP 请求头的 Cookie 解析中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 匹配 Cookie 中的 CLIENTINFO= 参数
cts ".*CLIENTINFO=" :
{
if (req_hdr_type == COOKIE && http_method == POST) {
// 检查 SAML SSO 会话标记
if (call_func(FUNC_ID_SESS_HASH_GET, 1, ($sport << 16) + SESSION_HASH_SAML_SINGLE_LOGOUT) == 0x01) {
call_func(FUNC_ID_SESS_HASH_DEL, 1, ($sport << 16) + SESSION_HASH_SAML_SINGLE_LOGOUT);
// 开始 Base64 解码
field_begin("http-req-cookie-b64-encoded");
call_func(FUNC_ID_SET_FIELD_FLAG, FIELD_FLAG_ID_DECODE_FILTER_START);
call_func(FUNC_ID_SET_DECODE_FILTER, BASE64_MODE_HTTP_COOKIE, DECODER_FILTER_TYPE_BASE64);
field_end();
if ($?) {
proto |= 0x20;
save_state();
jump req_cookie_client_info_decode_filter_state; // 跳转到检测状态
}
}
}
}

触发条件总结

  • HTTP POST 请求
  • Cookie 头中包含 CLIENTINFO= 参数
  • 存在 SAML Single Logout 会话标记(SESSION_HASH_SAML_SINGLE_LOGOUT

溢出检测状态

1
2
3
4
5
6
7
8
9
10
#if (PAN_ENGINE_VERSION >= (PAN_VERSION(11,1,0,0)))
state req_cookie_client_info_decode_filter_state
{
field_begin("http-req-cookie-client-info-decoded");
skip(2); // 跳过前 2 字节
if (*($ - 2):16 > 235) { // 读取这 2 字节作为 16-bit 整数,判断是否 > 235
eval("http-req-cookie-client-info-overflow", 1); // 设置溢出标志
}
}
#endif

检测逻辑

  1. CLIENTINFO 的值经过 Base64 解码
  2. 读取解码后数据的前 2 字节,解释为 16-bit 整数(大端序)
  3. 如果该整数 > 235(即 MAX_DEV_VSYS 常量),则设置 http-req-cookie-client-info-overflow = 1
  4. Threat ID 510019 规则匹配到此字段值为 1,触发告警

完整检测链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
攻击者发送 HTTP POST 请求

├─ 请求头 Cookie 包含 CLIENTINFO=<base64_value>


HTTP 解码器 (decoders/src/http.fnc)
│ 匹配 ".*CLIENTINFO=" (Cookie 头, POST 方法)
│ 检查 SAML SSO 会话
│ Base64 解码 CLIENTINFO 的值
│ 跳转到 req_cookie_client_info_decode_filter_state


溢出检测状态
│ 读取解码后数据前 2 字节 → 16-bit 整数
│ 判断: 值 > 235 (MAX_DEV_VSYS)?
│ 是 → eval("http-req-cookie-client-info-overflow", 1)


Threat ID 510019 (threats/vulnerability/http15/all.fnc)
│ 匹配: http-req-cookie-client-info-overflow == 1
│ 动作: alert


触发告警

Authentication Portal

Authentication Portal 是 PaloAlto PAN-OS 防火墙的一个组件,主要用于对用户进行强制认证,例如当用户尝试连接指定资源时,先跳转到 Authentication Portal,完成登录之后才放行。

当服务开启后,它默认会监听 6080、6081、6082 等端口。在系统内部这些端口是由 Nginx 服务监听的,它会加载一系列自定义模块,主要模块叫做 l3svc,对应 pan_http_l3svc_module.so 文件。

根据 Nginx 模块开发文档,该模块的 ngx_http_pan_l3svc_proc_data 函数中包含处理请求的关键函数,最主要的是 ngx_http_pan_l3svc_create_request

此函数包含一个 switch case 结构,它会根据传入的 opcode 选择不同函数来执行,opcode 是根据用户请求的 URL 来决定的,例如当访问 /SAML20/SP/SLO URL 时,代码会调用 ngx_http_pan_l3svc_proc_saml_slo 来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall ngx_http_pan_l3svc_proc_saml_slo(__int64 a1, __int64 a2, __int64 a3)
{
if ( (*(a1 + 936) & 0xA) != 0 )
return ngx_http_pan_l3svc_process_request(a1, a3, 10);
if ( *_pan_debug >= 2u )
{
a2 = 1188;
__pan_print(
"pan_http_l3svc_module.c",
1188,
"ngx_http_pan_l3svc_proc_saml_slo",
2,
0,
"unsupported method 0x%lx\n",
*(a1 + 936));
if ( *_pan_debug >= 2u )
{
a2 = 1191;
__pan_print("pan_http_l3svc_module.c", 1191, "ngx_http_pan_l3svc_proc_saml_slo", 2, 0, "shouldn't be here\n");
}
}
return ngx_http_pan_l3svc_serve_error_page(a1, a2, a3);
}

其中 ngx_http_pan_l3svc_process_request 函数的第三个参数就是 opcode,在这里它是 10,那么就对应下面的 switch 分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
case 0xA:
*v14 = 0;
*obj = 0;
v17 = 0;
v18 = 0;
v19 = 0;
v20 = 0;
v21 = 0;
memset(v22, 0, sizeof(v22));
if ( pan_parse_saml_sso_params(a1, v4, obj, v14, buf, 1024, v15, 1024u, *(v3 + 28)) )
return -1;
pan_serialize_saml_sso_params(l3svc_process_ctx, obj, *v14, buf, v15, *(v3 + 28));
goto LABEL_27;

大部分功能的处理流程都类似,主要涉及两类函数:解析并序列化数据、将序列化的数据发送到后端。

在上例中,pan_parse_saml_sso_params 函数用于从请求中解析各个参数,并将它们填充到指定结构体中。pan_serialize_saml_sso_params 则用来执行序列化操作,将结构体转换成字节数组。

随后在 LABEL_27 中的相关代码中,会将请求发送到后端继续处理,根据 Nginx 配置文件的定义:

1
2
3
4
upstream backend_l3svc {
#server 127.0.0.1:11984;
server unix:/etc/nginx/l3svc/l3svc_useridd.sock;
}

数据发送到本地 unix socket 文件,useridd 进程监听此 socket。pan_user_id_l3svc_request_proc_i 函数负责是请求解析的入口,它和 ngx_http_pan_l3svc_create_request 类似,也包含 switch case 结构。

l3svc 在向后端发送数据时会携带 opcode,useridd 根据 opcode 调用不同功能处理请求,例如当 opcode 等于 10 时,就会执行下面的 switch 分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
case 10u:
memset(dest, 0, 124);
LODWORD(p_n4) = 0;
if ( pan_create_responsebuf(v12, DWORD1(p_n4)) == 1 )
{
v21 = *(*(v12 + 384) + 8LL);
if ( pan_deserialize_saml_sso(v13, dest, v21, &p_s, s, &p_n4, &p_n4 + 1) == -1 )
{
LABEL_51:
methods_1 = -1;
}
else
{
methods = pan_user_id_handle_saml_slo(&p_n4 + 1, dest, v21, &p_s, s);
LABEL_24:
methods_1 = methods;
}
}
else
{
methods_1 = -1;
if ( *_pan_debug >= 2u )
__pan_print(
"pan_user_id_l3svc.c",
0xF85u,
"pan_user_id_l3svc_request_proc_i",
2,
0,
"Error creating responsebuf failed to process SAML_SLO \n",
p_n4);
}
break;

其整体逻辑是先将接收到的数据进行反序列化形成对应的结构体,然后再调用相关函数继续处理。最终处理的结构进行序列化之后再发回给 l3svc。

越界读取漏洞

根据威胁检测规则,当用户以 POST 方法访问 /SAML20/SP/SLO 并且 Cookie 包含 CLIENTINFO 参数时,需要检查此参数 Base64 解码后的前两个字节代表的数值是否大于 235 (即 MAX_DEV_VSYS)。

根据前面分析的 Authentication Portal 服务结构,访问 /SAML20/SP/SLO 时 l3svc 模块会调用 pan_parse_saml_sso_params 函数处理,涉及代码片段:

1
2
3
4
5
6
7
8
9
10
11
if ( ngx_http_parse_multi_header_lines(a1 + 416, "CLIENTINFO", &v42) != -5 )
{
max_clientinfo_size = a8 - 1;
v21 = v42;
if ( v42 > max_clientinfo_size )
v21 = max_clientinfo_size;
__snprintf_chk(s, a8, 1, -1, "%s", v43);
s[v21] = 0;
v19 = s;
}
v10->saml_cookie_size = strlen(v19);

此处对参数长度有限制,不能超过 1024 字节,相关参数序列化后发送到 useridd。在 pan_user_id_handle_saml_slo 函数里面会处理 CLIENTINFO 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
__int64 __fastcall pan_user_id_handle_saml_slo(
pthread_mutex_t **a1,
_OWORD *s,
char *dest,
char *p_p_s,
char *clientinfo)
{
// ...

v57 = 0;
v81 = __readfsqword(0x28u);
v79 = 0;
v78 = 0;
*rule = 0;
v80 = 0;
memset(sa, 0, 0x580u);
memset(s_1, 0, 0x6A0u);
memset(s_2, 0, 0x390u);
*&out_obj[1] = 0;
destb_2 = 0;
v48 = 0;
v46 = 0;
if ( !a1 || (obj = *a1) == 0 )
{
v18 = -1;
if ( *_pan_debug >= 2u )
__pan_print("pan_user_id_l3svc.c", 0xC8Bu, "pan_user_id_handle_saml_slo", 2, 0, "Invalid NULL request object ");
return v18;
}
desta = dest;
mutex = *a1;
*(obj + 72) = 0;
*(obj + 88) = 0;
*(obj + 96) = 0xFFFF0000LL;
obj_1 = obj;
p_p_s_1 = p_p_s;
if ( pan_decode_clientinfo(s_2, s, rule, clientinfo) ) // [1]
{
*(obj_1 + 72) = rule[0]; // [2]
*(obj_1 + 76) = rule[1]; // [3]
// ...
}
out_obj[2] = 2;
pan_user_id_handle_saml_unsolicited(&mutex, s, v10, desta, 10);
destb_1 = 0;
mutex_1 = mutex;
v15 = 0;
v13 = 0;
v14 = 0;
LABEL_47:
has_custom_page = pan_usrid_cpage_has_custom_page(*(mutex_1 + 64), *(mutex_1 + 72), 2u); // [4]
mutex_2 = mutex;
if ( has_custom_page ) // [5]
v27 = *(mutex + 72);
else
v27 = 0;
LOWORD(out_obj[1]) = v27; // [6]
out_obj[0] = 0;
v28 = v15 + v13 + destb_1 + 1044;
v29 = *(mutex + 384);
if ( v29 )
{
pan_string_buffer_destruct(v29);
*(mutex_2 + 384) = 0;
}
v30 = pan_string_buffer_construct(*(mutex_2 + 48), v28);
*(mutex_2 + 384) = v30;
if ( v30 )
{
v31 = *(*(mutex + 384) + 8LL);
if ( pan_serialize_saml_slo_resp(v31, v28, &out_obj[1], p_p_s_1, v14, out_obj) != -1 ) // [7]
{
v32 = out_obj[0];
if ( out_obj[0] )
{
v33 = 0;
mutex_3 = mutex;
n_1 = out_obj[0];
while ( 1 )
{
v36 = send(*(mutex_3 + 236), v31 + v33, n_1, 0); // [8]
if ( v36 == -1 )
break;
v33 += v36;
n_1 -= v36;
if ( v33 >= v32 )
goto LABEL_66;
}
// ...
}
}
// ...
}

[1] 处调用 pan_decode_clientinfo 函数解码 CLIENTINFO 参数,主要做 Base64 解码并且对数据进行初步检查,根据函数逻辑推断 CLIENTINFO 数据结构为:

1
2
3
4
5
struct client_info {
_DWORD vsys_id;
_DWORD rule_id;
char client_ip[54];
};

[2][3] 处将解码数据中的 vsys_idrule_id 赋值给 request object 结构体成员,在此 if 代码块中,使用 vsys_id 以及 rule_id 从内存查询对应的虚拟站点配置等信息,默认情况下只有一个虚拟站点 vsys0。

无论查询成功与否,都会来到 [4] 处调用 pan_usrid_cpage_has_custom_page 获取 Portal 的自定义页面,注意此处使用的是未经任何检测的 vsys_id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
_BOOL8 __fastcall pan_usrid_cpage_has_custom_page(__int64 a1, unsigned int vsys, unsigned int a3)
{
__int64 v4; // r12
int v5; // eax
char v6; // r14
int v7; // eax

v4 = *(_QWORD *)(a1 + 1368);
v5 = pthread_mutex_lock((pthread_mutex_t *)(v4 + 8));
if ( v5 && *_pan_debug >= 2u )
__pan_print(
(__int64)"/opt/build/workspace/PANOS/Branches/Orion/release-12.1.4-RELENG/release/12.1.4/src/libs/common/sys/include/pan_mutex.h",
0x122u,
"pan_mutex_lock",
2,
0,
(__int64)"pthread_mutex_lock() failed err=%d\n",
v5);
v6 = *(_BYTE *)(vsys + 0x2B3LL * *(unsigned __int8 *)(v4 + 1438) + v4 + 135LL * a3 + 56);
v7 = pthread_mutex_unlock((pthread_mutex_t *)(v4 + 8));
if ( v7 )
{
if ( *_pan_debug >= 2u )
__pan_print(
(__int64)"/opt/build/workspace/PANOS/Branches/Orion/release-12.1.4-RELENG/release/12.1.4/src/libs/common/sys/include/pan_mutex.h",
0x143u,
"pan_mutex_unlock",
2,
0,
(__int64)"pthread_mutex_unlock() failed err=%d\n",
v7);
__pan_assert_impl(
"0",
"/opt/build/workspace/PANOS/Branches/Orion/release-12.1.4-RELENG/release/12.1.4/src/libs/common/sys/include/pan_mutex.h",
324,
"pan_mutex_unlock",
1);
}
return v6 != 0;
}

该函数读取 config_base + 326 + VSYS + 691 * profile_idx 处的标志位,当读取到非 NULL 字节时返回 True,此时在 [5][6] 就会把 vsys_id 赋值给 out_obj[1],并且随后在 [7] 处传入 pan_serialize_saml_slo_resp 函数使用,这里会把 vsys_id 以及其它一些参数序列化并在 [8] 处发回给 l3svc。

由于 vsys_id 是用户完全可控且未经任何大小限制,传入 pan_usrid_cpage_has_custom_page 会导致越界读取,当目标位置存在非 0 值时,这个 vsys_id 还会返回 l3svc 模块继续处理。

在 l3svc 中,从 useridd 收到响应数据之后调用 ngx_http_pan_l3svc_process_header 函数,根据 opcode 调用 pan_send_saml_slo 解析响应数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
__int64 __fastcall pan_send_saml_slo(_QWORD *a1, __int64 a2, __int64 a3, __int64 a4, _DWORD *a5)
{
// ...

vsys = __ROL2__(*(a4 + 4), 8); // [1]
v41[0] = vsys;
// ...

if ( v17 <= 3 || (v21 = _byteswap_ulong(*(a4 + 22)), v22 = v7 - 26, v23 = v22 < v21, v24 = v22 - v21, v23) )
{
v5 = -1;
if ( *_pan_debug >= 2u )
__pan_print("pan_http_l3svc_module.c", 1727, "pan_send_saml_slo", 2, 0, "Data truncated!\n");
}
else
{
// ...
if ( v34 != 0x2000000 ) // [2]
{
v31 = a1;
v19 = v40;
v20 = dest;
v5 = pan_send_saml_slo_response(v31, a2, v41, v35, v40, dest);
goto LABEL_32;
}
v5 = ngx_http_send_page(a1, a2, a3, *(a2 + 28), vsys, v41, nullptr, dword_24E40); // [3]
}
}
LABEL_31:
v20 = dest;
v19 = v40;
LABEL_32:
__pan_free(*l3svc_process_ctx, v20, v43 + 1);
LABEL_33:
if ( v35 )
__pan_free(*l3svc_process_ctx, v35, (v44 + 1));
if ( v19 )
__pan_free(*l3svc_process_ctx, v19, (v45 + 1));
return v5;
}

[1] 处获取 useridd 发回的 vsys_id 值,在 [2] 处存在一个判断检查响应状态,在 [3] 处将 vsys_id 参数传入 ngx_http_send_page 函数,最终 vsys_id 被传入 ngx_http_l3svc_get_response_page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
__int64 __fastcall ngx_http_l3svc_get_response_page(__int64 a1, unsigned int vsys, unsigned int a3)
{
// ...

v3 = a1;
v14 = __readfsqword(0x28u);
*a4 = 0;
v4 = 9;
if ( a3 >= 2 )
{
v4 = 10;
if ( a3 - 2 >= 4 )
{
if ( a3 - 8 >= 5 )
v4 = 5LL * (a3 - 21 < 2) + 9;
else
v4 = *&asc_1A750[8 * (a3 - 8)];
}
}
v5 = l3svc_process_ctx[vsys + 0x3222];
if ( !v5 )
{
l3svc_process_ctx[vsys + 0x3222] = pan_cpage_vsys_construct(*l3svc_process_ctx);
v5 = l3svc_process_ctx[vsys + 0x3222];
}
v6 = *(v5 + 8 * v4);
if ( v6 )
{
buf = 0;
goto LABEL_25;
}
// ...
return v6;
}

代码将 vsys_id 作为 l3svc_process_ctx 的下标使用,整条链路上都没有限制 vsys_id 的范围,所以这里也存在越界读取。ngx_http_l3svc_get_response_page 的作用是获得 ResponsePage 对象,并在随后的代码中获取此对象中的内容作为页面响应给客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
response_page = ngx_http_l3svc_get_response_page(v11, vsys, v10);
if ( response_page )
{
v21 = response_page;
norify_page[0] = 0;
if ( pan_notify_page_build(*response_page, norify_page, 0xA000u, &v49) < 0 )
{
v24 = -1;
if ( *_pan_debug >= 2u )
__pan_print("pan_l3svc_utils.c", 826, "ngx_http_send_page", 2, 0, "pan_notify_page_build() failed\n");
}
else
{
if ( v10 > 1 )
{
url_form = 0;
if ( v10 <= 0x16 )
{
v25 = 0x60003C;
if ( _bittest(&v25, v10) )
{
url_form = ngx_http_get_url_form(v11, v10, 0, byte_2EE50, 25600, v22, a7);
}
else
{
v30 = 3840;
if ( _bittest(&v30, v10) )
url_form = __snprintf_chk(
byte_2EE50,
25600,
1,
25600,
"<style>\n"
"#dError1 {\n"
" color: #E10000;\n"
" margin-top: 10px;\n"
" padding-top: 10px;\n"
" padding-bottom: 10px;\n"
" margin-left: 10px;\n"
" margin-right: 10px;\n"
" padding-left: 10px;\n"
" padding-right: 10px;\n"
" font-weight: bold;\n"
" overflow: auto;\n"
"}\n"
"</style>\n"
"<form name=\"login\" id=\"login_form\" >\n"
"<h3><font color='red'><b>Authentication Failed</b></font></h3><div id=\"dError1\" class=\"dEr"
"ror1\">Please contact the administrator for further assistance</div></form>\r\n");
}
}
}
else
{
url_form = ngx_http_get_uid_form(v11, v10, 0, byte_2EE50, 0x6400u, a6, a7);
}
v44 = url_form;
l3svc_out[2] = 0;
l3svc_out[1] = 0;
l3svc_out[0] = 0;
l3svc_out_buf[0] = 0;
l3svc_out_buf[1] = 0;
l3svc_out_buf[2] = 0;
l3svc_out_buf[3] = 0;
l3svc_out_buf[4] = 0;
l3svc_out_buf[5] = 0;
l3svc_out_buf[6] = 0;
l3svc_out_buf[7] = 0;
l3svc_out_buf[8] = 0;
l3svc_out_buf[9] = 0;
l3svc_out_buf[10] = 0;
l3svc_out_buf[11] = 0;
l3svc_out_buf[12] = 0;
l3svc_out_buf[13] = 0;
unk_35380 = 0;
v31 = strlen(norify_page);
*(v11 + 76) = 200;
*(v11 + 94) = "text/html";
*(v11 + 93) = 9;
*&l3svc_out_buf[0] = norify_page;
*(&l3svc_out_buf[0] + 1) = &norify_page[v31];
v32 = DWORD2(l3svc_out_buf[4]);
WORD4(l3svc_out_buf[4]) |= 2u;
*(v11 + 109) = v31;
*&l3svc_out[0] = l3svc_out_buf;
if ( v21[1] )
{
*&l3svc_out_buf[5] = byte_2EE50;
*(&l3svc_out_buf[5] + 1) = &byte_2EE50[v44];
BYTE8(l3svc_out_buf[9]) |= 2u;
*(&l3svc_out[0] + 1) = &l3svc_out[1];
v33 = v44 + v31;
*(v11 + 109) = v33;
*(&l3svc_out[1] + 1) = &l3svc_out[2];
*&l3svc_out[1] = &l3svc_out_buf[5];
*&l3svc_out_buf[10] = v21[1];
*(&l3svc_out_buf[10] + 1) = v21[1] + *(v21 + 4);
LOWORD(v32) = word_35388;
word_35388 |= 2u;
v34 = &word_35388;
*(v11 + 109) = v33 + *(v21 + 4);
*(&l3svc_out[2] + 1) = 0;
*&l3svc_out[2] = &l3svc_out_buf[10];
}
else
{
*(&l3svc_out[0] + 1) = 0;
v34 = &l3svc_out_buf[4] + 4;
}
*v34 = v32 | 0x182;
if ( a4 <= 0x15 )
{
v35 = 2097173;
if ( _bittest(&v35, a4) )
{
v36 = ngx_list_push(v11 + 496);
if ( v36 )
{
*v36 = 1;
v36[1] = 15;
v36[2] = "X-Frame-Options";
v36[3] = 4;
v36[4] = "deny";
}
}
}
v37 = ngx_http_send_header(v11);
v24 = v37;
if ( v37 != -1 && v37 <= 0 && v11[1265] >= 0 )
{
v38 = v11;
v24 = ngx_http_output_filter(v11, l3svc_out);
if ( v24 == -2 )
{
v39 = 0;
while ( 1 )
{
++v39;
v40 = *_pan_debug;
if ( v39 > a8 )
break;
if ( v40 >= 5u && (!_pan_debug_dynamic_filter || _pan_debug_dynamic_filter(v38)) )
__pan_print("pan_l3svc_utils.c", 869, "ngx_http_send_page", 5, 0, "NGX_AGAIN.. retry count %d\n", v39);
v38 = v11;
v41 = ngx_http_output_filter(v11, 0);
if ( v41 != -2 )
{
v24 = v41;
if ( v39 && *_pan_debug >= 4u )
{
LODWORD(v43) = v39;
__pan_print(
"pan_l3svc_utils.c",
873,
"ngx_http_send_page",
4,
0,
"recieved NGX_EAGAIN.. succeeded writing after %d retries\n",
v43);
}
return v24;
}
}
v24 = -2;
if ( v40 >= 2u )
{
LODWORD(v43) = a8;
__pan_print(
"pan_l3svc_utils.c",
866,
"ngx_http_send_page",
2,
0,
"Failed to write response.. reached max configured retry attempts %d , rc:%ld\n",
v43,
-2);
}
}
}
}
}

如果可以通过 vsys_id 越界使 ResponsePage 对象包含我们可控的数据,就有可能实现任意地址读取。

进一步利用

想利用 vsys_id 越界读取任意地址内存,有两个关键条件:

  1. 在 useridd 中 vsys_id 越界后读取的单个字节不能为 0,使其能够通过 pan_usrid_cpage_has_custom_page 检查回到 l3svc 模块。
  2. vsys_id 作为下标在 l3svc 中越界读取指针时,该指针中包含攻击者伪造的 ResponsePage Vector。

必要时可能需要执行堆喷射操作,令尽可能多的 vsys_id 可以通过 pan_usrid_cpage_has_custom_page 检查,可能也需要在 l3svc 模块中执行堆喷射操作,在堆中构造大量指向可控数据的指针。

越界写入漏洞

当访问 /php/urlblock.php 等 URL 时,l3svc 模块会调用 pan_parse_url_params 函数来解析请求参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
if ( pan_url_read_mpdp_key(&unk_35250) == -1 )
{
v8 = -1;
if ( *_pan_debug >= 2u )
__pan_print(
"pan_l3svc_utils.c",
1895,
"pan_parse_url_params",
2,
0,
"can't read mpdp key from shared-memory.\n");
}
else
{
pan_url_encrypt_print_block_continue_param_impl(
"pan_l3svc_utils.c",
1898,
"pan_parse_url_params",
0,
0,
0,
0,
&unk_35250,
32);
dec_arg_obj[0] = a3 + 8;
dec_arg_obj[1] = a3 + 72;
v76 = (_BYTE *)(a3 + 140);
if ( pan_url_decrypt_arg_str(v4, dec_arg_obj, &unk_35250) == -1 )
{
v8 = -1;
if ( *_pan_debug >= 2u )
__pan_print(
"pan_l3svc_utils.c",
1905,
"pan_parse_url_params",
2,
0,
"decryption error: can't decrypt arg string.\n",
v66);
}
// ...
}

首先调用 pan_url_read_mpdp_key 函数获取 AES 密钥,然后调用 pan_url_decrypt_arg_str 尝试解密请求参数,这个函数定义在 libpancommon_mp.so 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
__int64 __fastcall pan_url_decrypt_arg_str(const char *haystack, __int64 dec_arg_obj, __int64 key)
{
// ...

v48 = __readfsqword(0x28u);
v5 = strstr(haystack, "args=");
if ( v5 )
{
args = (__int64)(v5 + 5);
for ( args_size = 0; *(_BYTE *)(args + args_size) && *(_BYTE *)(args + args_size) != '&'; ++args_size )
;
if ( (unsigned int)args_size < 513 )
{
decoded_size = modp_b64_decode_urlsafe((unsigned int *)&decoded_args, args, (unsigned int)args_size);
if ( decoded_size > 3 )
{
my_size = _byteswap_ulong(decoded_args.data_size);
decoded_size_min_4 = decoded_size - 4;
if ( my_size > 0x180 || my_size > decoded_size_min_4 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "invalid cipher text length %d, dlen=%d\n";
n289 = 289;
goto LABEL_21;
}
if ( (unsigned int)my_size <= 3 )
goto LABEL_19;
iv_size = _byteswap_ulong(*(unsigned int *)&decoded_args.vsys);
if ( *(_DWORD *)&decoded_args.vsys != 0x10000000 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "byte stream size is not matched %u, expected %u.\n";
n289 = 293;
goto LABEL_21;
}
my_size_min_4 = my_size - 4;
if ( my_size_min_4 > 0xF )
{
*(_OWORD *)iv = *(_OWORD *)&decoded_args.unk1;
body_size = my_size_min_4 - iv_size;
if ( body_size <= 3 )
goto LABEL_30;
stream_size_1 = iv_size;
size_2 = *(_DWORD *)(&decoded_args.unk1 + iv_size);
v20 = size_2 == 0x10000000;
v21 = _byteswap_ulong(size_2);
if ( !v20 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "byte stream size is not matched %u, expected %u.\n";
n289 = 294;
goto LABEL_21;
}
n0xF = body_size - 4;
if ( n0xF <= 0xF )
{
LABEL_30:
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 294;
goto LABEL_21;
}
v23 = &decoded_args.unk2[stream_size_1 + 3];
*(_OWORD *)gcm_tag = *(_OWORD *)v23;
n3_1 = n0xF - v21;
if ( n3_1 <= 3 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 295;
goto LABEL_21;
}
v25 = &v23[v21];
cipher_size = _byteswap_ulong(*(_DWORD *)v25);
if ( cipher_size <= 0 || decoded_size_min_4 - 44LL < (unsigned __int64)(unsigned int)cipher_size )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "invalid cipher text length %d, dlen=%d\n";
n289 = 297;
goto LABEL_21;
}
if ( (n3_1 & 0xFFFFFFFC) == 4 )
goto end;
v27 = *((_DWORD *)v25 + 1);
v20 = v27 == *(_DWORD *)v25;
enc_size = _byteswap_ulong(v27);
if ( !v20 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "byte stream size is not matched %u, expected %u.\n";
n289 = 301;
goto LABEL_21;
}
if ( n3_1 - 8 < enc_size )
{
end:
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 301;
goto LABEL_21;
}
__memcpy_chk(input_buf, v25 + 8, enc_size, 512);
dec_size = pan_url_do_decrypt(
(__int64)input_buf,
cipher_size,
key,
(__int64)iv,
(unsigned int *)&decoded_args,
gcm_tag); // [1]
if ( dec_size == -1 )
{
if ( *(_BYTE *)_pan_debug >= 2u )
__pan_print("pan_url_encrypt.c", 307, "pan_url_decrypt_arg_str", 2, 0, "aes_gcm decryption fail\n");
pan_url_encrypt_print_block_continue_param_impl(
"pan_url_encrypt.c",
0x135u,
"pan_url_decrypt_arg_str",
args,
(unsigned __int16)args_size,
iv,
0x10u,
key,
0x20u);
return -1;
}
else
{
if ( dec_size <= 3 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 316;
goto LABEL_21;
}
data_size = decoded_args.data_size;
data_size_1 = _byteswap_ulong(decoded_args.data_size);
if ( data_size_1 <= 1 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 317;
goto LABEL_21;
}
*(_WORD *)(dec_arg_obj + 28) = __ROL2__(decoded_args.vsys, 8);
if ( (data_size_1 & 0xFFFFFFFE) == 2 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 318;
goto LABEL_21;
}
*(_WORD *)(dec_arg_obj + 30) = __ROL2__(decoded_args.cat, 8);
if ( data_size == 0x4000000 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 319;
goto LABEL_21;
}
*(_BYTE *)(dec_arg_obj + 32) = decoded_args.unk1;
if ( data_size_1 - 5 <= 3 )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 320;
goto LABEL_21;
}
*(_DWORD *)(dec_arg_obj + 24) = _byteswap_ulong(*(unsigned int *)decoded_args.unk2);
if ( data_size_1 - 9 <= 3
|| (n = _byteswap_ulong(*(unsigned int *)decoded_args.unk3),
n_3 = data_size_1 - 13,
v34 = n_3 < n,
n3_3 = n_3 - n,
v34) )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 321;
goto LABEL_21;
}
n_4 = n;
memcpy(*(void **)(dec_arg_obj + 16), decoded_args.unk, n);// [2]
if ( n3_3 <= 3
|| (n_1 = _byteswap_ulong(*(_DWORD *)&decoded_args.unk[n_4]),
n_5 = n3_3 - 4,
v34 = n_5 < n_1,
n3_4 = n_5 - n_1,
v34) )
{
v8 = -1;
if ( *(_BYTE *)_pan_debug < 2u )
return v8;
log = "Data truncated!\n";
n289 = 322;
goto LABEL_21;
}
v40 = &decoded_args.unk[n_4];
src = &decoded_args.unk[n_4 + 4];
n_6 = n_1;
memcpy(*(void **)(dec_arg_obj + 8), src, n_1);
if ( n3_4 > 3 )
{
n_2 = _byteswap_ulong(*(_DWORD *)&v40[n_6 + 4]);
if ( n_2 <= n3_4 - 4 )
{
memcpy(*(void **)dec_arg_obj, &v40[n_6 + 8], n_2);
return 0;
}
}
v8 = -1;
if ( *(_BYTE *)_pan_debug >= 2u )
{
log = "Data truncated!\n";
n289 = 323;
goto LABEL_21;
}
}
}
else
{
LABEL_19:
v8 = -1;
if ( *(_BYTE *)_pan_debug >= 2u )
{
log = "Data truncated!\n";
n289 = 293;
goto LABEL_21;
}
}
}
else
{
v8 = -1;
if ( *(_BYTE *)_pan_debug >= 2u )
{
log = "Data truncated!\n";
n289 = 287;
goto LABEL_21;
}
}
}
else
{
v8 = -1;
if ( *(_BYTE *)_pan_debug >= 2u )
{
log = "exceeds maximum base64 length, dlen=%d\n";
n289 = 279;
LABEL_21:
__pan_print("pan_url_encrypt.c", n289, "pan_url_decrypt_arg_str", 2, 0, log);
}
}
}
else
{
v8 = -1;
if ( *(_BYTE *)_pan_debug >= 2u )
{
log = "Missing args string.\n";
n289 = 267;
goto LABEL_21;
}
}
return v8;
}

函数从请求中获取 args 参数值,在 [1] 处调用 pan_url_do_decrypt 函数使用之前读取到的 mpdp 密钥解密参数,解密成功之后继续处理,在 [2] 处存在 memcpy 复制数据操作,这里使用的 size 是直接从解密后的数据中获取的,只要求 size 不能超过 datasize,而 datasize 又是用户可控的,因此可能存在缓冲区溢出漏洞。

此处操作的目标缓冲区是 l3svc 传递进来的一个位于栈上的缓冲区。正常情况下由于缺少 mpdp 密钥、Stack Canary 等信息,无法未授权触发该越界写漏洞,但攻击者可以先利用前面的任意地址读取漏洞泄漏内存中必要的信息,然后利用此漏洞来实现任意代码执行。

利用检测

由于该漏洞已存在在野利用,而且尚未有修复补丁,建议受影响用户立刻应用 PaloAlto 提供的缓解措施,关闭 Authentication Portal 服务或将其限制在可信网络中访问,同时也应该导入 Thread ID 510019 来防御针对该漏洞的攻击。

如需在流量中排查攻击行为,有以下几点可以关注:

  1. 目标端口为 6080、6081 等
  2. 访问 URL 为 /SAML20/SP/SLO,请求方法为 POST 且 Cookie 中包含 CLIENTINFO 参数,该参数解码后的前 2 字节数值大于 135
  3. 访问 URL 为 /php/uid.php、/php/urlblock.php 或者 /php/urladmin.php 且请求中包含异常大的 args 参数值

参考文章

https://docs.paloaltonetworks.com/ngfw/administration/user-id/map-ip-addresses-to-users/map-ip-addresses-to-usernames-using-captive-portal/configure-captive-portal

https://www.youtube.com/watch?v=xp4Kh9ux5CM

https://security.paloaltonetworks.com/CVE-2026-0300

https://unit42.paloaltonetworks.com/captive-portal-zero-day/

  • Title: CVE-2026-0300
  • Author: Catalpa
  • Created at : 2026-05-09 00:00:00
  • Updated at : 2026-05-09 18:19:54
  • Link: https://wzt.ac.cn/2026/05/09/CVE-2026-0300/
  • License: This work is licensed under CC BY-SA 4.0.