安全攻防
evening
我写了一个假的 6335,Qdrant 把向量数据推了进去
2026-11-04
1 阅读
约9分钟阅读
先知社区
字号:
复现环境:Ubuntu 24.04.4 LTS,Docker 29.1.3。三个官方镜像 qdrant/qdrant v1.19.1、v1.19.2、v1.20.0,加一个 fullstorydev/grpcurl 当探针。集群跑在专用 docker 网络 172.31.0.0/24 里,节点 A 是 172.31.0.2、节点 B 是 172.31.0.3,6333 和 6335 都不映射到宿主机。 写在前面 这篇的重点是一个自己写的假节点被加成集群成员之后,真实的向量数据被推了过去。走到那一步经过的是 Qdrant 分布式集群里那个没人看的 6335 端口:两轮补丁都在收紧"谁能进门",还留了一处没配 key 就全开的盲区。 版本和时间是现查的。两条 advisory 都没分配 CVE,NVD 上搜不到,只有 GitHub 的 Security Advisory 页面有原文:GHSA-3gph-6c29-p29v 影响 1.18.0 到 1.19.1,修在 1.19.2(10 月 5 日);GHSA-frc7-44gm-99qq 影响 1.20.0 以前的全部版本,修在 1.20.0(10 月 22 日)。 验证是四组对照:1.19.1、1.19.2 各配一次 key,1.20.0 配 key 但不设 enforce,再跑一组什么都不配。区别只在这几个开关上,其余步骤一样;环境怎么搭写在第一节。 攻击者(同网段的容器 / 被拿下的跳板机) | | 直接连 172.31.0.2:6335,不带任何凭证 v +------------------+ +------------------+ | 节点 A :6335 |<------>| 节点 B :6335 | | 内部 gRPC | Raft | | | 只读 key 能改拓扑 | | | +------------------+ +------------------+ ^ | AddPeerToKnown(id=99, uri=172.31.0.9) | 集群把 172.31.0.9 当成合法成员,持续往那儿发 Raft 消息 往下走是先搭环境、按版本逐个做对照,然后回源码找原因、列还没关的面、给检测方法,收在能确认的边界上。 一、先把集群搭起来 为什么盯上 6335 Qdrant 开单机的时候不会启动 6335 端口。只有把 cluster.enabled 打开,节点之间才会在 6335 上跑一套内部的 gRPC,用来同步 Raft 状态、转移分片、交换遥测数据。很多人部署向量库就是 docker run -p 6333:6333 -p 6334:6334 ,压根没开集群,所以这个端口在大部分教程里是不存在的。 但只要上了生产集群,情况就不一样了。Qdrant 的默认配置把内部监听绑在 service.host 上,而 service.host 默认是 0.0.0.0 。能访问 6333 的机器,通常也能访问 6335。这个默认值本身不奇怪,问题在于 6335 上的服务要的是"集群成员"这个身份,而它判断身份的方式,在 1.20.0 之前一直有缝。 三个版本各拉一份 这次要看的补丁横跨三个版本。编号和修复版本前面提过,这里补一句覆盖范围:第二条 advisory 管的是 1.20.0 以前的全部版本,所以在它公开之前,1.19.2 虽然收住了第一条,仍然在第二条的射程里。1.20.0 之前没有任何一版同时躲过这两条。 我把三个版本的镜像都拉下来: docker pull qdrant/qdrant:v1.19.1 docker pull qdrant/qdrant:v1.19.2 docker pull qdrant/qdrant:v1.20.0 官方的镜像有一个坑。它的 Entrypoint 是空的, Cmd 写的是 ./entrypoint.sh ,工作目录在 /qdrant 。所以你不能照官方文档那样写 docker run qdrant/qdrant --uri http://... ,那样 docker 会把 --uri 当成要执行的程序,直接报 exec: "--uri": executable file not found in $PATH 。正确的写法是把启动脚本带上: docker run -d --name qa qdrant/qdrant:v1.19.1 ./entrypoint.sh --uri http://172.31.0.2:6335 这个错误我踩了两次,第一次还以为是镜像不完整,翻了下 docker inspect 才看出来。 把 6335 关在自己的网段里 验证不能碰任何外部目标,所以我建了一个专用的 docker 网络: docker network create qdrant-lab --subnet 172.31.0.0/24 节点 A 用 172.31.0.2,节点 B 用 172.31.0.3。两个节点的 6333 和 6335 都不往宿主机映射,只在这个网段里通。然后我再起一个 grpcurl 容器,也接进这个网络,扮演"能访问 6335、但不是集群成员"的主机。这个角色对应同一台宿主机上的另一个容器、同 VPC 里被拿下的跳板机,或者一个配错安全组的邻居。 集群用这条命令组起来: node A: --uri http://172.31.0.2:6335 node B: --bootstrap http://172.31.0.2:6335 两边都设 QDRANT__CLUSTER__ENABLED=true 和 QDRANT__CLUSTER__P2P__PORT=6335 。等十几秒, /cluster 返回 status: enabled ,两个 peer 都在列表里,集群就算起来了。 两个让我重跑的地方 第一个坑是 --uri 的端口。我按直觉写成 --uri http://172.31.0.2:6333 ,觉得反正都是本机。这个地址是给其他节点回连用的,必须指向 p2p 端口:写成 6333 节点能起来,但集群永远选不出 leader,B 那边 bootstrap 一直失败。改成 6335 就正常了。 第二个坑是启动顺序。两个节点同时起来时,B 会在 A 还没选出 leader 就去握手,日志里开始刷 Failed to send message ... transport error ,重试很多次也不一定成功。A 的日志会提示 leader 大约 4 秒后建立,等它出来再起 B,一次就成。脚本里那个 12 秒的 sleep 是重跑三次之后加的。 这两个坑跟漏洞无关,但直接影响你能不能复现后面的现象。第一次搭的时候我看到一堆 transport error,还以为是认证配错了。 二、1.19.1:只读 key 改了我的集群 匿名请求确实被拦了 第一组我按"最谨慎的运维"来配:管理 key、只读 key、 enforce_internal_auth 全开。 QDRANT__SERVICE__API_KEY=admin-key-123 QDRANT__SERVICE__READ_
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱