Bypassing Django-Defender IP Based Restriction

Hi there

Few minutes ago, I saw one of  tweet. I love Django and community that supports with their open source modules such as Django-Defender thus I’ve decided to look at source code from security perspective. 

What is Django-Defender ?

A simple Django reusable app that blocks people from brute forcing login attempts. The goal is to make this as fast as possible, so that we do not slow down the login attempts.

https://github.com/kencochrane/django-defender

Vulnerabilities

 

To keep it short, I will directly jump into the codes. Following codes are responsible for getting client ip address from HTTP requests.

def get_ip_address_from_request(request):
    """ Makes the best attempt to get the client's real IP or return
        the loopback """
    PRIVATE_IPS_PREFIX = ('10.', '172.', '192.', '127.')
    ip_address = ''
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
    if x_forwarded_for and ',' not in x_forwarded_for:
        if not x_forwarded_for.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(
                x_forwarded_for):
            ip_address = x_forwarded_for.strip()
    else:
        ips = [ip.strip() for ip in x_forwarded_for.split(',')]
        for ip in ips:
            if ip.startswith(PRIVATE_IPS_PREFIX):
                continue
            elif not is_valid_ip(ip):
                continue
            else:
                ip_address = ip
                break
    if not ip_address:
        x_real_ip = request.META.get('HTTP_X_REAL_IP', '')
        if x_real_ip:
            if not x_real_ip.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(
                    x_real_ip):
                ip_address = x_real_ip.strip()
    if not ip_address:
        remote_addr = request.META.get('REMOTE_ADDR', '')
        if remote_addr:
            if not remote_addr.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(
                    remote_addr):
                ip_address = remote_addr.strip()
            if remote_addr.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(
                    remote_addr):
                ip_address = remote_addr.strip()
    if not ip_address:
        ip_address = '127.0.0.1'
    return ip_address

Trusting user controlled data is always threat for applications. One of these vector is X-Forwarded-For ( XFF ) which is can be spoofed by client by tampering HTTP request with local proxy such as Bupr Suite.

As you can see above code, get_ip_address_from_request function is directly getting IP address from request.

The functions is checking out a comma in XFF string at the first if statement. Let’s assume we have single IP address on XFF. Now we have IP validation phase.

    if x_forwarded_for and ',' not in x_forwarded_for:
        if not x_forwarded_for.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(
                x_forwarded_for):
            ip_address = x_forwarded_for.strip()

First part of the if statement is related to private IP prefix like 10, 192, 172 etc.  Let’s check out is_valid_ip function.

def is_valid_ip(ip_address):
    """ Check Validity of an IP address """
    valid = True
    try:
        socket.inet_aton(ip_address.strip())
    except (socket.error, AttributeError):
        valid = False
    return valid

Everything will be fine if we try to pass valid IP address to net_aton function. If you pass string or something else exception will thrown.

>>> import socket
>>> socket.inet_aton("8.8.8.8".strip())
'\x08\x08\x08\x08'
>>>
>>>
>>> socket.inet_aton("I_m_not_valid".strip())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
socket.error: illegal IP address string passed to inet_aton
>>>

But what will be happened if you pass something different like following example ?

>>> socket.inet_aton("0x41.0x41.0x41.0x41".strip())
'AAAA'

As you can see, function didn’t thrown exception and return AAAA.

  1. 0x41.0x41.0x41.0x41 does not start with one of the PRIVATE IPS PREFIX.
  2.  It also does not thrown exception. That means is_valid_ip return True.
  3. Application will do IP based stuff with 0x41.0x41.0x41.0x41 which is can be spoofed by client at first place.

Demo

Django-Defender’s read me file says; “https://hub.docker.com is using Django-Defender”

I’ve created a new a new users and capture login request.

POST /account/login/?next=/account/welcome/ HTTP/1.1
Host: hub.docker.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://hub.docker.com/account/login/?next=/account/welcome/
Cookie: csrftoken=TCnfMo54U0Mr2Yv9KLeRqwRDDAZog2hC
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 98

csrfmiddlewaretoken=TCnfMo54U0Mr2Yv9KLeRqwRDDAZog2hC&username=mdisectest&password=notvalidpassword

I will use Burp Suite Intruder in order to simulate brute-force login. Web site blocked us after 10 failed login attempts and shows following error message when I try to reach login page.

Sorry, you have made too many failed login attempts with that username and have been locked out for 10 minutes.

This error maybe appeared because of same cookie. I’ve deleted the all cookies and try to reach login page one more time but result was same.

Now it’s time to do our trick that we’ve found earlier.

Django-defender bypass

Please look at grey row. I’ve put XFF string HTTP header.

Voila! We managed to bypass IP based ban. We are able to do brute-forcing again.

UPDATE:

25 Feb 2015 00:35 =Vulnerability reported to the developer team.

25 Feb 2015 01:00 = Ken Cochrane responded.

25  Feb 2015 08:00 = Patch is under the deportment. https://github.com/kencochrane/django-defender/pull/33