My server has been under this type of attack every day for about 1 year and as far as I have researched, it is a similar type of attack on multiple servers.
Django’s own security is not fully effective against this attack, it only causes the other party to get a 400 error, but since the attack is ongoing, the system goes down temporarily and then comes back up again. Probably because I’m using aws elastic beanstalk, the system gets itself back up and running.
172.31.44.14 - - [21/Sep/2024:01:43:13 +0000] "GET / HTTP/1.1" 400 154 "-" "Mozilla/5.0" "35.216.141.220"
172.31.44.14 - - [21/Sep/2024:01:45:30 +0000] "GET / HTTP/1.1" 400 154 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" "45.156.129.46"
172.31.26.204 - - [21/Sep/2024:01:46:39 +0000] "GET /.well-known/acme-challenge/0JCWGV2T84AUO8ZXX8HLI-GW7EEZNV9G HTTP/1.1" 301 5 "-" "Cpanel-HTTP-Client/1.0" "94.199.206.16"
172.31.26.204 - - [21/Sep/2024:01:46:40 +0000] "GET /.well-known/acme-challenge/0JCWGV2T84AUO8ZXX8HLI-GW7EEZNV9G HTTP/1.1" 200 6699 "-" "Cpanel-HTTP-Client/1.0" "94.199.206.16"
172.31.26.204 - - [21/Sep/2024:01:59:04 +0000] "GET /.env HTTP/1.1" 400 154 "-" "Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Raspbian Chromium/72.0.3626.121 Chrome/72.0.3626.121 Safari/537.36" "91.92.243.155"
172.31.44.14 - - [21/Sep/2024:02:11:07 +0000] "POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1" 400 157 "-" "-" "-"
172.31.44.14 - - [21/Sep/2024:02:11:07 +0000] "POST /cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/bin/sh HTTP/1.1" 400 157 "-" "-" "-"
172.31.44.14 - - [21/Sep/2024:02:11:07 +0000] "POST /hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:07 +0000] "GET /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:09 +0000] "GET /vendor/phpunit/phpunit/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:10 +0000] "GET /vendor/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:10 +0000] "GET /vendor/phpunit/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:10 +0000] "GET /vendor/phpunit/phpunit/LICENSE/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:11 +0000] "GET /vendor/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:12 +0000] "GET /phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:13 +0000] "GET /phpunit/phpunit/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:17 +0000] "GET /phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:18 +0000] "GET /phpunit/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:19 +0000] "GET /lib/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:19 +0000] "GET /lib/phpunit/phpunit/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:21 +0000] "GET /lib/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:21 +0000] "GET /lib/phpunit/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:21 +0000] "GET /lib/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:22 +0000] "GET /laravel/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:22 +0000] "GET /www/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:23 +0000] "GET /ws/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:24 +0000] "GET /yii/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:24 +0000] "GET /zend/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:24 +0000] "GET /ws/ec/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:24 +0000] "GET /V2/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:28 +0000] "GET /tests/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:28 +0000] "GET /test/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:28 +0000] "GET /testing/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:29 +0000] "GET /api/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:29 +0000] "GET /demo/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:29 +0000] "GET /cms/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:31 +0000] "GET /crm/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:32 +0000] "GET /admin/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:32 +0000] "GET /backup/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:33 +0000] "GET /blog/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:34 +0000] "GET /workspace/drupal/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:34 +0000] "GET /panel/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:34 +0000] "GET /public/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:35 +0000] "GET /apps/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
172.31.44.14 - - [21/Sep/2024:02:11:35 +0000] "GET /app/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 400 154 "-" "Custom-AsyncHttpClient" "47.251.99.88"
A solution of my own creation:
Models:
from django.db import models
from django.utils.timezone import now, timedelta
class BlockedIP(models.Model):
ip_address = models.GenericIPAddressField(unique=True)
blocked_url = models.URLField(max_length=200, null=True, blank=True)
blocked_at = models.DateTimeField(default=now)
ban_duration = models.DurationField(default=timedelta(hours=720))
ban_expires_at = models.DateTimeField(editable=False, help_text="Sadece bilgi amaçlı var")
def save(self, *args, **kwargs):
self.ban_expires_at = self.blocked_at + self.ban_duration
super().save(*args, **kwargs)
Middleware:
import re
from django.core.cache import cache
from django.http import HttpResponseForbidden
from .models import BlockedIP
from django.utils.timezone import now, timedelta
class BlockMaliciousRequestsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.ATTEMPTS_LIMIT_FOR_400 = 25
self.MSG = "Your IP address has been blocked."
# Regex pattern for paths to block
self.blocked_patterns = re.compile(
r'(\.env|\.php|\.ini|\.bak|\.log|\.sql|\.conf|\.old|\.tar|\.gz|\.sh|\.yml)$', re.IGNORECASE
)
def __call__(self, request):
ip = self.get_client_ip(request)
# We removed the check for authenticated users, as the protection is handled at a lower layer now.
# if request.user.is_authenticated:
# return self.get_response(request)
# Check cache for blocked IP
cache_key = f"blocked_ip_{ip}"
blocked_ip_data = cache.get(cache_key)
if blocked_ip_data is None:
# If not found in cache, or incorrect type, check the database
blocked_ip = BlockedIP.objects.filter(ip_address=ip, blocked=True).first()
if blocked_ip:
# Save the necessary data to cache
blocked_ip_data = {
'blocked_at': blocked_ip.blocked_at,
'ban_duration': blocked_ip.ban_duration.total_seconds()
}
cache.set(cache_key, blocked_ip_data, timeout=3600) # 1 hour (3600 seconds)
else:
blocked_ip_data = None
# If the IP is blocked, check if the block duration has expired
if blocked_ip_data:
blocked_at = blocked_ip_data['blocked_at']
ban_duration = timedelta(seconds=blocked_ip_data['ban_duration'])
if now() < blocked_at + ban_duration:
return HttpResponseForbidden(self.MSG)
else:
# If the block duration has expired, clear the cache
cache.delete(cache_key)
# Immediately block specific paths and HEAD requests
if self.blocked_patterns.search(request.path) or request.method == 'HEAD':
self.block_ip(ip, request.path)
return HttpResponseForbidden(self.MSG)
response = self.get_response(request)
# Block requests with a 400 status code
if response.status_code == 400:
attempts = cache.get(ip, 0) + 1
cache.set(ip, attempts, timeout=60) # 60 seconds
if attempts > self.ATTEMPTS_LIMIT_FOR_400:
self.block_ip(ip, request.path)
return HttpResponseForbidden(self.MSG)
return response
def get_client_ip(self, request):
"""Retrieve the client's IP address, accounting for reverse proxy."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def block_ip(self, ip, path):
"""Block the specified IP address for a given duration."""
ban_duration = timedelta(hours=720) # Block the IP address for 720 hours (1 month).
blocked_ip, created = BlockedIP.objects.get_or_create(
ip_address=ip,
defaults={
'blocked_at': now(),
'blocked_url': path,
'ban_duration': ban_duration
}
)
# Save the necessary data to cache
blocked_ip_data = {
'blocked_at': blocked_ip.blocked_at,
'ban_duration': ban_duration.total_seconds()
}
cache.set(f"blocked_ip_{ip}", blocked_ip_data, timeout=int(ban_duration.total_seconds()))
if not created:
# Update the previously blocked IP
blocked_ip.blocked_at = now()
blocked_ip.ban_duration = ban_duration
blocked_ip.blocked_url = path
blocked_ip.save()
Signals:
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.core.cache import cache
from .models import BlockedIP
@receiver(pre_delete, sender=BlockedIP)
def unblock_ip_cache(sender, instance, **kwargs):
ip = instance.ip_address
cache_key = f"blocked_ip_{ip}"
cache.delete(cache_key)
settings:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # Security should be first
'blocklist.middleware.BlockMaliciousRequestsMiddleware', # Handles malicious requests
'django.contrib.sessions.middleware.SessionMiddleware', # Session management
'django.middleware.locale.LocaleMiddleware', # For language settings
'django.middleware.common.CommonMiddleware', # General request handling
'django.middleware.csrf.CsrfViewMiddleware', # CSRF protection
'django.contrib.auth.middleware.AuthenticationMiddleware', # User authentication, required for is_authenticated
'django_ratelimit.middleware.RatelimitMiddleware', # For rate limiting and security controls
'django.contrib.messages.middleware.MessageMiddleware', # Manages user messages
'django.middleware.clickjacking.XFrameOptionsMiddleware', # Clickjacking protection
]
But I still can’t handle the issue the way I want. The problem is that I add user control in the middleware so that the security measures I take do not block users registered in the system.
if request.user.is_authenticated:
return self.get_response(request)
For this check to work correctly, I need to add my own middleware after AuthenticationMiddleware. If I do this, the django security will block the attack before it reaches my middleware and the attacker will only get a 400 error. This security measure fails to protect the environment and my system temporarily goes critical and then comes back up.
'django.contrib.auth.middleware.AuthenticationMiddleware'
If I put my middleware at the top without user control, I occasionally block users in the system.
What are your suggestions for how I can defeat this attack without damaging my system and prevent the attacker from accessing my site?
Thank you.