Few minutes ago, I saw one of @PythonWeekly 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.
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("18.104.22.168".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.
- 0x41.0x41.0x41.0x41 does not start with one of the PRIVATE IPS PREFIX.
- It also does not thrown exception. That means is_valid_ip return True.
- Application will do IP based stuff with 0x41.0x41.0x41.0x41 which is can be spoofed by client at first place.
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.
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.
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