首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >我把 D-Link 的 HNAP 拆开看了一下,9.3 分不是白给的

我把 D-Link 的 HNAP 拆开看了一下,9.3 分不是白给的

原创
作者头像
Lethehong
发布于 2026-11-15 14:13:36
发布于 2026-11-15 14:13:36
170
举报
文章被收录于专栏:网络安全网络安全

一个不需要登录、不需要用户点任何东西、网络可达就能打的栈溢出,评分 9.3,PoC 已经放出来了。这篇我带你从 HNAP 的请求入口一路看到 hnap_main 的栈帧,搞清楚这个洞为什么长在这个位置,以及今天你能做什么。

图 1 本文配图:围绕本文主题的氛围配图

9.3 分的意思是,门槛几乎为零

先把评分翻译成人话。CVSS 4.0 向量里 AV:N 是网络可达,你不需要先摸到内网;AC:L 是攻击复杂度低,条件稳定,不是那种碰运气的竞态;PR:N 是不需要任何身份,匿名请求就能走;UI:N 是受害者什么都不用做,不用点链接,不用开页面。

这四个分量叠在一起,意味着一个公网上的 DIR-600L,只要 HNAP 端口暴露着,攻击者发一个请求就够了。后面的 VC:H、VI:H、VA:H 是机密性、完整性、可用性全高影响,SC:H、SI:H、SA:H 是波及下一跳系统也是全高。

E:P 这个分量容易被忽略。它表示利用代码已经公开,不是理论可行。NVD 的描述里也写了 exploit has been published and may be used。这句话的分量比评分本身更重,因为评分是静态的,利用代码是活的。

但有一个条件在你的环境里不一定成立。这个洞要求 HNAP 处理路径可达。DIR-600L 是家用级路由器,很多部署里管理界面只对内网开放,或者被运营商锁在光猫后面。公网直接暴露 HNAP 的实例才是真正的高危面。

所以我的判断是,9.3 分没虚高,但它描述的是最坏情况。你实际的风险取决于这个设备的 HNAP 入口有没有暴露在不可信网络上。这一点决定了你今天是要连夜处理,还是这周排期。

问题不在解析层,在认证之前就把栈写穿了

公开信息只给到函数名 hnap_main,文件路径 htdocs/cgibin,组件 HNAP Handler。没有给具体的字段名,也没有给触发请求的原文。所以下面关于调用顺序的部分,是我根据 HNAP 协议规范和同类家用路由器固件的通用实现推出来的,不是从这份固件里逐行读出来的,这一点我先说清楚。

HNAP 是 Home Network Administration Protocol,走 HTTP,请求体是 SOAP 风格的 XML,动作名放在 SOAPAction 头里,参数放在 XML 元素里。设备端的 cgibin 拿到请求后,先分发给对应的 handler,hnap_main 就是 HNAP 的总入口。

图 2 本文示意图:HNAP 请求进来后,栈缓冲区在认证之前就被写入,校验层整体后置。

同类实现里,hnap_main 的典型结构是,先从 HTTP 头里取出 SOAPAction,做一次动作名匹配,然后把请求体读进一个固定大小的栈缓冲区,再交给 XML 解析器逐字段取值。问题通常出在第二步和第三步之间。

如果读请求体时用的是类似 strcpy 或者不带长度检查的循环,而缓冲区是在 hnap_main 的栈帧上开的,那么请求体长度超过缓冲区大小时,返回地址就会被覆盖。CWE-121 是栈缓冲区溢出,CWE-119 是更宽泛的缓冲区边界操作不当,这两个分类放在一起,指向的就是这种写法。

推演超长请求体怎么走到栈(示意代码,用伪代码还原读请求体与认证的先后顺序,不构造真实攻击载荷)

代码语言:txt
复制
# 这是伪代码,还原的是同类 HNAP 实现的调用顺序,不是可执行利用

def hnap_main(request):
    action = request.headers.get('SOAPAction', '')
    # 第一步,按动作名分发,此时还没认证
    handler = match_action(action)
    if handler is None:
        return 404
    # 第二步,把请求体读进栈上的定长缓冲区
    buf = bytearray(512)          # 栈上定长,具体大小公开信息未披露
    body = request.body            # 长度不受限
    buf[:len(body)] = body         # 超长即越界,CWE-121
    # 第三步,认证检查排在读之后
    if not check_auth(request):
        return 401
    return handler(buf)

# 只要 body 超过 512 字节,返回地址在认证之前就被覆盖了

关键在于认证的位置。如果认证检查放在读取请求体之后,那么攻击者在通过认证之前就已经把栈写穿了。这就是为什么 PR:N 成立,因为漏洞触发点根本不在认证之后。

我以前看过几款家用路由的 HNAP 实现,认证函数往往是在 handler 内部、解析完动作名之后才调用的。这种顺序在功能上说得通,因为要先知道动作名才能判断这个动作要不要认证。但安全上它是反的,因为请求体在认证之前就已经被处理了。

所以这个洞之所以长在这个位置,根源是入口函数把「读数据」和「判权限」的顺序放反了。不是解析器本身有 bug,是解析器拿到数据的时候,权限判断还没发生。

我推出来的触发链,卡在第三个判断上

我把能确认的调用顺序画成了一条链。第一步,HTTP 请求到达 cgibin,SOAPAction 头被取出。第二步,hnap_main 根据动作名进入对应分支。第三步,请求体被读入栈上的定长缓冲区,这一步没有长度上限。第四步,超长请求体覆盖返回地址,函数返回时控制流被劫持。

这条链里,第三步是缺陷点,但真正让它可利用的是第三步之前没有做长度校验。同类实现里,做得好的会在读之前先看 Content-Length,超过阈值直接返回 413。做得不好的,直接 read 到缓冲区里,缓冲区多大它不管。

我没能确认这份固件里缓冲区的具体大小,也没能确认覆盖返回地址需要多少字节的偏移。公开描述没有给这些数字,我也没有固件样本可以跑。所以我能确认的是缺陷类型和触发位置,不能确认的是利用代码的具体构造。

这一点很重要,因为网上如果出现「三行脚本拿下」的说法,那多半是拿别人的 PoC 改的,不是从这份固件里逆出来的。我不写那种话。

但缺陷类型和函数位置已经足够支撑一个判断,这个洞的触发条件很低,低到不需要任何前置交互。对于暴露在公网的设备,这意味着从 PoC 公开到被扫到,中间的时间窗口可能只有几个小时。

图 3 本文示意图:从 HTTP 请求到控制流劫持,四步里只有第三步是缺陷点,但前面没有拦。

今天能做的三件事,按顺序来

第一件,立刻确认你的 DIR-600L 有没有把管理界面暴露在公网。这不是让你扫全网,是让你查自己的资产。如果你不知道设备在哪里,先查出口 NAT 规则和端口映射。

图 4 本文示意图:影响面按暴露面和影响程度切四格,右上角是今天必须处理的那一格。

第二件,如果确认暴露,今天之内关掉公网访问。DIR-600L 的管理界面通常在 80 或 8080,HNAP 走的是同一个 HTTP 服务。最直接的过渡办法是在上游防火墙或者光猫上把入站规则删掉,只留内网访问。

查本机是不是 DIR-600L(示意代码,从内网侧确认设备型号和固件版本是否落在受影响区间)

代码语言:txt
复制
#!/bin/bash
# 在内网扫一遍常见网关地址,看谁在跑 D-Link 管理界面
for ip in 192.168.0.1 192.168.1.1 192.168.2.1 10.0.0.1; do
  code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 2 "")
  if [ "$code" = "200" ] || [ "$code" = "401" ]; then
    title=$(curl -s --connect-timeout 2 "" | grep -io '<title>[^<]*</title>' | head -1)
    echo "$ip 可达 HTTP $code $title"
  fi
done
# 命中后进管理界面看型号页,DIR-600L 的固件版本会写在首页底部
# 受影响版本为 2.17b02,同系列 2.17 也在另一条 CVE 里出现

第三件,如果关不掉,比如设备在远端无人值守,那就把 HNAP 服务停掉。有些固件版本允许在管理界面里关闭 HNAP,有些不允许。不允许的话,只能靠上游 ACL 限制源地址。

我要说清楚,这三件事都是缓解,不是修复。D-Link 官方目前没有针对这个 CVE 发布补丁的通告,NVD 条目状态是 Deferred,意思是厂商可能已经停止支持这个型号。DIR-600L 是老产品,指望官方出固件的概率不高。

所以过渡方案要按「长期没有补丁」来设计。如果这个设备承担的是关键网络出口,我的建议是换掉,不是打补丁。这不是危言耸听,是这类家用路由生命周期结束后的常态。

同类实现里,做得对的是把校验放在读之前

我对比了几款同代家用路由的 HNAP 处理逻辑,发现一个规律。做得好的实现,会在 hnap_main 的最前面先做三件事,检查 Content-Length 是否超过上限,检查 SOAPAction 是否在白名单里,检查来源地址是否在允许网段。这三件事都在读请求体之前完成。

做得不好的实现,顺序是反的,先读请求体,再解析动作名,最后才判断要不要认证。这个顺序在功能上能跑通,因为大多数正常请求不会超长,所以测试阶段发现不了。

这就是为什么这个洞以前没人发现。它不是功能缺陷,正常使用永远触发不了。它只在恶意构造的超长请求体下才暴露,而家用路由的测试很少覆盖这种输入。

另一个原因是 HNAP 协议本身比较老,设计之初没有把安全边界想清楚。SOAPAction 放在头里,参数放在体里,头和体分开处理,但处理顺序没有强制规定。厂商实现时各写各的,顺序不一致就成了常态。

所以我的判断是,这个洞不是孤例,同代产品里大概率还有类似的实现。你今天修了 DIR-600L,不代表同类设备就安全了。这是一类问题,不是一个点。

这次没查到的,比查到的更值得记下来

公开信息里缺三块。第一,hnap_main 里栈缓冲区的具体大小和偏移,没有披露。第二,触发请求的完整报文格式,没有披露。第三,D-Link 官方的修复计划,没有披露,NVD 状态是 Deferred。

这三块缺任何一块,都没法做精确的利用复现。所以我在前面写的调用链,是基于同类实现的推断,不是从这份固件里逐行验证的。这个区别我反复强调,是因为安全写作里最容易犯的错就是把推断写成事实。

还有一块我没验证的,是这个洞能不能绕过 ASLR。栈溢出覆盖返回地址之后,能不能稳定跳转到攻击者控制的位置,取决于固件有没有开栈保护。DIR-600L 这个年代的固件,很多是关掉栈保护的,但我没有样本,不能下结论。

所以这篇文章的边界是,我告诉你缺陷长在哪一层、触发顺序是什么、今天该做什么,但我不给你利用代码,也不承诺「一定能打」。那种承诺在这个信息量下是不负责任的。

一个老路由的洞,照出的是整条链的惯性

这件事往深了看,不是一个函数写错了。是「先读数据再判权限」这个顺序,在很多嵌入式 Web 服务里被当成了默认写法。因为功能上它能跑,测试阶段它不报错,所以它就一直在那里。

我今天能做的,是把暴露面收掉。但真正该改的,是以后选设备时把「安全更新还在不在」当成一个硬指标。一个停止支持的设备,哪怕今天没洞,明天也会有。

DIR-600L 这个洞会不会被大规模利用,取决于公网上还有多少台暴露的实例。这个数字我没查到。但我知道,只要有一台还在公网上,PoC 公开之后的每一天都是在赌。

写在最后

一个停止支持的设备上的 9.3 分漏洞,真正难的不是修,是承认它修不了。你今天能收掉的是暴露面,收不掉的是「买的时候没看支持周期」这个惯性。下次选设备,把「安全更新还剩几年」写在参数表第一行,比事后连夜查 NAT 规则便宜得多。

以上,既然看到这里了,如果觉得不错,随手点个赞、在看、转发三连吧,如果想第一时间收到推送,也可以给我个星标⭐~

谢谢你看我的文章,我们,下次再见。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 9.3 分的意思是,门槛几乎为零
  • 问题不在解析层,在认证之前就把栈写穿了
  • 我推出来的触发链,卡在第三个判断上
  • 今天能做的三件事,按顺序来
  • 同类实现里,做得对的是把校验放在读之前
  • 这次没查到的,比查到的更值得记下来
  • 一个老路由的洞,照出的是整条链的惯性
  • 写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档