回归最本质的信息安全

如何利用Ruby本地解析器漏洞绕过SSRF过滤器

2017年11月13日发布

48,154
0
0

导语:概要 本文是针对我们在Resolv::getaddresses中发现的一个漏洞的安全建议,攻击者可以利用该漏洞绕过多种服务器端请求伪造过滤器,诸如GitLab和HackerOne之类的应用程序都受到这个漏洞的威胁。本文披露的内容均遵循HackerOne的“漏洞披露指

概要

本文是针对我们在Resolv::getaddresses中发现的一个漏洞的安全建议,攻击者可以利用该漏洞绕过多种服务器端请求伪造过滤器,诸如GitLab和HackerOne之类的应用程序都受到这个漏洞的威胁。本文披露的内容均遵循HackerOne的“漏洞披露指南”(链接地址:https://www.hackerone.com/disclosure-guidelines)。

该漏洞已分配的编号为CVE-2017-0904(链接地址:http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2017-0904)。

漏洞详细信息

由于Resolv::getaddresses与操作系统高度相关,因此可以通过使用不同的IP格式使其返回空值。如果这个漏洞被利用的话,可以令攻击者绕过用于抵御SSRF攻击的黑名单。

1.png

机器1

irb(main):002:0> Resolv.getaddresses("127.0.0.1")
=> ["127.0.0.1"]
irb(main):003:0> Resolv.getaddresses("localhost")
=> ["127.0.0.1"]
irb(main):004:0> Resolv.getaddresses("127.000.000.1")
=> ["127.0.0.1"]

机器2

irb(main):008:0> Resolv.getaddresses("127.0.0.1")
=> ["127.0.0.1"]
irb(main):009:0> Resolv.getaddresses("localhost")
=> ["127.0.0.1"]
irb(main):010:0> Resolv.getaddresses("127.000.000.1")
=> [] # 😱

这个安全问题可以在最新稳定版的Ruby上进行重现: 

$ ruby -v
ruby 2.4.3p201 (2017-10-11 revision 60168) [x86_64-linux]
$ irb
irb(main):001:0> require 'resolv'
=> true
irb(main):002:0> Resolv.getaddresses("127.000.001")
=> []

POC代码

irb(main):001:0> require 'resolv'
=> true
irb(main):002:0> uri = "0x7f.1"
=> "0x7f.1"
irb(main):003:0> server_ips = Resolv.getaddresses(uri)
=> [] # The bug!
irb(main):004:0> blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
=> ["127.0.0.1", "::1", "0.0.0.0"]
irb(main):005:0> (blocked_ips & server_ips).any?
=> false # Bypass

引发漏洞的根本原因

下面,我们介绍引发这个安全漏洞的根本原因。为了提高可读性,我在下面的代码片段中添加了相应的注释。

当我们在调试模式(irb-d)下运行irb时,将返回如下所示的错误:

irb(main):002:0> Resolv.getaddresses "127.1"
Exception `Resolv::DNS::Config::NXDomain' at /usr/lib/ruby/2.3.0/resolv.rb:549 - 127.1
Exception `Resolv::DNS::Config::NXDomain' at /usr/lib/ruby/2.3.0/resolv.rb:549 - 127.1
=> []

从上面可以看出,该异常来自fetch_resource() [1](链接地址:https://github.com/ruby/ruby/blob/e16bd0f4d81ef74035712853a5eb527f28abb342/lib/resolv.rb#L514-L554)。 “NXDOMAIN”响应表明,发生异常的原因是解析器无法找到相应的PTR记录。实际上,这没有什么好奇怪的,因为我们稍后将会看到,resolv.rb使用的是操作系统的解析器。

# Reverse DNS lookup on 💻 Machine 1.
$ nslookup 127.0.0.1
Server:   127.0.0.53
Address:  127.0.0.53#53
Non-authoritative answer:
1.0.0.127.in-addr.arpa  name = localhost.
Authoritative answers can be found from:
$ nslookup 127.000.000.1
Server:   127.0.0.53
Address:  127.0.0.53#53
Non-authoritative answer:
Name: 127.000.000.1
Address: 127.0.0.1
# NXDOMAIN for 127.1.
$ nslookup 127.1
Server:   127.0.0.53
Address:  127.0.0.53#53
** server can't find 127.1: NXDOMAIN

接下来,通过下面的代码,读者就会明白我们之前为什么说Resolv::getaddresses是与具体的操作系统高度相关的。

getaddresses用来接收地址(名称)并将其传递给each_address,一旦解析完成,它将被附加到ret数组中。

# File lib/resolv.rb, line 100
def getaddresses(name)
  # This is the "ret" array.
  ret = []
  # This is where "address" is appended to the "ret" array.
  each_address(name) {|address| ret << address}
  return ret
end

each_address通过@resolvers来处理name。

# File lib/resolv.rb, line 109
def each_address(name)
    if AddressRegex =~ name
      yield name
      return
    end
    yielded = false
    # "name" is passed on to the resolver here.
    @resolvers.each {|r|
      r.each_address(name) {|address|
        yield address.to_s
        yielded = true
      }
      return if yielded
    }
end

@resolvers是利用initialize()函数来完成初始化的。

# File lib/resolv.rb, line 109
def initialize(resolvers=[Hosts.new, DNS.new])
    @resolvers = resolvers
end

更进一步来说,初始化工作实际上是通过将config_info设置为nil来完成的,就本例来说,这里使用的是默认配置/etc/resolv.conf。

# File lib/resolv.rb, line 308
# Set to /etc/resolv.conf ¯_(ツ)_/¯
def initialize(config_info=nil)
  @mutex = Thread::Mutex.new
  @config = Config.new(config_info)
  @initialized = nil
end

下面展示的是默认的配置:

# File lib/resolv.rb, line 959
def Config.default_config_hash(filename="/etc/resolv.conf")
  if File.exist? filename
    config_hash = Config.parse_resolv_conf(filename)
  else
    if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
      require 'win32/resolv'
      search, nameserver = Win32::Resolv.get_resolv_info
      config_hash = {}
      config_hash[:nameserver] = nameserver if nameserver
      config_hash[:search] = [search].flatten if search
    end
  end
  config_hash || {}
end

这表明Resolv::getaddresses是与操作系统高度相关的,并且,如果提供的IP地址在反向DNS查找期间失败的话,getaddresses将返回一个空的ret数组。

缓解措施

我建议放弃Resolv::getaddresses,转而使用Socket库。

irb(main):002:0> Resolv.getaddresses("127.1")
=> []
irb(main):003:0> Socket.getaddrinfo("127.1", nil).sample[3]
=> "127.0.0.1"

Ruby核心开发团队也是建议使用Socket库。

既然网络地址是通过操作系统的解析器来解析的,那么地址检查的正确方式,就是使用操作系统的解析器,而非resolv.rb。 例如,可以使用socket库的Addrinfo.getaddrinfo。

——Tanaka Akira
% ruby -rsocket -e '
as = Addrinfo.getaddrinfo("192.168.0.1", nil)
p as
p as.map {|a| a.ipv4_private? }
'
[#<Addrinfo: 192.168.0.1 TCP>, #<Addrinfo: 192.168.0.1 UDP>, #<Addrinfo: 192.168.0.1 SOCK_RAW>]
[true, true, true]

受影响的应用程序和Gem

GitLab社区版和企业版

报告的链接地址:https://hackerone.com/reports/215105

对于Mustafa Hasan(链接地址:https://hackerone.com/strukt)的报告(链接地址:https://hackerone.com/reports/135937)(!17286(https://gitlab.com/gitlab-org/gitlab-ce/issues/17286))中提供的修复方法,可以通过利用这个漏洞轻易绕过。 虽然GitLab引入了一个黑名单,但仍然会使用Resolv::getaddresses来解析用户提供的地址,然后将输出与黑名单中的值进行比较。那么,这意味着人们不能再使用某些地址,http://127.0.0.1和http://localhost/,而这些正是Mustafa Hasan在原始报告中使用的地址。通过利用这个绕过漏洞,攻击者就可以扫描GitLab intance的内部网络。

1.png

11.png

GitLab提供了一个补丁:https://about.gitlab.com/2017/11/08/gitlab-10-dot-1-dot-2-security-release/。

John Downey(链接地址:https://twitter.com/jtdowney)提供的private_address_check(链接地址:https://github.com/jtdowney/private_address_check)

报告的链接地址:https://github.com/jtdowney/private_address_check/issues/1

private_address_check(链接地址:https://github.com/jtdowney/private_address_check)是一个用来防御SSRF的Ruby Gem。实际上,真正的过滤代码位于lib / private_address_check.rb中。该程序首先尝试使用Resolv::getaddresses来解析用户提供的URL,然后将返回的值与黑名单中的值进行比较。同样,攻击者可以使用前面介绍的技术来绕过该过滤器。

# File lib/private_address_check.rb, line 32
def resolves_to_private_address?(hostname)
  ips = Resolv.getaddresses(hostname)
  ips.any? do |ip|
    private_address?(ip)
  end
end

因此,HackerOne(链接地址:https://hackerone.com/reports/287245)也受此绕过漏洞的影响,因为它们也是通过private_address_check gem来防御“Integrations”面板上的SSRF攻击的:

https://hackerone.com/{BBP}/integrations.

111.png1111.png

令人遗憾的是,我无法利用这个SSRF漏洞,因为这里只包括一个过滤器绕过问题。不过,HackerOne仍然为这份安全报告提供了奖励,因为他们认为任何潜在的安全问题都应该得到重视,而这个绕过漏洞也是一个潜在的风险。

这个安全问题已在0.4.0版(链接地址:https://github.com/jtdowney/private_address_check/commit/58a0d7fe31de339c0117160567a5b33ad82b46af)中进行了修复。

未受该漏洞影响的应用程序和Gem

Arkadiy Tetelman(链接地址:https://twitter.com/arkadiyt)提供的ssrf_filter(链接地址:https://github.com/arkadiyt/ssrf_filter)

这个gem不会受到该漏洞的影响,因为它会检查返回的值是否为空。

# File lib/ssrf_filter/ssrf_filter.rb, line 116
raise UnresolvedHostname, "Could not resolve hostname '#{hostname}'" if ip_addresses.empty?
irb(main):001:0> require 'ssrf_filter'
=> true
irb(main):002:0> SsrfFilter.get("http://127.1/")
SsrfFilter::UnresolvedHostname: Could not resolve hostname '127.1'
  from /var/lib/gems/2.3.0/gems/ssrf_filter-1.0.2/lib/ssrf_filter/ssrf_filter.rb:116:in `block (3 levels) in <class:SsrfFilter>'
  from /var/lib/gems/2.3.0/gems/ssrf_filter-1.0.2/lib/ssrf_filter/ssrf_filter.rb:107:in `times'
  from /var/lib/gems/2.3.0/gems/ssrf_filter-1.0.2/lib/ssrf_filter/ssrf_filter.rb:107:in `block (2 levels) in <class:SsrfFilter>'
  from (irb):2
  from /usr/bin/irb:11:in `<main>'

Ben Lavender(链接地址:https://github.com/bhuga)提供的faraday-restrict-ip-addresses(链接地址:https://rubygems.org/gems/faraday-restrict-ip-addresses/versions/0.1.1)

这个gem使用的是Ruby核心开发团队推荐的Addrinfo.getaddrinfo。

# File lib/faraday/restrict_ip_addresses.rb, line 61
def addresses(hostname)
      Addrinfo.getaddrinfo(hostname, nil, :UNSPEC, :STREAM).map { |a| IPAddr.new(a.ip_address) }
    rescue SocketError => e
      # In case of invalid hostname, return an empty list of addresses
      []
end

小结

我要特别感谢Tom Hudson(链接地址:https://twitter.com/TomNomNom)和Yasin Soliman(链接地址:https://twitter.com/SecurityYasin)在我挖掘这个漏洞期间提供的帮助。

此外,在编写本文过程中,John Downey(链接地址:https://twitter.com/jtdowney)和Arkadiy Tetelman(链接地址:https://twitter.com/arkadiyt)也给予了积极的响应。其中,John Downey迅速为我们提供了补丁,而Arkadiy Tetelman则帮我弄清了为什么他们的gem不受这个问题的影响。

最后需要说明的一点是,这篇文章的源代码不是太讲究,敬请谅解。

本文翻译自:https://edoverflow.com/2017/ruby-resolv-bug/,如若转载,请注明原文地址: http://www.4hou.com/vulnerable/8441.html

点赞 0
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

fanyeee

fanyeee

这个人很懒,什么也没留下

发私信

发表评论