mercredi 15 octobre 2014, 12:04:30 (UTC+0200)

Bypassing blacklists based on IPy

A few months ago, when working on my slides for Insomni'hack, I had a few conversations with the Prezi security team. Among many defense-in-depth protections, they introduced some code forbidding access to private IP addresses. Their conversion backend (the one I exploited) was using Python urllib2, and the blacklist was implemented via the IPy library.

Given that I enjoy bypassing blacklists, I asked Prezi for this specific piece of code. And they gave it to me ;-) Thanks guys! So, The code they used looks like that:

import urllib2, IPy
from socket import gethostbyname
from urlparse import urlparse
def has_private_ip(url, logger_func=None):
    [... more checks ...]

    # Confirm IP type is not private
    is_private = IPy.IP(ip_address).iptype() == 'PRIVATE'
    if is_private:
        log('Invalid IP for URL (private): %s' % url)
    return is_private

The first thing to notice is that the [... more checks ...] part is quite complex by itself. Converting an URL to an IP address is not a trivial task in a security-sensitive context. For example, you may want to take care of HTTP redirects and DNS-rebinding attacks. But let's focus on the code checking if the IP address is private or not. There's a simple call to iptype(), a function of the IPy library. Prezi's wrapper around this call could be more paranoid: if the function returns something else than 'PRIVATE' (for example 'RESERVED'), then it would be considered as OK. Explicitly checking for 'PUBLIC' would be better.

Now, IPy. It defines a few IPv4 and IPv6 ranges, based on the first bits of the IP addresses. For IPv4:

IPv4ranges = {
    '0':                'PUBLIC',   # fall back
    '00000000':         'PRIVATE',  # 0/8
    '00001010':         'PRIVATE',  # 10/8
    '01111111':         'PRIVATE',  # 127.0/8
    '1':                'PUBLIC',   # fall back
    '1010100111111110': 'PRIVATE',  # 169.254/16
    '101011000001':     'PRIVATE',  # 172.16/12
    '1100000010101000': 'PRIVATE',  # 192.168/16
    '111':              'RESERVED', # 224/3

This code too could be more paranoid: the fallbacks are 'PUBLIC', so subverting the parsing logic may bypass the blacklist. Under the hood, the iptype() function converts the IP address to a list of bits using strBin() and then tries to match this list against the previously shown IP ranges:

 def iptype(self):

    bits = self.strBin()
    for i in xrange(len(bits), 0, -1):
        if bits[:i] in IPv4ranges:
            return IPv4ranges[bits[:i]]
    return "unknown"

As you may have notice, a fourth state ('unknown') appears, but it can't be reached because of the fallbacks. Unless you can produce bits different of both '0' and '1' :-o

Now that the context is defined, let's do some hacking. Given that urllib2 supports tons of formats for IP addresses, maybe we could find a format misinterpreted by IPy and then wrongly considered as non 'PRIVATE'. Go go fuzzing!

import IPy

loopback = [
 '',           # Normal
 '2130706433',          # Integer
 '0x7F000001',          # Hexa
 '0x7F.0x00.0x00.0X01', # Hexa - dotted
 '0177.0000.0000.0001', # Octal

for ip in loopback:
   print ip + ':',
      print IPy.IP(ip).iptype()
      print 'Problem in IPy'

Simple setup: five ways to encode the loopback address, each of them supported by urllib2. The results? PRIVATE
2130706433: PRIVATE
0x7F000001: PRIVATE
0x7F.0x00.0x00.0X01: Problem in IPy
0177.0000.0000.0001: PUBLIC

Both the normal and integer formats are correctly considered as 'PRIVATE'. The hexadecimal format is OK too if not dotted. The dotted version will trigger a 'ValueError: invalid literal for int() with base 10' exception in parseAddress() when initializing the IPy object. Depending on how the code is structured, this could be enough for bypassing a filter. But the most interesting format is the octal one. '0177.0000.0000.0001' is considered as 'PUBLIC' by IPy and resolved to '' by urllib2, that's a perfect fit! If we try to generalize the tests (using 10/8, 192.168/16,, ...), it appears that the hexadecimal dotted format will always raise the same exception. And that the octal format will confuse IPy a lot:

[=] 0177.0000.0000.0001 (
[+] PUBLIC - Fallback 1

[=] 0251.0376.0251.0376 (
[!] '0251.0376.0251.0376': single byte must be 0 <= byte < 256

[=] 0300.0250.0001.0002 (
[!] '0300.0250.0001.0002': single byte must be 0 <= byte < 256

[=] 0254.0020.0003.0004 (

[=] 0012.0013.0014.0015 (
[+] PUBLIC - Fallback 0

The bug was reported to IPy's maintainer (Jeff Ferland aka autocracy) in March. No news since then :-( Prezi patched their own filter and awarded me $500. Not a big payout, but the real impact was near null in their setup. Thanks defense in depth mechanisms! Anyway, if you are using IPy, maybe you should take care of these bypasses...

Posted by Nicolas Grégoire | Permanent link
Copyright 2010-2021 Agarri