Multiple Vulnerabilities in Proxmox VE & Proxmox Mail Gateway
Background
Proxmox Virtual Environment (Proxmox VE or PVE) 是一个开源类型1 hypervisor。它包含一个基于Web的管理界面,使用Perl编写。另一个Proxmox产品Proxmox Mail Gateway (PMG) 同样使用Perl编写,并具有类似的Web管理界面。它们共享部分代码库。
在本文中,我将逐步介绍如何调试PVE的Web服务,并分析在PVE和PMG中发现的三个漏洞。
[更新] 这是对本文的快速小更新。MITRE于2022年12月9日回复我们,为剩余的两个漏洞分配了CVE-2022-35507和CVE-2022-35508。
非常感谢MITRE的回复。
Locating the source code
PVE是一个基于Debian的Linux发行版。ISO安装程序可在其网站获取。请注意,如果您想复现本文中的任何漏洞,请使用2022年5月4日更新的“Proxmox VE 7.2 ISO Installer”,该版本除非手动运行apt update,否则不包含补丁。
在默认安装中,Web服务应监听端口8006。
通过几个命令,不难发现Web服务的脚本位于/usr/share/perl5/:
|
|
Setting up debug environment
我选择IntelliJ IDEA及其Perl插件进行调试。以下是设置步骤:
In IDEA
- 在PVE服务器上打包
/usr/share/perl5/,并在IDEA中作为项目打开 - 转到Settings > Plugins并安装Perl插件
- 转到Settings > Languages & Frameworks > Perl5,
- 选择一个Perl5解释器(本地或Docker均可),然后
- 设置Target version为v5.32,与PVE使用的
perl --version相同
- 在Project窗口(Alt+1)中,右键单击perl5目录,Mark Directory as > Perl5 Library Root。
此时,您应该在IDEA中拥有正确的语法高亮和依赖解析。
- 转到Run > Edit Configurations,添加一个新的“Perl Remote Debugging”条目并保存:
- Name: PVE remote
- Remote project root: /usr/share/perl5
- Connection mode: IDE connects to the perl process
- Server host: 您的PVE服务器IP
- Server port: 12345
On the PVE server
运行以下命令安装所需的调试工具:
|
|
全部设置完成。要启动调试会话,在IDEA中点击Run > Debug ‘PVE remote’,并在服务器上运行PERL5_DEBUG_HOST=<your PVE server IP> PERL5_DEBUG_PORT=12345 PERL5_DEBUG_ROLE=server perl -T -d:Camelcadedb /usr/bin/pveproxy start -debug。如果一切顺利,调试器应默认在SSL.pm的第330行中断,如下图所示。
Bug 0x01: Post-auth XSS in API inspector
通过登录Web界面,可以观察到许多请求发送到路径/api2/json/下的端点。通常,/api后的json表示响应数据的格式,服务器可能支持多种格式用于不同目的。例如,xml可能用于RPC调用,jsonp用于跨域<script>标签,或html用于设置innerHTML。在PVE中,如果我们将json改为html,服务器将返回一个包含json结果的“API Inspector”页面:
进一步测试显示,服务器未正确转义用户输入。如果我们访问一个不存在的API端点,请求路径将反映在<a>标签的href属性中。因此,攻击者可以注入HTML标签以实现反射型跨站脚本(XSS)。
Further Analysis
函数handle_request位于perl5/PVE/APIServer/AnyEvent.pm第1100行,是我们的入口点。如果请求路径以/api2开头,它将把请求传递给函数handle_api2_request。
步入handle_api2_request,我们可以看到在第865行,变量$rel_uri和$format通过正则表达式从请求路径的其余部分提取。然后调用函数PVE::APIServer::Formatter::get_formatter来获取用于生成响应的“格式化器”。
稍后,在第946行调用$formatter。在生成导航栏的“面包屑”HTML时,请求路径直接连接到<a>标签的href属性。
Impacts, attack conditions & constraints
由于认证cookie PVEAuthCookie设置了Session属性,成功利用需要受害者在同一浏览器会话中登录Web界面,然后访问恶意链接。
攻击者可以通过执行恶意JavaScript代码访问Web界面中的每个功能。其中一个功能是执行shell命令。以下视频演示了一个可能的攻击场景。在视频中,受害者登录了PVE Web UI,然后访问了一个链接。在攻击者的机器上生成了PVE主机的反向shell。
Patch
此漏洞在pve-http-server版本4.1-2中通过将用户输入编码为HTML实体来修复。
Bug 0x02: CRLF injection in response headers
在处理HTTP请求时,如果有任何错误,PVE服务器将在响应的状态行中写入错误消息。
相应的代码位于perl5/PVE/APIServer/AnyEvent.pm:
|
|
在[1]处,服务器使用正则表达式匹配错误消息的第一行,试图避免额外行在[2]处破坏HTTP响应。然而,此方法仅防止LF(%0a)。在基于Chromium的浏览器中,仍然可以使用CR(%0d)注入响应头。
这是在Burp Suite中的响应样子:
Impacts, attack conditions & constraints
在测试时,使用CR(%0d)注入响应头仅在基于Chromium的浏览器(Chrome、MS Edge、Opera等)中有效,且仅使用CR(%0d)无法注入到响应体中。Firefox在没有LF(%0a)的情况下不识别CR(%0d)作为有效的换行指示符。
PVE中的这个漏洞起初可能看起来完全无害。不幸的是,在AnyEvent.pm第1327行,有一个对传入HTTP请求的长度限制检查。如果请求头超过8192字节,服务器将拒绝处理HTTP请求。
|
|
因此,攻击者可以制作一个恶意网页,在受害者的PVE域上多次设置长cookie。一旦受害者访问恶意网页,后续对PVE域的HTTP请求将携带非常长的cookie头,从而被服务器拒绝。
以下视频演示了此客户端DoS漏洞。在视频中,受害者最初能够使用PVE Web UI。访问恶意链接后,受害者无法再访问Web UI,直到清除cookie。
需要注意的是,Chrome默认允许第三方cookie。这是利用此客户端DoS漏洞的必要条件,因为我们是从攻击者的域设置cookie到受害者的PVE域。然而,如果受害者在浏览器设置中更改了cookie策略为“阻止第三方cookie”或“阻止所有cookie(不推荐)”,此攻击将无效。
Patch
此漏洞在pve-http-server版本4.1-3中通过添加对\r\n的额外检查来修复。
Bug 0x03: Post-auth SSRF + LFI + Privilege Escalation
SSRF
PVE服务器可以作为独立节点运行或加入集群以与其他节点连接。这种设计自然允许节点之间交换信息。例如,API /api2/json/nodes/{node_name}/status用于按名称查询集群中节点的状态。它也可以用于查询节点本身。
如果我们将node_name更改为不存在的值“test”,我们将看到此错误消息:HTTP/1.1 500 hostname lookup 'test' failed - failed to get address info for: test: No address associated with hostname。似乎服务器正在尝试对给定的node_name执行DNS查找。使用Burp Collaborator的快速测试验证了我们的猜测:
通过步进调试,我们能够定位到AnyEvent.pm中的proxy_request的相应代码。事实证明,服务器将node_name解析为IP地址,然后将我们的HTTP请求中继到https://{IP}:8006/api2/json/nodes/{node_name}/status。
我们可能想尝试的是设置我们自己的HTTPS服务器监听端口8006,并带有有效的SSL证书,观察中继的请求是否可以进入。虽然这样不行,因为在发出请求之前执行了多个检查,其中一个检查期望为集群中的每个节点找到/etc/pve/nodes/{node_name}/pve-ssl.pem。无论我们输入自己的域名还是IP地址,服务器都找不到证书文件,因为node_name不指向集群中的任何真实节点。因此,在TLS握手期间抛出错误“HTTP/1.1 596 tls_process_server_certificate: certificate verify failed”并停止在那里。
我们注意到的另一件事是,在构建$target URL时,$uri被附加到端口(上图中的第699、703和705行)。开发人员可能假设$uri总是以斜杠(/)开头。虽然这不是真的,因为我们发现可以用其URL编码形式%2F替换斜杠(/)而不破坏请求解析器。
我们尝试通过使用at符号(@)将URL的起始部分变为用户信息并附加我们自己的域,但其中一个健全性检查再次阻止了我们。经过几次尝试,我们设法找到了一个合适的API来利用此SSRF漏洞:GET /api2/json/nodes/{node_name}/tasks/{upid}/log。此API接受任何字符串作为upid,这意味着我们可以将node_name设置为有效节点,以免因证书问题失败。然后我们使用URL编码的斜杠和@来控制主机名。
PVE中没有任何权限的认证用户能够执行此SSRF攻击。由于PVE和PMG之间共享大量代码库,PMG中仅具有低权限“Help Desk”角色或“Audit”角色的认证用户也可以使用API /api2/html/nodes/{node_name}/pbs/{remote}/snapshot/利用此SSRF漏洞。
Arbitrary file read
在http_request的回调函数内部,服务器在响应头中查找pvestreamfile头(第778行)并将其值提取到变量$stream中。$stream随后传递给sysopen,服务器将文件的内容作为响应体返回。
此漏洞代码也存在于PMG中。攻击者可以利用前面介绍的SSRF漏洞,仅凭PVE中的非特权账户或PMG中的低权限账户读取PVE/PMG服务器上的任意文件。sysopen在进程“pve(pmg)proxy worker”中以uid=33(www-data)调用。
Privilege escalation in PMG via unsecured backup file
凭借读取任意文件的能力,黑客可能对存储在服务器上的凭据和密钥特别感兴趣。我们决定深入研究认证过程的实现,以查看服务器是否在数据库或配置文件中存储任何内容,无论是明文还是由某些“密钥”加密。
PVE/PMG中的认证通过使用RSA/SHA-1签名和验证字符串来实现。成功登录后,服务器将为客户端签名一个“ticket”,即“PVEAuthCookie”或“PMGAuthCookie”。以下是ticket的示例:
|
|
双冒号分隔明文和签名。明文的格式是PVE:{username}@{realm}:{hex(timestamp)}。而签名是使用存储在/etc/pve/priv/authkey.key(对于PVE)或/etc/pmg/pmg-authkey.key(对于PMG)的私钥生成的,只有root用户具有对这些文件的读写权限。
|
|
然而,事实证明,如果PMG中的备份功能曾经被使用过,备份文件将包含authkey。更重要的是,它对www-data用户可读:
|
|
备份文件的路径可以从任务日志中提取,该日志也对www-data用户可访问。结合上述所有漏洞,攻击者可以伪造一个ticket,以实现从低权限“Help Desk”角色或“Audit”角色到“root@pam”的权限提升,从而在PMG中获得完全访问权限。
Proof-of-concept
我们在下面附加了python脚本和一个演示此漏洞利用的视频。在视频中,攻击者以“Help Desk”用户身份登录PMG Web UI,由于权限低,无法更改当前用户的角色。运行漏洞利用后,生成了一个伪造的ticket,攻击者以“root@pam”用户身份获得了Web UI的访问权限。
|
|