04 January, 2013

Update Windows Firewall rule based on eventlog data - Windows Server 2008

I promised in the title that this blog will be about real issues and real solutions in a large scale, in large IT infrastructure. People who have seen IT operations in an enterprise scale, they know that there are always exceptions. So this article is my exception here. This particular little post is about how I worked around a little IT problem at home. Yes, this time there are no 1000+ servers, no multiple regions involved, just my server at home and PowerShell of course.

Here is the problem: I have Cygwin running on my Windows 2008 server for the sake of ssh and scp access.  The problem of running ssh on a server is that there's always someone to perform trial-and-error attack with some semi-random user name and password. And they keep doing it. I thought I'd need to fix this and get rid of the random hacker trying to get into my ssh server. Because I want to be flexible in terms of where I want to use the ssh server from, I can't do white listing and reject every other IP, I need to be reactive and block IPs which try to get in with invalid user, so this is not a solution, it's more like a workaround. As Eddie Izzard would say, "it's not really a manoeuvre..., it's more of a gesture."

Fortunately, the ssh daemon logs events into the Application eventlog with the source IP address and the invalid user name...etc. something like:
sshd: PID 2212: Invalid user testuser from 111.111.111.11

Therefore a script would need to:
  • Query the evetlog for events with "invalid user" string
  • Parse the source IP address from the event text
  • Add the source IP to a firewall rule which blocks connections to port 22 from the specified IPs

First and second steps then, get the IP(s) from the events
$ips = Get-Eventlog -LogName Application -newest 100 | ?{($_.source -ieq "sshd") -and ($_.message -imatch ": invalid user \w+ from \d")} | %{[regex]::match($_.message, "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b").value } | select -Unique

A bit of explanation:
Get-Eventlog -LogName Application -newest 100
This is for reading the latest 100 events from Application log.

?{($_.source -ieq "sshd") -and ($_.message -imatch ": invalid user \w+ from \d")}
Filtering on events with source sshd and where the event text contains ": invalid user" and some letters and then " from " and some numbers.

%{[regex]::match($_.message, "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b").value }
Getting each of these events and enumerate an IP address from the event message with regex (start with a word boundary, matching to 1, 2 or 3 digits and a dot and then 3 times 1,2 or 3 digits - which is supposed to be the format of an IP address.

| select -Unique
Get a list of unique entries. There can be multiple events for an IP in the eventlog, but we only want to have an IP once.

Third step, adding the new IP(s) the the ssh blocking firewall rule:
# reading current IPs in the 'ssh block' firewall rule and
# picking only the list of IPs up from the netsh output

$output = netsh advfirewall firewall show rule name="ssh block"
[string]$fwips = ($output -match "RemoteIP:") -ireplace "RemoteIP: +|/32",""
$fwipsArr = $fwips.split(",")
writelog 0 "Current number of IPs in FW rule: $($fwipsArr.count)"

# creating a new array and adding the new IPs to it
$newipsArr = $fwipsArrforeach($ip in $ips){
   $newipsArr += $ip
}

$newipsArr = $newipsArr | sort -Unique
$fwipsArr = $fwipsArr | sort
writelog 0 "New number of IPs in FW rule: $($newipsArr.count)"

# creating the string for netsh command
$newIPs = [string]::join(",", $newipsArr)  
$oldIPs = [string]::join(",", $fwipsArr)

# if the new list is the same as the old one, then we don't update the FW rule

if($newIPs -eq $oldIPs){ 
   writelog 1 "No need to add the same IP again"
}
else{  
   writelog 1 "Adding addresses : $newips "
   Start-sleep -s 3
   $retval = netsh advfirewall firewall set rule name="ssh block" new remoteip="$newips" action="block"
   if(($retval -imatch "Updated 1 rule") -and ($retval -imatch "ok.")){
      writelog 0 "OK"
   }
   else{
      writelog 2 "Error"
   }
}


What I did with this script was that I eventually created a scheduled task triggered on the sshd event and it does the job. If someone tries to login via ssh with an invalid user, the script blocks the source IP so the attacker cannot access the ssh port anymore from that IP. I know it's not ideal... it's more of a gesture... ;)

Clipboard friendly code with logging included:
 $date = get-date -uformat "%Y-%m-%d-%H-%M-%S"
 $log = "c:\temp\addFWRule_$date.log"
 $global:logfile = New-Item -type file $log -force
  
  #### Function for creating log entries in a logfile and on standard output 
  function writeLog ([int]$type, [string]$message, [string]$modifier) {  
    #usage: writeLog 0 "info or error message" 
    # $modifier: <nonewline | extendline> 
    # Value nonewline: writes the output the the console and the logfile without carriage return 
    # Value extendline: writes the message to the output without date and  
    # both values can be used e.g. for writing to the console and logfile and put a status message at the end of the line as a second step 
    $date = get-date -uformat "%Y/%m/%d %H:%M:%S" 
  
    if($modifier -eq "extendline"){ 
       switch ($type) { 
         "0"   {$color = "Green"} 
         "1"   {$color = "Yellow"} 
         "2"   {$color = "Red"} 
       } 
    } 
    else{ 
       switch ($type) { 
         "0"   {$message = $date + ", INF, " + $message; $color = "Green"} 
         "1"   {$message = $date + ", WAR, " + $message; $color = "Yellow"} 
         "2"   {$message = $date + ", ERR, " + $message; $color = "Red"} 
       } 
    } 

    if($modifier -eq "nonewline"){ 
       write-host $message -ForegroundColor $color -NoNewLine 
       $bytes = [text.encoding]::ascii.GetBytes($message) 
       $bytes | add-content $script:logfile -enc byte 
    } 
    else{ 
       write-host $message -ForegroundColor $color 
       Add-Content $global:logfile $message 
    } 
  } 

 $ips = Get-Eventlog -LogName Application -newest 100 | ?{($_.source -ieq "sshd") -and ($_.message -imatch ": invalid user \w+ from \d")} | %{[regex]::match($_.message, "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b").value } | select -Unique
  
 # reading current IPs in the 'ssh block' firewall rule and 
 # picking only the list of IPs up from the netsh output
 $output = netsh advfirewall firewall show rule name="ssh block"
 [string]$fwips = ($output -match "RemoteIP:") -ireplace "RemoteIP: +|/32",""
 $fwipsArr = $fwips.split(",")
 writelog 0 "Current number of IPs in FW rule: $($fwipsArr.count)"
  
 # creating a new array and adding the new IPs to it
 $newipsArr = $fwipsArr
  
 foreach($ip in $ips){
      $newipsArr += $ip
 }
  
 $newipsArr = $newipsArr | sort -Unique
 $fwipsArr = $fwipsArr | sort
 writelog 0 "New number of IPs in FW rule: $($newipsArr.count)"
  
 # creating the string for netsh command
 $newIPs = [string]::join(",", $newipsArr) 
 $oldIPs = [string]::join(",", $fwipsArr) 
  
 # if the new list is the same as the old one, then we don't update the FW rule
 if($newIPs -eq $oldIPs){
      writelog 1 "No need to add the same IP again"
 }
 else{
      writelog 1 "Adding addresses : $newips "
      Start-sleep -s 3
      $retval = netsh advfirewall firewall set rule name="ssh block" new remoteip="$newips" action="block"

   if(($retval -imatch "Updated 1 rule") -and ($retval -imatch "ok.")){
     writelog 0 "OK"
   }
   else{
     writelog 2 "Error"
   }
 }
  


May The Force....
t

2 comments:

  1. Rather than using Get-Eventlog I'd recommend using Get-WinEvent with a FilterHashTable. It speeds up the process tremendously.

    Take a look at this post to see how to use it: http://theboywonder.co.uk/2012/03/15/speeding-up-get-winevent-in-powershell-by-using-filterhashtable/

    Using a FilterHashTable lets you filter early in the pipeline. This allows you to skip iterating over the newest 100 events to get the events you need.

    Food for thought:
    - accidentally entering wrong credentials will lock you out of your own system because you are using "select -unique" to get at the IP addresses
    - Check if there are failed login attempts from an IP 2 or 3 times in a row and only then block it
    - from the Linux/Unix world I know of things like port knocking or ssh key pair authentication or even one time passwords (OTP) that could also help you lower the risks of getting hacked
    - I'd clear the list of blocked IPs every now and then
    a) to not let it get too messy, and
    b) to get access to your system in case you have locked yourself out and won't be getting home anytime soon

    Hope to hear from you :-)

    ReplyDelete
  2. Thanks for the comments. Yep, Get-Winevent is much better, quicker, tidier, here is where I used it in a recent article:
    http://tompaps.blogspot.hu/2013/03/enumerate-eventlog-netlogon-errors-from.html?showComment=1365189532073#c7666859008596136390

    Get-Eventlog just came to mind when I wrote this one at home on a long night, doesn't make much difference on my home server, but indeed, the quicker the better.

    These are all valid comments so thanks again for these, the original script which runs has my username as exclusion, so if my username is in the event it will not block it. However, I think I'll put your suggestions in there to make it a bit more bullet proof. I should really setup a proper firewall I guess :)

    cheers,
    t

    ReplyDelete