Sploitus

Exploit for CVE-2026-48710

kitploit · 2026-08-25

Exploit Code

MARKDOWN86 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-CUTEECAT-CVE-2026-48710
# CVE-2026-48710

#Принцип:  
Starlette — это легковесный Python ASGI-фреймворк с открытым исходным кодом.  
Причина в том, что при обработке структуры данных url фреймворк Starlette доверяет значению host, отправленному клиентом, и напрямую подставляет его в переменную url для последующей проверки прав доступа.

Исходный url обрабатывается в uvicorn\protocols\http\httptools_impl.py  
def on_message_begin(self) -> None: self.url = b"" self.expect_100_continue = False self.headers = [] self.scope = { "type": "http", "asgi": {"version": self.asgi_version, "spec_version": "2.3"}, "http_version": "1.1", "server": self.server, "client": self.client, "scheme": self.scheme,  
"root_path": self.root_path, "headers": self.headers, "state": self.app_state.copy(), }

root@kitploit:~
    
    
    # Parser callbacks
    def on_url(self, url: bytes) -> None:
        self.url += url #self.url设置为原始url
    
    def on_header(self, name: bytes, value: bytes) -> None:
        name = name.lower()
        if name == b"expect" and value.lower() == b"100-continue":
            self.expect_100_continue = True
        self.headers.append((name, value))
    
    def on_headers_complete(self) -> None:
        http_version = self.parser.get_http_version()
        method = self.parser.get_method()
        self.scope["method"] = method.decode("ascii")
        if http_version != "1.1":
            self.scope["http_version"] = http_version
        if self.parser.should_upgrade() and self._should_upgrade():
            return
        parsed_url = httptools.parse_url(self.url)#使用httptools拆分原始url
        raw_path = parsed_url.path  #raw_path设置为原始url拆分出的path(访问的path)
        path = raw_path.decode("ascii")  #path设置为使用ascii解码以后的原始path值
        if "%" in path:  #处理url解码以后的中文从重编码
            path = urllib.parse.unquote(path)
        full_path = self.root_path + path
        full_raw_path = self.root_path.encode("ascii") + raw_path
        self.scope["path"] = full_path
        self.scope["raw_path"] = full_raw_path
        self.scope["query_string"] = parsed_url.query or b""
    

(комментарии набраны мной вручную)

Этот код находится внутри метода класса URL:

Код: \starlette\datastructures.py

host_header = None for key, value in scope["headers"]: if key == b"host": host_header = value.decode("latin-1") break if host_header is not None: url = f"{scheme}://{host_header}{path}" #基于用户传入的请求头二次定义url

В starlette\routing.py:

root@kitploit:~
    
    
        route_path = get_route_path(scope)
        if scope["type"] == "http" and self.redirect_slashes and route_path != "/":
            redirect_scope = dict(scope)
            if route_path.endswith("/"):
                redirect_scope["path"] = redirect_scope["path"].rstrip("/")
            else:
                redirect_scope["path"] = redirect_scope["path"] + "/"
    
            for route in self.routes:
                match, child_scope = route.matches(redirect_scope)
                if match != Match.NONE:
                    redirect_url = URL(scope=redirect_scope)   #调用URL方法
                    response = RedirectResponse(url=str(redirect_url))
                    await response(scope, receive, send)
                    return
    

В результате при отправке на конечную точку, требующую аутентификации, некорректного запроса с заголовком host, на уровне маршрутизации url повторно определяется как конечная точка, не требующая аутентификации.  
Например, в этом url http://127.0.0.1:9999/admin  
admin — это конечная точка, требующая аутентификации.  
Отправляется пакет:  
url = http://127.0.0.1:9999/admin  
header{  
host = 123?  
}  
После повторного определения url становится http://123?/admin  
Затем url разбирается с помощью функции urlsplit  
Результат: SplitResult(scheme='http', netloc='123', path='', query='/admin', fragment='')  
Так как path='', последующая аутентификация считает, что пользователь обращается к корневому каталогу сайта, поэтому запрос пропускается напрямую, хотя на самом деле возвращается содержимое каталога admin.