深入剖析SSRF过滤绕过:Hemmelig应用CVE-2025-69206漏洞技术解析

本文详细分析了Hemmelig应用中一个严重的SSRF过滤器绕过漏洞CVE-2025-69206的技术细节。该漏洞允许攻击者通过DNS重绑定和开放重定向服务,绕过应用对内部IP地址的检查,从而发起服务器端请求伪造攻击。文章包含具体的代码分析、攻击原理、漏洞复现步骤以及完整的修复方案。

漏洞概述:SSRF过滤器绕过漏洞

在Hemmelig应用的“Secret Requests”功能中,发现了一个服务器端请求伪造(SSRF)过滤器绕过漏洞,该漏洞被分配了编号CVE-2025-69206。这个漏洞存在于应用对Webhook URL的验证逻辑中,使得已经通过身份验证的用户能够利用DNS重绑定或开放重定向服务,绕过应用对内部/私有IP地址的拦截机制,从而让服务器向内部网络资源发起HTTP请求。

核心问题 在于应用的验证逻辑仅对URL的主机名字符串进行模式匹配,而没有实际解析域名并检查其最终指向的IP地址是否属于受保护的私有地址范围。在7.3.3之前的版本中,此漏洞处于未修复状态。

技术细节:漏洞成因分析

脆弱的验证函数

漏洞的核心位于/api/lib/utils.ts文件中的isPublicUrl函数。这个函数的设计初衷是检查用户输入的Webhook URL是否指向公共地址,而非内部资源。以下是该函数存在缺陷的原始代码片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const isPublicUrl = (url: string): boolean => {
    const parsed = new URL(url);
    const hostname = parsed.hostname.toLowerCase();
    
    const blockedPatterns = [
        /^localhost$/,
        /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
        /^192\.168\.\d{1,3}\.\d{1,3}$/,
        // ... 其他模式
    ];
    
    return !blockedPatterns.some((pattern) => pattern.test(hostname));
};

两种主要的绕过方式

1. DNS重绑定绕过

验证函数仅检查主机名字符串是否匹配预定义的黑名单模式(如localhost127.0.0.1等)。这意味着,攻击者可以使用localtest.me这样的域名,该域名在DNS层面最终会解析到127.0.0.1,但由于其主机名字符串localtest.me本身不在黑名单模式中,因此能轻松通过验证。

2. 开放重定向绕过

攻击者可以提供一个外部公共域名(如httpbin.org)的URL,该URL指向一个会重定向到内部地址的端点(例如 httpbin.org/redirect-to?url=http://127.0.0.1)。由于初始URL的主机名httpbin.org是公共域名,能通过验证。而当服务器后续执行HTTP请求并跟随重定向时,就会访问到被禁止的内部地址。

漏洞复现与影响评估

攻击步骤演示

为了证明此漏洞的可利用性,可以按照以下步骤进行概念验证:

  1. 设置监听器:在运行Hemmelig应用的容器上,使用Node.js启动一个临时端口监听,用以接收来自服务器的请求。命令示例如下:
    1
    
    node -e "require('http').createServer((req,res)=>{console.log(req.method,req.url,req.headers);res.end('ok')}).listen(8080,()=>console.log('Listening on 8080'))"
    
  2. 利用漏洞:以普通用户身份登录,进入“Secret Requests”功能创建新请求。在Webhook URL输入框中,可以尝试以下两种有效载荷之一:
    • 使用域名重定向http://localtest.me:8080
    • 使用httpbin执行重定向httpbin.org/redirect-to?url=http://127.0.0.1:8080
  3. 触发请求:在另一个浏览器标签页中确认请求并创建秘密信息。点击保存后,之前设置的监听端口(本例中的8080端口)就会收到来自Hemmelig服务器发起的请求。

实际影响与局限性

虽然攻击者能够完全绕过SSRF过滤器,但此漏洞的实际影响是有限的,因为它属于盲SSRF(Blind SSRF)类型。服务器在发起请求后,并不会将目标的响应内容返回给攻击者,因此攻击者无法直接读取内部服务的响应数据。

尽管如此,攻击者仍然可以利用响应时间差等技术手段,来探测目标网络内部特定端口是否开放,从而获取有价值的情报。

修复方案与安全建议

修复后的安全验证函数

要彻底修复此漏洞,必须将基于主机名的静态字符串匹配,改为基于实际解析出的IP地址的动态检查。以下是推荐的修复后代码:

 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
31
32
33
34
35
36
37
38
import { isIP } from 'is-ip';
import dns from 'dns/promises';

export const isPublicUrl = async (url: string): Promise<boolean> => {
    const parsed = new URL(url);
    const hostname = parsed.hostname;
    
    // 将主机名解析为IP地址
    let addresses: string[];
    try {
        if (isIP(hostname)) {
            addresses = [hostname];
        } else {
            addresses = await dns.resolve4(hostname).catch(() => []);
            const ipv6 = await dns.resolve6(hostname).catch(() => []);
            addresses = [...addresses, ...ipv6];
        }
    } catch {
        return false;
    }
    
    // 将解析出的所有IP地址与私有地址段黑名单进行比对
    const privateRanges = [
        /^127\./,        // IPv4回环地址
        /^10\./,         // A类私有地址
        /^192\.168\./,   // C类私有地址
        /^172\.(1[6-9]|2\d|3[0-1])\./, // B类私有地址
        /^169\.254\./,   // 链路本地地址
        /^::1$/,         // IPv6回环地址
        /^fe80:/i,       // IPv6链路本地地址
        /^fc00:/i,       // IPv6唯一本地地址(ULA)
        /^fd/i,          // IPv6 ULA(另一种格式)
    ];
    
    return addresses.length > 0 && !addresses.some(ip => 
        privateRanges.some(pattern => pattern.test(ip))
    );
};

额外的安全加固措施

除了核心的IP解析检查外,还应采取以下措施以增强防御:

  1. 禁用重定向跟随:在用于执行Webhook请求的fetch调用中,明确配置不自动跟随HTTP重定向。
  2. 逐跳验证:如果业务上必须允许重定向,则应在服务器处理HTTP响应的每个重定向步骤后,重新对新获得的URL执行完整的isPublicUrl验证。
  3. 更新依赖:所有使用Hemmelig的用户应立即升级到7.3.3或更高版本,该版本已包含针对此漏洞的补丁。

参考信息

  • 漏洞编号:CVE-2025-69206
  • GitHub安全公告:GHSA-vvxf-wj5w-6gj5
  • 官方补丁提交:HemmeligOrg/Hemmelig.app@6c909e5
  • NVD数据库条目:https://nvd.nist.gov/vuln/detail/CVE-2025-69206
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计