Summary

This is a security advisory for a bug that I discovered in Resolv::getaddresses that enabled me to bypass multiple Server-Side Request Forgery filters. Applications such as GitLab and HackerOne were affected by this bug. The disclosure of all reports referenced in this advisory follow HackerOne's Vulnerability Disclosure Guidelines.

This bug was assigned CVE-2017-0904.

Vulnerability Details

Resolv::getaddresses is OS-dependent, therefore by playing around with different IP formats one can return blank values. This bug can be abused to bypass exclusion lists often used to protect against SSRF.

💻 Machine 1 💻 Machine 2 ruby 2.3.3p222 (2016-11-21) [x86_64-linux-gnu] ruby 2.3.1p112 (2016-04-26) [x86_64-linux-gnu]

💻 Machine 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"]

💻 Machine 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") => [] # 😱

This issue is reproducible in the latest stable build of 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") => []

Proof of concept

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

Root cause

The following section describes the root cause of this bug. I have added some comments in the code snippets to help the reader follow along.

When we run irb in debug mode ( irb -d ) the following error is returned:

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 => []

So the exception stems from fetch_resource() [1]. The "NXDOMAIN" response indicates that the resolver cannot find a corresponding PTR record. No surprise there, since, as we will see later on, resolv.rb uses the operating system's resolver.

# 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.