Vaultwarden 1.37.3 默认 X-Real-IP trusted-local 在 Docker NAT 环境下绕过登录速率限制
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · vaultwarden-ip-header-spoof
Vaultwarden 1.37.3 - dani-garcia
默认的 IP_HEADER=X-Real-IP 加上 IP_HEADER_TRUSTED_PROXIES=local 会把任何非全局 TCP 对端当作反向代理。原版 Docker 发布端口 NAT 属于 RFC1918。客户端提供的 X-Real-IP 会成为限速键。唯一值永远不会共享同一个突发配额。
| ID | 暂无 CVE |
| CWE | CWE-307 / CWE-290 |
| CVSS | 高危:7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| 产品 | Vaultwarden |
| 受影响版本 | 1.37.3(eb212e23)。镜像 vaultwarden/server:1.37.3。Docker 发布端口 / userland-proxy / Desktop NAT,或任何 RFC1918 对端。 |
| 认证 | 无需认证 |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1 |
攻击者移除了基于 IP 的暴力破解防护。他们在每次尝试时发送一个新的 X-Real-IP。限速器是一个以该地址为键的 DashMap,因此突发和周期永远不会触发。
POST /identity/connect/token 密码授权。默认突发为每个 IP 每 60 秒 10 次。唯一的头部会重置计数器。/api/two-factor/send-email-login、POST /admin,以及未认证的 hint / prelogin / delete-recover / auth-request / Send access 全部以 ClientIp 为键。docker run -p 80:80 和 compose 80:80 使对端成为桥接或 Desktop NAT 地址。trusted=local 即 !is_global(peer)。这正好匹配。一个会用真实客户端替换 X-Real-IP 的反向代理,以及一个对端为全局地址的主机绑定,都能让限速器保持诚实。漏洞在于默认配置加上一个没有覆盖的发布端口。
在 1.35.x / 1.36.0 / 1.37.0 的 GHSA 之后,我锁定了 dani-garcia/vaultwarden 1.37.3(eb212e23)。GHSA-c5rv 将 send-email-login 放到了 LIMITER_LOGIN 上。遗留问题在于 ClientIp 是如何被选择的。
config.rs 将 ip_header 默认为 X-Real-IP,将 ip_header_trusted_proxies 默认为 local。auth.rs 的 ip_header_is_trusted 将 local 视为 !is_global(remote)。ratelimit.rs 将 LIMITER_LOGIN 以该 IpAddr 为键。Docker 桥接、Desktop VM NAT 和回环都是非全局的。
我在回环 127.0.0.1:18170:80 上启动了原版 vaultwarden/server:1.37.3,没有覆盖代理,并在 18171 上启动了第二个实例,设置 IP_HEADER=none。注册了 [email protected]。使用错误密码且没有头部的密码授权在第 11 次请求时返回 429(突发 10)。同样的授权,每次尝试使用唯一的 X-Real-IP: 198.51.100.{n},十四次都保持 HTTP 400,零次 429。在 none 实例上,唯一的 203.0.113.{n} 仍然在第 11 次返回 429。
SUCCESS VAULTWARDEN-IP-HEADER-SPOOF nohdr-429-at=11 spoof-400=14 spoof-429=0 none-429-at=11 VAULTWARDEN-IP-HEADER-SPOOF-WITNESS
已记录的弯路:1.37.3 镜像不在本地,所以第一次 compose 等待拉取;第一次 /alive 在 Rocket 绑定时返回空回复,然后在第 2 次等待时返回 200;虚拟注册 JSON(masterPasswordHash + 一个短 key)就足够了,无需长密钥重试。一个反向 shell。做戏。判定标准是:没有头部时返回 429,使用唯一头部时返回 400。
cd lab
./run.sh
目标仅限 http://127.0.0.1:18170(默认)和 http://127.0.0.1:18171(IP_HEADER=none)。镜像 vaultwarden/server:1.37.3。
将 IP_HEADER 默认为 none,或将 IP_HEADER_TRUSTED_PROXIES 默认为空列表 / 显式代理 CIDR。将 local 视为“任何 RFC1918 对端”正是发布端口的情况。转发头的存在并不证明有代理。
src/config.rs(ip_header、ip_header_trusted_proxies)src/auth.rs(ip_header_is_trusted、ClientIp)src/ratelimit.rs