CVE-2024-0012 Palo Alto 认证绕过漏洞分析

漏洞信息

漏洞编号:CVE-2024-0012

漏洞名称:Palo Alto Networks PAN-OS Management 管理端权限绕过漏洞

漏洞原理:nginx 配置问题导致在处理 .js.map$ 路由的时候无需身份验证,导致认证绕过,可以配合 CVE-2024-9474 实现命令执行。

环境搭建

搭建版本:11.0.2

出现问题:vm login 登不上,解决方法等待系统可能还在初始化。初始化过程中会出现 IP 地址,WEB 的账号密码和 CLI 是相通的,可以在 WEB 上面修改。

默认用户密码:admin

https://172.253.1.163

image-20250515144753672

登录后 CLI 的,通过 RCE 漏洞修改 admin 用户的 shell。

1
usermod -s /bin/bash admin

image-20250515144954102

测试过直接添加一个用户(通过执行 shell 脚本,漏洞有长度限制),可以通过 admin 切换到该用户,但是不能直接登录。

1
usermod -aG root admin

下载链接:

环境分析

基本架构

通过进程可以发现有:

httpd => apache

nginx

image-20250515151043095

1
netstat -tulnp | grep -E 'httpd|nginx'

image-20250515152913827

nginx 监听 80,443

apache 监听 28250

image-20250515153126042

1
grep -R 'proxy_pass' /etc/nginx/

image-20250515153257041

所以可以知道是 Apache + PHP 提供 WEB 服务,Nginx 作为反向代理

1
request => nginx(443) => apache(28250) => php

apache

所以 WEB 其实是由 apache 启动的,查看 apache 配置:

image-20250515154100204

1
2
cat /etc/httpd/mgmtui/conf/httpd.conf
grep -Ev '^\s*#|^\s*$' /etc/httpd/mgmtui/conf/httpd.conf
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
# Apache 配置文件、模块等的根目录(并非网站根目录)
ServerRoot "/etc/httpd/mgmtui"
# Apache 启动后记录 PID 的位置
PidFile "/var/run/mgmtui/httpd.pid"
# 核心转储文件保存目录
CoreDumpDirectory "/var/cores"
# Apache 监听本地 28250 端口
Listen 28250

# 监听所有 IP 的 28250 端口
<VirtualHost _default_:28250>
# 设置环境变量,抓取 Authorization 头内容
SetEnvIf Authorization "(.*)" _HTTP_AUTHORIZATION=$1
</VirtualHost>

# 加载 conf.modules.d 配置
Include conf.modules.d/*.conf

User nobody
Group nobody
ServerName 127.0.0.1
# WEB 根目录
DocumentRoot "/var/appweb/htdocs"

# 如果访问 /PAN_help/*.css|js|html|htm,重写为 .gz 文件(用于前端资源压缩加载)
<Location "/">
DirectorySlash off
RewriteEngine on
RewriteRule ^(.*)(\/PAN_help\/)(.*)\.(css|js|html|htm)$ $1$2$3.$4.gz [QSA,L]
AddEncoding gzip .gz
Options Indexes FollowSymLinks
Require all granted
</Location>

# 禁止访问 .htaccess 等以 .ht 开头的文件
<Files ".ht*">
Require all denied
</Files>
<Files "*.css.gz">
ForceType text/css
</Files>
<Files "*.js.gz">
ForceType application/javascript
</Files>
<Files "*.html.gz">
ForceType text/html
</Files>
<Files "*.htm.gz">
ForceType text/html
</Files>
ErrorLogFormat "%{cu}t %l [%P %{g}T] %F: %E: %M"
ErrorLog "/var/log/pan/mgmt_httpd_error.log"
LogLevel info
LogFormat "%>s %t %T %b %U \"%{User-Agent}i\"" combined
LogFormat "%>s %t %T %b %U \"%{User-Agent}i\"" common
LogFormat "%>s %t %T %b %U \"%{User-Agent}i\"" combinedio
CustomLog "/var/log/pan/mgmt_httpd_access.log" combined
TypesConfig /etc/httpd/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
AddDefaultCharset UTF-8
MIMEMagicFile conf/magic
EnableMMAP off
FileETag None

子配置:

1
2
3
4
ServerRoot "/etc/httpd/mgmtui"
Include conf.modules.d/*.conf

ls -al /etc/httpd/mgmtui/conf.modules.d

image-20250515155025136

1
grep -Ev '^\s*#|^\s*$' /etc/httpd/mgmtui/conf.modules.d/php-httpd.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 加载 PHP7 模块,使 Apache 能解析 .php 文件
LoadModule php7_module ../modules/libphp7.so

# 禁止 Web 客户端访问以 .user.ini 命名的文件(通常用于 PHP 的用户级配置)
<Files ".user.ini">
Order allow,deny # 默认 deny 优先
Deny from all # 拒绝所有访问
Satisfy All # 所有条件都需满足(主要用于兼容旧版 Apache)
</Files>

# 将 .php 文件交给 PHP 模块处理
AddHandler application/x-httpd-php .php

# 指定 php.ini 的位置(PHP 配置文件)
PHPINIDir "/etc/httpd/mgmtui"

# 当访问目录时,默认首页文件为 index.php
DirectoryIndex index.php

# 特殊情况:将名为 ocsp 的文件(无扩展名)当作 PHP 文件处理
<FilesMatch ^ocsp$>
SetHandler application/x-httpd-php
</FilesMatch>

PHP 配置文件:

1
grep -vE '^\s*;|^\s*$' /etc/httpd/mgmtui/php.ini
1
2
3
4
5
6
7
[PHP]

auto_prepend_file = uiEnvSetup.php ; PAN-MODIFIED
default_mimetype = "text/html"
default_charset = "UTF-8"
include_path = ".:/var/appweb/htdocs/phpincludes:/usr/lib64/php/modules" ; PAN-MODIFIED
extension_dir = "/usr/lib64/php/modules" ; PAN-MODIFIED - likely not needed. it is already working
  • auto_prepend_file:在每个PHP脚本开始执行前自动包含进来

  • include_path:设置include()或require()函数包含文件的参考路径

  • extension_dir:php 扩展目录

1
/var/appweb/htdocs/index.php

image-20250515164804368

image-20250515164831574

1
request => nginx(443) => apache(28250) => php(php.ini,index.php) => envSetup.php => target.php

打包 web 目录分析。

1
tar -czvf htdocs.tar.gz /var/appweb/htdocs/

nginx

1
ps aux | grep nginx

image-20250516181804420

1
tar -czvf nginx.tar.gz /etc/nginx

nginx.conf:

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

http {
# 定义上游服务器, request => nginx => backend_mgmt
upstream backend_mgmt {
server 127.0.0.1:28250;
}
# 上游服务器返回的跳转处理: 28250 => Location http://xxx:28250/php/login.php => Location http://xxx:80/php/login.php(nginx)
proxy_redirect http://$host:28250/ $scheme://$host:$server_port/;
proxy_redirect http://$proxy_host:28250/ $scheme://$host:$server_port/;



# https server
server {
listen [::]:443 ssl default_server ipv6only=off;
listen [::]:4443 ssl ipv6only=off;

# $gohost 表示上游服务
set $gohost "backend_mgmt";
set $devonly 0;
set $gohostExt "";

set $wf_pub "wildfire.paloaltonetworks.com";
set $wf_pri "";
# 包含了一个子配置
include conf/locations.conf;
}

# http server
server {
listen [::]:80 ipv6only=off;

# include location rules
set $gohost "backend_mgmt";
set $devonly 0;
set $gohostExt "";

set $wf_pub "wildfire.paloaltonetworks.com";
set $wf_pri "";

include conf/locations.conf;
}
}

conf/locations.conf

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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# 允许的请求方法
add_header Allow "GET, HEAD, POST, PUT, DELETE, OPTIONS";
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|OPTIONS)$) {
return 405;
}

# rewrite_log on;

# static ones
# 统计
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}

# Chrome cache large source map making them out of date.
# 解决 Chrome 缓存大型 source map 文件导致它们过时的问题
location ~ \.js\.map$ {
add_header Cache-Control "no-cache; no-store"; # 不缓存不存储
proxy_pass_header Authorization; # 是将客户端请求中的 Authorization 头传递给上游服务器,Authorization nginx 是默认会过滤掉的,所以需要显式的设置传输到上游服务器
proxy_pass http://$gohost$gohostExt; # 传输给上游服务
}

# turn on auth check by default
# 默认设置
set $panAuthCheck 'on';

# unauth 的时候不检查认证
if ($uri ~ ^\/unauth\/.+$) {
set $panAuthCheck 'off';
}

if ($uri = /php/logout.php) {
set $panAuthCheck 'off';
}

# new login rules
# 匹配登录的静态文件 images js
location ~ ^/login/(images|js)/ {
# 重写: /login/(images|js)/(.*)$ => /$1/login/$2
# $1 (images|js), $2 (.*)
rewrite /login/(images|js)/(.*)$ /$1/login/$2 break;
# 静态资源根目录
root /var/appweb/htdocs;
# It is directly retrieving static files, we can not do proxy_hide_header, instead should just use add_header
add_header Last-Modified "";
add_header Cache-Control "max-age=86400";
}

location ~ ^/login/(css|fonts)/ {
root /var/appweb/htdocs/styles/;
# It is directly retrieving static files, we can not do proxy_hide_header, instead should just use add_header
add_header Last-Modified "";
add_header Cache-Control "max-age=86400";
}

location /php/login.php {
client_max_body_size 1k;
limit_req zone=unauthRateLimit burst=10;
# 登录的时候也是关闭认证
set $panAuthCheck 'off';

proxy_set_header X-Client-Cert $ssl_client_escaped_cert;
# 包含默认的配置( 请求头的配置 )
include conf/proxy_default.conf;
proxy_pass_header Authorization;
proxy_pass http://$gohost$gohostExt;
# Remove Last-modified from proxy header
proxy_hide_header Last-Modified;
}

# SAML related
location /SAML20/SP/ACS {
set $panAuthCheck 'off';
proxy_intercept_errors on;
rewrite /SAML20/SP/ACS /unauth/php/DualLogin.php break;
proxy_set_header X-ORIG-URI /SAML20/SP/ACS;
include conf/proxy_default.conf;
proxy_pass http://$gohost$gohostExt;
}


# not allow json, lock, conf file access
location ~ ^/(vendor|python)([^\/]*)/(.*).(json|lock|conf)$ {
return 404;
}

# redirect `/upload/xxx` to panPhpModule
# upload 交给 @upload_regular 处理
location /upload/ {
error_page 402 =200 @upload_regular;
return 402;
}

# block unauthorized `/upload` access
# this prevents hacker dumping files to the system to fill up disk space.
# 不允许 upload 的访问
location /upload {
return 403;
}

# plugins not going through proxy and backend
location ~ ^/plugins/([^\/]*)/ui/(js|styles|generated|VMSeries_Help|help)/ {
rewrite /plugins/([^\/]*)/ui/(js|styles|generated|VMSeries_Help|help)/(.*)$
/installed/$1/ui/$2/$3
break;
# It is directly retrieving static files, we can not do proxy_hide_header, instead should just use add_header
add_header Last-Modified "";
add_header Cache-Control "max-age=86400";
root /opt/plugins;
}

location /webui/ {
proxy_http_version 1.1;
# ^/webui(\/.*)$ =》 /php-packages/firewall_webui/php/api/index.php$1
# /webui/abc => /php-packages/firewall_webui/php/api/index.php/abc
rewrite ^/webui(\/.*)$ /php-packages/firewall_webui/php/api/index.php$1 break;

# look for upload
# if ($content_type ~* "multipart/form-data") {
# error_page 402 =200 @upload_api;
# }

include conf/proxy_default.conf;
proxy_pass_header Authorization;
proxy_pass_header token;
proxy_pass_header tid;
proxy_pass http://$gohost$gohostExt;
}

# index.php 同样是关闭认证
location /api/index.php {
proxy_http_version 1.1;
proxy_set_header Connection "Close";
set $panAuthCheck 'off';
# $args 是原始请求
# 这里会匹配有 password=的把其中的密码进行替换做加密处理, 这样就不会在日志中泄露了
set $obfuscated_args $args;
if ($obfuscated_args ~ (?i)(.*)(?=password=)password=[^&]*(.*)) {
set $obfuscated_args $1password=****$2;
}
if ($obfuscated_args ~ (?i)(.*)(?=key=)key=[^&]*(.*)) {
set $obfuscated_args $1key=****$2;
}
if ($obfuscated_args ~ (?i)(.*)(?=REST_API_TOKEN=)REST_API_TOKEN=[^&]*(.*)) {
set $obfuscated_args $1REST_API_TOKEN=****$2;
}
# 原始请求的处理
set $obfuscated_request $request;
if ($obfuscated_request ~ (?i)(.*)(?=password=)password=[^&]*(.*)) {
set $obfuscated_request $1password=****$2;
}
if ($obfuscated_request ~ (?i)(.*)(?=key=)key=[^&]*(.*)) {
set $obfuscated_request $1key=****$2;
}
if ($obfuscated_request ~ (?i)(.*)(?=REST_API_TOKEN=)REST_API_TOKEN=[^&]*(.*)) {
set $obfuscated_request $1REST_API_TOKEN=****$2;
}

# rewrite below will add `index.php` to $uri.
# capture the original uri before that happens,
# for api_metric logging.
set $orig_uri $uri;

access_log /var/log/nginx/access.log stripped;
access_log /var/log/nginx/api_metrics.log stripped_metric;

# look for upload
if ($content_type ~* "multipart/form-data") {
error_page 402 =200 @upload_api;
return 402;
}

if ($devonly = 1) {
set $gohostExt $server_port;
}

include conf/proxy_default.conf;
proxy_pass http://$gohost$gohostExt;
# Remove Last-modified from proxy header
proxy_hide_header Last-Modified;
}


location /api {
# do not know why "last" is needed and "break" does not work for api browser
rewrite ^(/api)(\/?)$ $1/index.php last;
rewrite ^(/api)(\/.*)$ $1/index.php$2 last;
}

location /restapi/ {
proxy_http_version 1.1;
proxy_set_header Connection "Close";
set $panAuthCheck 'off';

set $obfuscated_args $args;
if ($obfuscated_args ~ (?i)(.*)(?=password=)password=[^&]*(.*)) {
set $obfuscated_args $1password=****$2;
}
if ($obfuscated_args ~ (?i)(.*)(?=key=)key=[^&]*(.*)) {
set $obfuscated_args $1key=****$2;
}

set $obfuscated_request $request;
if ($obfuscated_request ~ (?i)(.*)(?=password=)password=[^&]*(.*)) {
set $obfuscated_request $1password=****$2;
}
if ($obfuscated_request ~ (?i)(.*)(?=key=)key=[^&]*(.*)) {
set $obfuscated_request $1key=****$2;
}

# rewrite below will add `index.php` to $uri.
# capture the original uri before that happens,
# for api_metric logging.
set $orig_uri $uri;

rewrite ^/restapi(\/.*)$ /php/restapi/index.php$1 break;

access_log /var/log/nginx/access.log obfus_access;
access_log /var/log/nginx/restapi_metrics.log api_metric;

# `wget` POST does not work.
# If not regression, this will be removed
# if ($arg_client = 'wget') {
# error_page 402 =200 @api_wget_file; return 402;
# set $isWgetLoad "w";
# }
# if ($arg_file-name) {
# set $isWgetLoad "${isWgetLoad}f";
# }

# look for upload
if ($content_type ~* "multipart/form-data") {
error_page 402 =200 @upload_api;
return 402;
}

include conf/proxy_default.conf;
proxy_pass http://$gohost$gohostExt;
# Remove Last-modified from proxy header
proxy_hide_header Last-Modified;
}

# route `/restapi-doc` to `/restapi-doc/` as `DirectorySlash off` in httpd
location = /restapi-doc {
if ($args) {
return 302 /restapi-doc/?$args;
}
return 302 /restapi-doc/;
}

# It is added to prevent restapi-doc page using default config
location /restapi-doc/ {
include conf/proxy_default.conf;
proxy_pass http://$gohost$gohostExt;
}

# location @api_wget_file {
# client_body_in_file_only on;
# proxy_set_header X-API-WGET-FILTER-FILE-PATH $request_body_file;
# }

location @upload_api {
upload_pass @back_upload_api;
include conf/upload_default.conf;
}

location @back_upload_api {
include conf/proxy_default.conf;
proxy_pass http://$gohost$gohostExt;
# Remove Last-modified from proxy header
proxy_hide_header Last-Modified;
}

location @upload_regular {
upload_pass @back_upload_regular;
include conf/upload_default.conf;
}

location @back_upload_regular {
proxy_intercept_errors on;
include conf/proxy_default.conf;
proxy_pass http://$gohost$gohostExt;
# Remove Last-modified from proxy header
proxy_hide_header Last-Modified;
}

# Custom error response pages
error_page 400 /error_page/400.html;
error_page 495 /error_page/400.html;
error_page 496 /error_page/400.html;
error_page 497 /error_page/400.html;
error_page 403 /error_page/403.html;
error_page 404 /error_page/404.html;
error_page 500 /error_page/500.html;
error_page 501 /error_page/501.html;
error_page 502 /error_page/502.html;
error_page 503 /error_page/503.html;
error_page 504 /error_page/504.html;

location /error_page/ {
root /var/appweb/htdocs;
# It is directly retrieving static files, we can not do proxy_hide_header, instead should just use add_header
add_header Last-Modified "";
add_header Cache-Control "max-age=86400";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
internal; # 只有通过内部重定向(如 rewrite 或 error_page)才能访问这个路径,而不能直接从外部(客户端)访问。
}

# not allow internal page access
location /phpincludes {
return 404;
}
location /php/include {
return 404;
}
location /lib/worldmap {
return 404;
}

# default catch all
set $addXframe 0;

# route `/debug` to `/debug/` as `DirectorySlash off` in httpd
location = /debug {
return 302 /debug/;
}

location / {
# intercept errors is turned on only for GUI pages,
# so that they are directed to the proper error pages.
proxy_intercept_errors on;
if ($devonly = 1) {
set $gohostExt $server_port;
}
if ($uri = /) {
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection '1; mode=block;';
add_header X-Content-Type-Options 'nosniff';
add_header Strict-Transport-Security 'max-age=31536000';
}
include conf/proxy_default.conf;
proxy_pass_header Authorization;
proxy_pass http://$gohost$gohostExt;
# Remove Last-modified from proxy header
proxy_hide_header Last-Modified;
proxy_cookie_path / "/; HttpOnly; SameSite=Strict";
}

# sample: /wf_report/public|private/...whatever goes to Wildfire
# /wf_report/private/wildfire.paloaltonetworks.com/443/xxx/api/1.0/box/VERSION HTTP/1.0
#
location ~ ^/wf_report/([^\/]*)/([^\/]*)/([^\/]*)/(xxx/)?(.*)$ {
proxy_intercept_errors on;
default_type text/plain;
if ($request_method = OPTIONS) {
add_header Content-Length 0;
add_header Access-Control-Allow-Origin "null";
add_header Access-Control-Allow-Methods "POST,GET,OPTIONS";
add_header Access-Control-Allow-Headers "x-requested-with";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Max-Age 30;
add_header Strict-Transport-Security 'max-age=31536000';
return 200;
}
set $wftype "$1";
set $myproxy "$2";
set $myport "$3";
set $myuri "$5";

rewrite "^/wf_report/(.*)$" /php/monitor/wfAuthorize.php/$myuri break;

# -------------------------
proxy_set_header X-Wf-Server $myproxy;
proxy_set_header X-Wf-Server-Port $myport;
proxy_set_header X-Wf-Server-Type $wftype;
# -------------------------
include conf/proxy_default.conf;
proxy_pass http://$gohost$gohostExt;

add_header Access-Control-Allow-Origin "null";
add_header Access-Control-Allow-Methods "POST,GET";
add_header Access-Control-Allow-Headers "x-requested-with";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Max-Age 30;
add_header Strict-Transport-Security 'max-age=31536000';
# Remove Last-modified from proxy header
proxy_hide_header Last-Modified;
}

location ~ ^/php/monitor/(wfAuthorize|wfProxy).php {
internal;
}

nginx 默认过滤请求头,所以是需要显式的设置,其他的也就会默认的传输:

image-20250518001035357

image-20250518001021340

conf/proxy_default.conf

1
2
3
4
5
6
7
8
9
10
11
# default proxy request header setting
# 设置了一些请求头, 不过必须包含才会起作用, 所以可以看到匹配的规则中设置 $panAuthCheck 了都要包含一下
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Scheme $scheme;
proxy_set_header X-Real-Port $server_port;
proxy_set_header X-Real-Server-IP $server_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-pan-ndpp-mode $pan_ndpp_mode;
proxy_set_header Proxy "";
proxy_set_header X-pan-AuthCheck $panAuthCheck;

nginx 解析规则:

img

学习到了 nginx 的很多知识

而且从上图中也知道了,nginx 中配置的规则就是匹配到了就走这条后面的就不处理了。

整理一下匹配的规则( 按顺序 ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/nginx_status:127.0.0.1
~ \.js\.map$ :整体上是为了处理缓存,不过在 set $panAuthCheck 'on'; 之前就导致了X-pan-AuthCheck可以被自定义
set $panAuthCheck 'on'; 配置默认启用认证检测
^\/unauth\/.+$ 无需认证
/php/logout.php 无需认证
^/login/(images|js)/ login 的静态资源访问,重写规则
^/login/(css|fonts)/ 静态资源 /var/appweb/htdocs/styles/
/php/login.php $panAuthCheck 'off' => conf/proxy_default.conf => 转到上游服务
/SAML20/SP/ACS off /unauth/php/DualLogin.php X-ORIG-URI /SAML20/SP/ACS
/SAML20/SP/SLO
/SAML20/SP/TEST
^/(vendor|python)([^\/]*)/(.*).(json|lock|conf)$ => 404 不允许访问
/upload/ => @upload_regular
/upload => 403
/plugins/([^\/]*)/ui/(js|styles|generated|VMSeries_Help|help)/(.*)$ => /installed/$1/ui/$2/$3 => /opt/plugins
/webui/ => ^/webui(\/.*)$ /php-packages/firewall_webui/php/api/index.php$1
/api/index.php => $args,$request 中敏感信息替换为 *, multipart/form-data => @upload_api
/api => /api/xfdfs => /apifsdfs 的情况重写 $1/index.php$2
/restapi/ => /api/index.php 类似
/restapi-doc => /restapi-doc/
/restapi-doc/ => include conf/proxy_default.conf 认证生效
@upload_api =》 @back_upload_api incinternallude conf/proxy_default.conf 认证生效
/error_page/ =》 xxx

漏洞分析

php.ini 和 index.php 都表明了先加载 envSetup.php:

只有 4 个条件都满足后才会进行认证验证:

  • $_SERVER[‘HTTP_X_PAN_AUTHCHECK’] != ‘off’
  • $_SERVER[‘PHP_SELF’] !== ‘/CA/ocsp’
  • $_SERVER[‘PHP_SELF’] !== ‘/php/login.php’
  • stristr($_SERVER[‘REMOTE_HOST’], ‘127.0.0.1’) === false
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
if (
$_SERVER['HTTP_X_PAN_AUTHCHECK'] != 'off'
&& $_SERVER['PHP_SELF'] !== '/CA/ocsp'
&& $_SERVER['PHP_SELF'] !== '/php/login.php'
&& stristr($_SERVER['REMOTE_HOST'], '127.0.0.1') === false
) {
$_SERVER['PAN_SESSION_READONLY'] = true;
$ws = WebSession::getInstance($ioc);
$ws->start();
$ws->close();
// these are horrible hacks.
// This whole code should be removed and only make available to a few pages: main, debug, etc.
if (
!Str::startsWith($_SERVER['PHP_SELF'], '/php-packages/panorama_webui/php/api/index.php')
&& !Str::startsWith($_SERVER['PHP_SELF'], '/php-packages/firewall_webui/php/api/index.php')
) {
if (Backend::quickSessionExpiredCheck()) {
if (isset($_SERVER['QUERY_STRING'])) {
Util::login($_SERVER['QUERY_STRING']);
} else {
Util::login();
}
exit(1);
}
}
}

image-20250516131149267

HTTP_X_PAN_AUTHCHECK => Req Header X-PAN-AUTHCHECK

这里请求的是 Apache 的 WEB 服务:

1
2
curl http://127.0.0.1:28250/php/ztp_gate.php -v
curl http://127.0.0.1:28250/php/ztp_gate.php -v -H "X-PAN-AUTHCHECK: off"

image-20250516133116566

可以看到 X-PAN-AUTHCHECK 是可以有效绕过的。

但是放在 80,443 这些 nginx 的服务端口就不行了。

image-20250516133249006

这个原因其实在从上面的 nginx 配置分析就可以看出来。除过几个特定的路径设置了 $panAuthCheck ‘off’ 也就是 proxy_default.conf 中的 X-pan-AuthCheck $panAuthCheck 其他的路由在刚开始就被默认配置为 on 了:

  • ^/unauth/.+$
  • /php/logout.php
  • /php/login.php
  • /SAML20/SP/ACS
  • xxxx

不过很明显的是存在漏网之鱼的:

image-20250518005025252

而自定义的 X-PAN-AUTHCHECK 并不在 nginx 默认的过滤范围之内( Authorization、下划线…)那么也就是说,客户端自定义的请求头是可以被传递到 apache 的,也就是造成了身份认证绕过。( 下面那个 /unauth 路由的也是,还有个认证绕过的漏洞,这里不讲 )

image-20250518005410367

然后还有一个问题,为什么访问 /php/ztp_gate.php/.js.map 会解析 /php/ztp_gate.php 呢?

没有找到什么关于的配置,本地测试 apache,nginx 都是可以这样解析的 .php/随机字符串

image-20250518011157928

学到了:

image-20250518011349251

可以看到解析是始终是 php 文件

image-20250518011529083

这也解释了为什么会有这种构造了:

1
^/webui(\/.*)$ /php-packages/firewall_webui/php/api/index.php$1

参考链接


CVE-2024-0012 Palo Alto 认证绕过漏洞分析
https://liancccc.github.io/2025/05/06/技术/漏洞分析/CVE-2024-0012/
作者
守心
发布于
2025年5月6日
许可协议