如何利用Ruby本地解析器漏洞绕过SSRF过滤器 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

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

fanyeee 漏洞 2017-11-13 16:47:21
310482
收藏

导语:

概要

本文是针对我们在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不受这个问题的影响。

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

  • 分享至
取消

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

扫码支持

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

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务