Commit 19b79a1b authored by root's avatar root

Initial commit

parents
#!/usr/bin/perl
use strict;
use warnings;
use autodie qw(open close);
use Time::Piece;
use Net::IP;
use constant DATETIME_FORMAT => '%Y-%m-%d %H:%M:%S';
use constant CURRENT_TIME => localtime->epoch();
use constant BANNED_FN => '/opt/global_filter/var/banned/rdp_ddos/banned';
use constant STATS_FN => '/opt/global_filter/var/stats/rdp_ddos/stats';
use constant BAN_LIFT_TIME => CURRENT_TIME - (60 * 60 * 24 * 3);
my $banned_old = {}; # prefixes banned before
my $banned_new = {}; # prefixes to ban
my $connections = {}; # here we'll keep the connections' list
my $attacker_hosts = {}; # here we'll keep the attackers' list
my $stats_attackers = {}; # here we'll count attackers
my $stats_targets = {}; # here we'll count targets
open(my $banned_fh, '<', BANNED_FN);
while(<$banned_fh>) {
chomp;
if($_ =~ /^\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,2})?)(\s*#\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s*)?$/) {
my $updated = defined($4)
? Time::Piece->strptime($4, DATETIME_FORMAT)->epoch()
: CURRENT_TIME;
my $prefix_string = $1;
my $prefix_object = Net::IP->new($prefix_string, 4);
die("Can't parse the $prefix_string IP-address: " . Net::IP::Error())
unless(defined($prefix_object));
my $banned_record = {
prefix => $prefix_object,
updated => $updated
};
if($updated >= BAN_LIFT_TIME) {
$banned_old->{$prefix_string} = $banned_record;
}
}
}
close($banned_fh);
while(<STDIN>) {
chomp;
if($_ =~ /^\d{2}:\d{2}:\d{2}\.\d{6} IP (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\.\d+ > (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\.3389: .+$/) {
my $src_address = $1;
my $dst_address = $2;
$connections->{$src_address} = {}
unless(defined($connections->{$src_address}));
$connections->{$src_address}->{$dst_address} = 0
unless(defined($connections->{$src_address}->{$dst_address}));
$connections->{$src_address}->{$dst_address}++;
if(scalar(keys(%{$connections->{$src_address}})) >= 16) {
$attacker_hosts->{$src_address} = $connections->{$src_address}->{$dst_address};
$stats_attackers->{$src_address} = 1;
$stats_targets->{$dst_address} = 1;
}
}
}
my $attacker_networks = {};
foreach my $src_address (keys(%{$attacker_hosts})) {
(my $src_network_string = $src_address) =~ s/^(\d{1,3}\.\d{1,3}\.\d{1,3})\.\d{1,3}/$1.0\/24/;
unless(defined($attacker_networks->{$src_network_string})) {
my $src_network_object = Net::IP->new($src_network_string);
die("Can't parse the $src_network_string IP-address: " . Net::IP::Error())
unless(defined($src_network_object));
$attacker_networks->{$src_network_string} = {
prefix => $src_network_object,
counter => 0
};
}
$attacker_networks->{$src_network_string}->{'counter'}++;
}
foreach my $attacker_network_key (keys(%{$attacker_networks})) {
if($attacker_networks->{$attacker_network_key}->{'counter'} >= 32) {
$banned_new->{$attacker_network_key} = {
prefix => $attacker_networks->{$attacker_network_key}->{'prefix'},
updated => CURRENT_TIME
};
}
}
BANNED_OLD_LOOP:
foreach my $banned_old_key (keys(%{$banned_old})) {
foreach my $banned_new_key (keys(%{$banned_new})) {
# If there's a network in the "new" list that is identical to a network from the "old" list or covers it, then
# the network from the "old" list shall NOT be added to the "new" list.
my $overlaps = $banned_old->{$banned_old_key}->{'prefix'}->overlaps($banned_new->{$banned_new_key}->{'prefix'});
if($overlaps == $Net::IP::IP_IDENTICAL) {
$banned_new->{$banned_new_key}->{'updated'} = CURRENT_TIME;
next BANNED_OLD_LOOP;
} elsif($overlaps == $Net::IP::IP_A_IN_B_OVERLAP) {
next BANNED_OLD_LOOP;
}
}
$banned_new->{$banned_old_key} = {
prefix => $banned_old->{$banned_old_key}->{'prefix'},
updated => $banned_old->{$banned_old_key}->{'updated'}
}
}
ATTACKER_HOSTS_LOOP:
foreach my $address_string (keys(%{$attacker_hosts})) {
my $address_object = Net::IP->new($address_string, 4);
die("Can't parse the $address_string IP-address: " . Net::IP::Error())
unless(defined($address_object));
my $listed = 0;
foreach my $banned_new_key (keys(%{$banned_new})) {
my $overlaps = $banned_new->{$banned_new_key}->{'prefix'}->overlaps($address_object);
if($overlaps == $Net::IP::IP_IDENTICAL) {
$banned_new->{$banned_new_key}->{'updated'} = CURRENT_TIME;
next ATTACKER_HOSTS_LOOP;
} elsif($overlaps == $Net::IP::IP_B_IN_A_OVERLAP && $banned_new->{$banned_new_key}->{'updated'} == CURRENT_TIME) {
next ATTACKER_HOSTS_LOOP;
}
}
$banned_new->{$address_string} = {
prefix => $address_object,
updated => CURRENT_TIME
};
}
open($banned_fh, '>', BANNED_FN);
foreach my $banned_new_key (keys(%{$banned_new})) {
printf(
$banned_fh
"%-32s# %s\n",
$banned_new->{$banned_new_key}->{'prefix'}->prefix(),
gmtime($banned_new->{$banned_new_key}->{'updated'})->strftime(DATETIME_FORMAT)
);
}
close($banned_fh);
open(my $stats_fh, '>>', STATS_FN);
printf(
$stats_fh
"%d %d %d\n",
CURRENT_TIME,
scalar(keys(%{$stats_attackers})),
scalar(keys(%{$stats_targets}))
);
close($stats_fh);
#!/bin/sh
PATH=/bin:/usr/bin:/usr/sbin
stats_directory=/opt/global_filter/var/stats/rdp_ddos
stats_directory_history="${stats_directory}/history/$(date +%Y%m%d%H%M%S)"
mkdir -p ${stats_directory_history};
for host in $(cat /opt/global_filter/etc/hosts); do
scp -q -i /opt/global_filter/.ssh/rdp_ddos_fetcher.rsa "rdp_ddos_stats@${host}":"stats.pcap" "${stats_directory}/${host}.pcap"
done
(
for f in ${stats_directory}/*.pcap; do
cat "${f}" | tcpdump -r - -n -n 2>/dev/null
mv "${f}" "${stats_directory_history}"
done
) | perl /opt/global_filter/bin/rdp_ddos_analyzer.pl
#!/bin/sh
PATH="/bin:/usr/bin:/sbin:/usr/sbin"
global_filter_ipset_name="global_filter__rdp_ddos"
global_filter_url="https://secure.tucha.ua/global-filter/banned/rdp_ddos"
iptables_comment="RDP DDoS-attack mitigation rule"
trap "exit ${EXIT_CODE_ERROR}" TERM
top_pid="$$"
function log {
echo "[$(date +%F\ %T)]" $* >&2
}
function warn {
log "[!]" $*
}
function die {
warn $*
kill -s TERM "${top_pid}"
}
# Enabling iptables for the bridged traffic
centos_release=$(cat /etc/centos-release | sed -r 's/^CentOS (Linux )?release ([[:digit:]]+)\.[[:digit:].]+ \(.+\)[[:space:]]*$/\2/')
[[ ${?} -ne 0 ]] &&
die "Can't look up for the OS release"
case "${centos_release}" in
6)
filter_status=$(sysctl -n net.bridge.bridge-nf-call-iptables)
[[ ${?} -ne 0 ]] &&
log "Can't lookup the net.bridge.bridge-nf-call-iptables variable"
if [[ "${filter_status}" != "1" ]]; then
sysctl -w net.bridge.bridge-nf-call-iptables=1
[[ ${?} -ne 0 ]] &&
die "Can't change net.bridge.bridge-nf-call-iptables"
log "Changed net.bridge.bridge-nf-call-iptables to 1"
fi
;;
7)
lsmod | grep '^br_netfilter[[:space:]]' >/dev/null
result="${?}"
if [[ "${result}" -eq 1 ]]; then
modprobe br_netfilter
[[ ${?} -ne 0 ]] &&
die "Can't load the br_netfilter kernel module"
log "Loaded br_netfilter"
elif [[ "${result}" -ne 0 ]]; then
die "Can't look up for the br_netfilter kernel module"
fi
;;
*)
die "This OS isn't supported"
;;
esac
# Getting the list of banned networks
global_filter_desired=$(
curl -f -s "${global_filter_url}" |
sed -rn 's/^[[:space:]]*([[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\/[[:digit:]]{1,2})([[:space:]]*#.*)?/\1/gp' |
sed -r 's/^([[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3})\/32$/\1/g'
)
[[ ${?} -ne 0 ]] &&
die "Can't get the list of banned networks"
[[ -z "${global_filter_desired}" ]] &&
die "The list of banned networks is empty"
# Checking if the ipset list is present
global_filter_current="$(ipset -L "${global_filter_ipset_name}")"
[[ ${?} -ne 0 ]] &&
ipset -N "${global_filter_ipset_name}" nethash
# Getting the members
global_filter_current="$(echo "${global_filter_current}" | awk 'BEGIN { echo = 0; } // { if(echo == 1) { print $1; }; } /^Members:$/ { echo = 1; }')"
# Adding new members
for member_desired in ${global_filter_desired}; do
found=0
for member_current in ${global_filter_current}; do
[[ "${member_desired}" == "${member_current}" ]] &&
found=1
done
if [[ "${found}" -eq 0 ]]; then
ipset add "${global_filter_ipset_name}" "${member_desired}"
[[ ${?} -ne 0 ]] &&
die "Can't add ${member_desired} to the ${global_filter_ipset_name} list"
log "Added ${member_desired}"
fi
done
# Removing inactive members
for member_current in ${global_filter_current}; do
found=0
for member_desired in ${global_filter_desired}; do
[[ "${member_current}" == "${member_desired}" ]] &&
found=1
done
if [[ "${found}" -eq 0 ]]; then
ipset del "${global_filter_ipset_name}" "${member_current}"
[[ ${?} -ne 0 ]] &&
die "Can't remove ${member_desired} from the ${global_filter_ipset_name} list"
log "Removed ${member_current}"
fi
done
# Making sure about the iptables rule's presence
rules_found=$(iptables -L FORWARD)
[[ ${?} -ne 0 ]] &&
die "Can't get the list of iptables rules"
echo "${rules_found}" | grep "/\* ${iptables_comment} \*/" >/dev/null
result="${?}"
if [[ "${result}" -eq 1 ]]; then
iptables -I FORWARD 1 -m set --match-set "${global_filter_ipset_name}" src -d 0.0.0.0/0 -p tcp -m tcp --dport 3389 -j DROP -m comment --comment "${iptables_comment}"
log "Added the iptables rule"
elif [[ "${result}" -ne 0 ]]; then
die "Can't look up for the iptables rule"
fi
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment