【技术分享】看我如何利用Ruby原生解析器漏洞绕过SSRF过滤器

阅读量148342

|评论1

|

发布时间 : 2017-11-13 14:58:33

x
译文声明

本文是翻译文章,文章来源:edoverflow.com

原文地址:https://edoverflow.com/2017/ruby-resolv-bug/

译文仅供参考,具体内容表达以及含义原文为准。

http://p4.qhimg.com/t014a4c2088e4915dd8.jpg

译者:興趣使然的小胃

预估稿费:100RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

一、简介

我在Ruby的Resolv::getaddresses中发现了一个漏洞,利用这个漏洞,攻击者可以绕过多个SSRF(Server-Side Request Forgery,服务端请求伪造)过滤器。诸如GitLab以及HackerOne之类的应用程序会受此漏洞影响。这份公告中披露的所有报告细节均遵循HackerOne的漏洞披露指南

此漏洞编号为CVE-2017-0904


二、漏洞细节

Resolv::getaddresses的执行结果与具体的操作系统有关,因此输入不同的IP格式时,该函数可能会返回空值。在防御SSRF攻击时,常用的方法是使用黑名单机制,而利用这个漏洞可以绕过这种机制。

实验环境为:

环境1:ruby 2.3.3p222 (2016-11-21) [x86_64-linux-gnu]

环境2:ruby 2.3.1p112 (2016-04-26) [x86_64-linux-gnu]

在环境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

四、根本原因

接下来我们来分析导致这个漏洞的根本原因。

我在代码片段中添加了一些注释语句,以便读者理顺代码逻辑。

getaddresses函数的输入参数(name)为待解析的某个地址,在函数内部,该参数会传递给each_address函数。

# File lib/resolv.rb, line 100
def getaddresses(name)
  ret = []
  each_address(name) {|address| ret << address} # Here!
  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
    @resolvers.each {|r| # Here!
      r.each_address(name) {|address|
        yield address.to_s
        yielded = true
      }
      return if yielded
    }
end
# File lib/resolv.rb, line 109
def initialize(resolvers=[Hosts.new, DNS.new])
    @resolvers = resolvers
end

进一步跟下去,initialize实际的初始化代码如下所示(我保留了源代码中的注释语句,这些语句能提供许多有价值的信息):

# File lib/resolv.rb, line 308
##
# Creates a new DNS resolver.
#
# +config_info+ can be:
#
# nil:: Uses /etc/resolv.conf.
# String:: Path to a file using /etc/resolv.conf's format.
# Hash:: Must contain :nameserver, :search and :ndots keys.
# :nameserver_port can be used to specify port number of nameserver address.
#
# The value of :nameserver should be an address string or
# an array of address strings.
# - :nameserver => '8.8.8.8'
# - :nameserver => ['8.8.8.8', '8.8.4.4']
#
# The value of :nameserver_port should be an array of
# pair of nameserver address and port number.
# - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]]
#
# Example:
#
#   Resolv::DNS.new(:nameserver => ['210.251.121.21'],
#                   :search => ['ruby-lang.org'],
#                   :ndots => 1)

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

这些代码表明,Resolv::getaddresses的执行结果与具体操作系统有关,当输入不常见的IP编码格式时,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 Core开发团队也给出了相同的建议:

“如果待解析地址由操作系统的解析器负责解析,那么检查地址的正确方式是使用操作系统的解析器,而非使用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

6.1 GitLab社区版及企业版

相关报告请参考此处链接

Mustafa Hasan在提交给HackerOne的报告中描述了GitLab的一个SSRF漏洞,利用本文介绍的这个漏洞,可以轻松绕过前面的补丁。GitLab引入了一个排除列表(即黑名单),但会先使用Resolv::getaddresses来解析用户提供的地址,然后将解析结果与排除列表中的值进行比较。这意味着用户再也不能使用诸如http://127.0.0.1以及http://localhost/这样的地址,这些地址正是Mustafa Hasan在原始报告中提到的地址。绕过排除列表限制后,我就可以扫描GitLab的内部网络。

http://p2.qhimg.com/t019b7eb7bb696c787f.png

spacer.gif

http://p2.qhimg.com/t01c11af66ef414a505.png

GitLab提供了新的补丁:

https://about.gitlab.com/2017/11/08/gitlab-10-dot-1-dot-2-security-release/

6.2 private_address_check

相关报告请参考此处链接

private_address_checkJohn Downey开发的一个Ruby gem,可以用来防止SSRF攻击。真正的过滤代码位于lib/private_address_check.rb文件中。private_address_check的工作原理是先使用Resolv::getaddresses来解析用户提供的URL地址,然后将返回值与黑名单中的值进行对比。这种场景中,我可以使用GitLab案例中用过的技术再一次绕过这个过滤器。

# 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在“Integrations”页面中使用了private_address_check来防止SSRF攻击,因此HackerOne也会受这种绕过技术影响

该页面地址为:

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

http://p2.qhimg.com/t012bdcf7d196beba6c.png

spacer.gifhttp://p5.qhimg.com/t01fc7f550396582760.png

不幸的是,我无法利用这个SSRF漏洞,因此这个问题只是一个过滤器绕过问题。HackerOne还是鼓励我提交问题报告,因为他们会把任何潜在的安全问题纳入考虑范围,而这个绕过技术正好落在这类问题中。

private_address_check在0.4.0版中修复了这个漏洞。


七、不受影响的应用及gem

7.1 ssrf_filter

Arkadiy Tetelman开发的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>'

7.2 faraday-restrict-ip-addresses

Ben Lavender开发的faraday-restrict-ip-addresses也不受此漏洞影响,其遵循了Ruby Code开发团队提供的建议。

# 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以及Yasin Soliman在挖掘这个漏洞过程中提供的帮助。

John Downey以及Arkadiy Tetelman的反应都非常敏锐。John Downey第一时间提供了修复补丁,Arkadiy Tetelman帮我理清了为何他们开发的gem不受此问题影响。

本文翻译自edoverflow.com 原文链接。如若转载请注明出处。
分享到:微信
+10赞
收藏
興趣使然的小胃
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66