如何利用Ruby本地解析器漏洞繞過SSRF過濾器
概要
本文是針對我們在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
irb(main):002:0> Resolv.getaddresses("127.0.0.1")n=> ["127.0.0.1"]nirb(main):003:0> Resolv.getaddresses("localhost")n=> ["127.0.0.1"]nirb(main):004:0> Resolv.getaddresses("127.000.000.1")n=> ["127.0.0.1"]n
機器2
irb(main):008:0> Resolv.getaddresses("127.0.0.1")n=> ["127.0.0.1"]nirb(main):009:0> Resolv.getaddresses("localhost")n=> ["127.0.0.1"]nirb(main):010:0> Resolv.getaddresses("127.000.000.1")n=> [] # ??n
這個安全問題可以在最新穩定版的Ruby上進行重現:
$ ruby -vnruby 2.4.3p201 (2017-10-11 revision 60168) [x86_64-linux]n$ irbnirb(main):001:0> require resolvn=> truenirb(main):002:0> Resolv.getaddresses("127.000.001")n=> []n
POC代碼
irb(main):001:0> require resolvn=> truenirb(main):002:0> uri = "0x7f.1"n=> "0x7f.1"nirb(main):003:0> server_ips = Resolv.getaddresses(uri)n=> [] # The bug!nirb(main):004:0> blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]n=> ["127.0.0.1", "::1", "0.0.0.0"]nirb(main):005:0> (blocked_ips & server_ips).any?n=> false # Bypassn
引發漏洞的根本原因
下面,我們介紹引發這個安全漏洞的根本原因。為了提高可讀性,我在下面的代碼片段中添加了相應的注釋。
當我們在調試模式(irb-d)下運行irb時,將返回如下所示的錯誤:
irb(main):002:0> Resolv.getaddresses "127.1"nException `Resolv::DNS::Config::NXDomain at /usr/lib/ruby/2.3.0/resolv.rb:549 - 127.1nException `Resolv::DNS::Config::NXDomain at /usr/lib/ruby/2.3.0/resolv.rb:549 - 127.1n=> []n
從上面可以看出,該異常來自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.n$ nslookup 127.0.0.1nServer: 127.0.0.53nAddress: 127.0.0.53#53nNon-authoritative answer:n1.0.0.127.in-addr.arpa name = localhost.nAuthoritative answers can be found from:n$ nslookup 127.000.000.1nServer: 127.0.0.53nAddress: 127.0.0.53#53nNon-authoritative answer:nName: 127.000.000.1nAddress: 127.0.0.1n# NXDOMAIN for 127.1.n$ nslookup 127.1nServer: 127.0.0.53nAddress: 127.0.0.53#53n** server cant find 127.1: NXDOMAINn
接下來,通過下面的代碼,讀者就會明白我們之前為什麼說Resolv::getaddresses是與具體的操作系統高度相關的。
getaddresses用來接收地址(名稱)並將其傳遞給each_address,一旦解析完成,它將被附加到ret數組中。
# File lib/resolv.rb, line 100ndef getaddresses(name)n # This is the "ret" array.n ret = []n # This is where "address" is appended to the "ret" array.n each_address(name) {|address| ret << address}n return retnendn
each_address通過@resolvers來處理name。
# File lib/resolv.rb, line 109ndef each_address(name)n if AddressRegex =~ namen yield namen returnn endn yielded = falsen # "name" is passed on to the resolver here.n @resolvers.each {|r|n r.each_address(name) {|address|n yield address.to_sn yielded = truen }n return if yieldedn }nendn
@resolvers是利用initialize()函數來完成初始化的。
# File lib/resolv.rb, line 109ndef initialize(resolvers=[Hosts.new, DNS.new])n @resolvers = resolversnendn
更進一步來說,初始化工作實際上是通過將config_info設置為nil來完成的,就本例來說,這裡使用的是默認配置/etc/resolv.conf。
# File lib/resolv.rb, line 308n# Set to /etc/resolv.conf ˉ_(ツ)_/ˉndef initialize(config_info=nil)n @mutex = Thread::Mutex.newn @config = Config.new(config_info)n @initialized = nilnendn
下面展示的是默認的配置:
# File lib/resolv.rb, line 959ndef Config.default_config_hash(filename="/etc/resolv.conf")n if File.exist? filenamen config_hash = Config.parse_resolv_conf(filename)n elsen if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORMn require win32/resolvn search, nameserver = Win32::Resolv.get_resolv_infon config_hash = {}n config_hash[:nameserver] = nameserver if nameservern config_hash[:search] = [search].flatten if searchn endn endn config_hash || {}nendn
這表明Resolv::getaddresses是與操作系統高度相關的,並且,如果提供的IP地址在反向DNS查找期間失敗的話,getaddresses將返回一個空的ret數組。
緩解措施
我建議放棄Resolv::getaddresses,轉而使用Socket庫。
irb(main):002:0> Resolv.getaddresses("127.1")n=> []nirb(main):003:0> Socket.getaddrinfo("127.1", nil).sample[3]n=> "127.0.0.1"n
Ruby核心開發團隊也是建議使用Socket庫。
既然網路地址是通過操作系統的解析器來解析的,那麼地址檢查的正確方式,就是使用操作系統的解析器,而非resolv.rb。 例如,可以使用socket庫的Addrinfo.getaddrinfo。
——Tanaka Akiran% ruby -rsocket -e nas = Addrinfo.getaddrinfo("192.168.0.1", nil)np asnp as.map {|a| a.ipv4_private? }nn[#<Addrinfo: 192.168.0.1 TCP>, #<Addrinfo: 192.168.0.1 UDP>, #<Addrinfo: 192.168.0.1 SOCK_RAW>]n[true, true, true]n
受影響的應用程序和Gem
GitLab社區版和企業版
報告的鏈接地址:https://http://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的內部網路。
GitLab提供了一個補丁:https://http://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://http://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 32ndef resolves_to_private_address?(hostname)n ips = Resolv.getaddresses(hostname)n ips.any? do |ip|n private_address?(ip)n endnendn
因此,HackerOne(鏈接地址:https://hackerone.com/reports/287245)也受此繞過漏洞的影響,因為它們也是通過private_address_check gem來防禦「Integrations」面板上的SSRF攻擊的:
https://hackerone.com/{BBP}/integrations.
令人遺憾的是,我無法利用這個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 116nraise UnresolvedHostname, "Could not resolve hostname #{hostname}" if ip_addresses.empty?nirb(main):001:0> require ssrf_filtern=> truenirb(main):002:0> SsrfFilter.get("http://127.1/")nSsrfFilter::UnresolvedHostname: Could not resolve hostname 127.1n 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>n from /var/lib/gems/2.3.0/gems/ssrf_filter-1.0.2/lib/ssrf_filter/ssrf_filter.rb:107:in `timesn 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>n from (irb):2n from /usr/bin/irb:11:in `<main>n
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 61ndef addresses(hostname)n Addrinfo.getaddrinfo(hostname, nil, :UNSPEC, :STREAM).map { |a| IPAddr.new(a.ip_address) }n rescue SocketError => en # In case of invalid hostname, return an empty list of addressesn []nendn
小結
我要特別感謝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 更多內容請關注「嘶吼專業版」——Pro4hou
推薦閱讀:
※有良好的上網習慣是否表示不會成為他人的肉雞?
※Tripwire:一款據稱可以發現任何網站漏洞的安全工具
※實用教程:從網路中獲取NTLM Hash的四種方法
※Atom也爆遠程代碼執行漏洞?就問你怕不怕!
※Amazon S3 的鍋!澳大利亞近5萬名公民敏感信息泄露
TAG:信息安全 |