Как стать автором
Обновить

Port Knocking для Windows

Время на прочтение9 мин
Количество просмотров8.9K

Мне довольно часто приходится настраивать "одинокие" терминальные сервера(и не только терминальные) в "Облаках", с "легким, быстрым" доступом к нему по RDP.

Все объяснения для пользователей\заказчиков, что такие сервера должны быть доступны только с доверенных IP или через VPN или с 2FA, воспринимаются "в штыки" и тогда приходится рисковать...

Конечно сервер защищается от Bruteforce(а), используются парольная политика, нестандартный порт, но все равно сервер постоянно под угрозой, в месяц можно увидеть по 15000 попыток подобрать пароль.

Такое обстоятельство дел заставило меня подумать о простом и действенном способе защиты сервера и в то же время этот способ не должен усложнить пользователям подключение к серверу.

Первое что пришло в голову - Port Knocking, использую его на RouterOS, но беглое гугление показало что для Windows не существует подобного штатного функционала, поиск сторонних средств которые могли бы помочь организовать задуманное не дал результата,  больше покопавшись нашел только странные и страшные поделки на Java не внушавшие доверия.

Тогда решил сделать свой PortKnocking для Windows. Написать его решил на PowerShell, чтоб не пришлось устанавливать на сервер дополнительно Java или Python.

Т.к. есть опыт c телеграм ботами(@SuperMon_Bot), решил добавить и информирование о работе PortKnocking через телеграм.

Определился что вся задумка должна состоять из нескольких скриптов:

  • Подготовка и настройка всего необходимого, создание правил фаервола, создание задач в планировщике, деактивация старых правил RDP.

  • Главный скрипт который слушает порты и добавляет адреса в правила фаервола.

  • Скрипт который обнуляет список адресов из правила фаервола.

  • Позже коллега уговорил меня добавить скрипт который проверят корректность выбранных портов и возможность их использовать.

Решил что постучится на два разных порта от большего к меньшему, с небольшой разницей во времени будет достаточно.

Например, постучаться на порт 65000 и в течении 3-х секунд постучаться на порт 1025.

Все настройки решил вынести в отдельный файл XML,  для удобства, чтоб не преходилось менять эти данные в нескольких разных скриптах.

0. settings.xml

Hidden text
<config>
	<system>
		<Telegramtoken>XXXXXXX</Telegramtoken >
		<Telegramchatid>YYYYY</Telegramchatid>
		<port1>zzzz</port1>
        		<port2>zzzz</port2>
        <RDPport>63389</RDPport>
        <SafeIPs>z.z.z.z</SafeIPs>
	</system>
	<tasks>
	</tasks>
</config> 

Пояснение XML:

Telegramtoken - вставляем токен телеграм бота. Как регистрировать ботов описывать не буду, подобной информации достаточно в интернете.

Telegramchatid - вставляем ID пользователя или чата, куда будут отправляться информация о работе приложения.

port1 - порт на котором будет ожидаться первый стук.

port2 - порт на котором будет ожидаться второй стук.

SafeIPs - доверенные адреса с которых должен остаться доступ даже без стука.

1. Далее пишу главный скрипт который будет слушать порты и добавлять постучавших в разрешённые IP:

Port-Knocking-2-main.ps1

Hidden text
#Вычитиваем данные из XML

[xml]$xmlConfig = Get-Content -Path ("C:\Install\Port-Knocking\settings.xml")
$Telegramtoken  = $xmlConfig.config.system.Telegramtoken
$Telegramchatid = (($xmlConfig.config.system.Telegramchatid).Split(",")).Trim()
$port1 = $xmlConfig.config.system.port1
$port2 = $xmlConfig.config.system.port2

#Добавляем функцию отправки сообщений в телеграм

Function Send-Telegram {
    Param([Parameter(Mandatory=$true)][String]$Message)
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    $Response = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($Telegramtoken)/sendMessage?chat_id=$($Telegramchatid)&text=$($Message)"
}


#Основная часть срипта
#Вечный цикл
While ($True) {
    $Listener1 = [System.Net.Sockets.TcpListener][int]$port1; #Слушаем первый порт
    $Listener1.Start();

    While($true){

        $client1 = $Listener1.AcceptTcpClient();
        $IP1 = $client1.client.RemoteEndPoint.Address.IPAddressToString
        $Client1.Close();
        break

    } 
    #Слушаем второй порт 4 секунды
    $Listener1.Stop()

        $IP2 = Start-Job -Name port2 {
        [xml]$xmlConfig = Get-Content -Path ("C:\Install\Port-Knocking\settings.xml")
		$port2 = $xmlConfig.config.system.port2
        $Listener2 = [System.Net.Sockets.TcpListener][int]$port2;
        $Listener2.Start();
        $Client2 = $Listener2.AcceptTcpClient();
        $IP2 = $client2.client.RemoteEndPoint.Address.IPAddressToString
        $Client2.Close();
        $Listener2.Stop()
        return $IP2
        } | Wait-Job -Timeout 4 | Receive-Job
    Stop-Job -Name port2
    Remove-Job -Name port2 
	
#Сравниваем IP	
    if ($IP1 -eq $IP2){
        $CurrentIPs = (Get-NetFirewallRule -DisplayName "!RDP-for-port-knocking" | Get-NetFirewallAddressFilter ).RemoteAddress
        $CurrentIPs += $IP2
        Set-NetFirewallRule -DisplayName "!RDP-for-port-knocking" -RemoteAddress  $CurrentIPs 
		Send-Telegram -Message "$env:COMPUTERNAME added IP - $IP1" #Если IP совпали, добавляем в правило фаервола и отправляем сообщение в телегу
    }	
	else {
		Send-Telegram -Message "$env:COMPUTERNAME was scanned from  $IP1" #Если не совпали, отправляем собщение что нас сканировали
	}
    # Обнуляем IP		
    $IP1 = $null
    $IP2 = $null
	
}

2. Далее приступил к скрипту который в конце суток удаляет все добавленные IP, кроме тех что в списке SafeIPs

Port-Knocking-3-ZeroTime.ps1

Hidden text

[xml]$xmlConfig = Get-Content -Path ("C:\Install\Port-Knocking\settings.xml")
$Telegramtoken  = $xmlConfig.config.system.Telegramtoken
$Telegramchatid = (($xmlConfig.config.system.Telegramchatid).Split(",")).Trim()
$SafeIPs = (($xmlConfig.config.system.SafeIPs).Split(",")).Trim()

Set-NetFirewallRule -DisplayName "!RDP-for-port-knocking" -RemoteAddress $SafeIPs

Function Send-Telegram {
    Param([Parameter(Mandatory=$true)][String]$Message)
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    $Response = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($Telegramtoken)/sendMessage?chat_id=$($Telegramchatid)&text=$($Message)"
}

Send-Telegram -Message "$env:COMPUTERNAME cleared IPs"

3. Далее приступил к скрипту который по сути "подготавливает почву" для работы главного скрипта:

Port-Knocking-0-Install.ps1

Hidden text
#Start-Transcript -Path C:\Install\Port-Knocking\log01.txt -Append -Force

[xml]$xmlConfig = Get-Content -Path ("C:\Install\Port-Knocking\settings.xml")
$port1 = $xmlConfig.config.system.port1
$port2 = $xmlConfig.config.system.port2
$RDPport = $xmlConfig.config.system.RDPport
$PortsListener = $port1, $port2
$SafeIPs = (($xmlConfig.config.system.SafeIPs).Split(", ")).Trim()

#Создаем павило для RDP для подстраховки, чтоб не потерять управление сервером. 
New-NetFirewallRule -DisplayName '!RDP-for-SafeIPs' -Direction Inbound -LocalPort $RDPport -Protocol TCP -Action Allow –RemoteAddress $SafeIPs  -Enabled True  -Verbose 

#Создаем правило RDP для Port Knocking
New-NetFirewallRule -DisplayName '!RDP-for-port-knocking' -Direction Inbound -LocalPort $RDPport -Protocol TCP -Action Allow –RemoteAddress $SafeIPs -Enabled True -Verbose

#Создаем правило для портов на которые будем ожидать стук
New-NetFirewallRule -DisplayName '!For-port-knocking-Listener' -Direction Inbound -LocalPort $PortsListener -Protocol TCP -Action Allow   -Enabled True  -Verbose

#Отключаем “старое” правило для RDP
Get-NetFirewallPortFilter | Where-Object LocalPort -eq $RDPport |  Get-NetFirewallRule | Where-Object {($_.DisplayName -ne '!RDP-for-port-knocking') -and (($_.DisplayName -ne '!RDP-for-SafeIPs'))} | Disable-NetFirewallRule

#Создаем задачу в планировщике для запуска основного скрипта
Register-ScheduledTask  -TaskName 'Port-Knocking-2-main-task' -Xml (Get-Content 'C:\Install\Port-Knocking\Tasks\Port-Knocking-2-main-task.xml'  | Out-String) -Force

#Создаем задачу в планировщике для запуска “обнуления” адресов
Register-ScheduledTask  -TaskName 'Port-Knocking-3-ZeroTime' -Xml (Get-Content 'C:\Install\Port-Knocking\Tasks\Port-Knocking-3-ZeroTime.xml'  | Out-String) -Force

#Запускаем задачу прослушивания портов
Start-ScheduledTask -TaskName 'Port-Knocking-2-main-task'

Задачи в планировщике решил для упрощения создать с помощью экспорта из XML.

Сами задачи ниже:

Port-Knocking-2-main-task.xml

Hidden text
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2022-10-13T16:40:50.3934022</Date>
    <Author>Admin</Author>
    <URI>\Port-Knocking-2-main-task</URI>
  </RegistrationInfo>
  <Principals>
    <Principal id="Author">
      <UserId>S-1-5-18</UserId>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <RestartOnFailure>
      <Count>20</Count>
      <Interval>PT5M</Interval>
    </RestartOnFailure>
    <StartWhenAvailable>true</StartWhenAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
  </Settings>
  <Triggers>
    <BootTrigger>
      <ExecutionTimeLimit>P1D</ExecutionTimeLimit>
      <Delay>PT5M</Delay>
      <Repetition>
        <Interval>PT2M</Interval>
        <Duration>P1D</Duration>
      </Repetition>
    </BootTrigger>
  </Triggers>
  <Actions Context="Author">
    <Exec>
      <Command>Powershell.exe</Command>
      <Arguments>-ExecutionPolicy Bypass  -File "C:\Install\Port-Knocking\Port-Knocking-2-main.ps1"</Arguments>
    </Exec>
  </Actions>
</Task>

Создается задача Port-Knocking-2-main-task с такими параметрами:

Запуск -  при старте системы 

Запуск программы - Powershell.exe -ExecutionPolicy Bypass  -File "C:\Install\Port-Knocking\Port-Knocking-2-main.ps1"

Немедленно запускать задачу если пропущен плановый запуск

При сбое выполнения перезапускать через 5 минут

Если задача уже выполняется - не запускать новый екземпляр.

Port-Knocking-3-ZeroTime.xml

Hidden text
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2022-10-13T18:14:46.5186083</Date>
    <Author>Admin</Author>
    <URI>\Port-Knocking-3-ZeroTime</URI>
  </RegistrationInfo>
  <Principals>
    <Principal id="Author">
      <UserId>S-1-5-18</UserId>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
  </Settings>
  <Triggers>
    <CalendarTrigger>
      <StartBoundary>2022-10-13T23:59:00</StartBoundary>
      <ExecutionTimeLimit>PT30M</ExecutionTimeLimit>
      <ScheduleByDay>
        <DaysInterval>1</DaysInterval>
      </ScheduleByDay>
    </CalendarTrigger>
  </Triggers>
  <Actions Context="Author">
    <Exec>
      <Command>Powershell.exe</Command>
      <Arguments>-ExecutionPolicy Bypass  -File  "C:\Install\Port-Knocking\Port-Knocking-3-ZeroTime.ps1"</Arguments>
    </Exec>
  </Actions>
</Task>

Создается задача Port-Knocking-2-main-task с такими параметрами:

Запуск -  Ежедевно в 23:59

Запуск программы - Powershell.exe -ExecutionPolicy Bypass  -File C:\Install\Port-Knocking\Port-Knocking-3-ZeroTime.ps1"

Останавливать задачу выполняемую дольше 1ч.

4. Позже, по просьбе коллеги как говорил выше написал скрипт проверки возможности использования выбранных портов. Хотя по моему мнению это лишнее, настоящий администратор знает что происходит на сервере или как это проверить вручную)))

Check-correct-settings.ps1

Hidden text
$ErrorActionPreference  =  'SilentlyContinue'

[xml]$xmlConfig = Get-Content -Path ("C:\Install\Port-Knocking\settings.xml")
$port1 = $xmlConfig.config.system.port1
$port2 = $xmlConfig.config.system.port2
$RDPport = $xmlConfig.config.system.RDPport

$RDPport0 =  (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -name "PortNumber").PortNumber
$RDPportState = Get-NetTCPConnection -LocalPort $RDPport0 -State Listen

if (($RDPport -eq $RDPport0) -and ($RDPportState)){
    Write-Host "RDP port - $RDPport0 and port is OK" -ForegroundColor Green
}
else{
    Write-Host "RDP port  is not $RDPport0 or  port in not liten, check please..." -ForegroundColor Red
     
}

$port1State = Get-NetTCPConnection -LocalPort $port1 -State Listen
$port2State = Get-NetTCPConnection -LocalPort $port2 -State Listen

if (($port1State -eq $null)  -and ($port2State -eq $null)){
     Write-Host "This ports($port1, $port2) can be use for Port-Knocning" -ForegroundColor Green
   
}
else{
    Write-Host "This ports($port1, $port2) can not be use for Port-Knocning" -ForegroundColor Red
   
}

Результат работы скрипта если RDP порт выбран неправильно.

5. Далее встал вопрос как пользователям стучаться. Для админа это совсем не вопрос есть telnet, Test-NetConnection, есть даже готовые клиенты.

Но для пользователей подготовил папку в которой лежит маленький exe(paping.exe) и CMD файл с таким содержимым:

paping.exe X.X.X.X -p zzz  -c 2

sleep 1

paping.exe X.X.X.X -p zzz  -c 2

6. Протестировав данную поделку в течении пары недель остался доволен, пользователи сильно не бурчали, попытки подобрать пароль к серверу свелись практически к нулю.

Как это работает на практике:

Администратор копирует папку, запускает от имени адмнистратора скрипт Port-Knocking-0-Install.ps1

Готово к использованию.

Пользователь при первом подключении в день запускает файл.

Пользователь ждет 3 секунды

Пользователь подключается к серверу как обычно по RDP.

Больше в эти сутки запускать Knocker не нужно, только на следующие сутки.

Примеры сообщений которые получает администратор.

При добавлении IP.

Если сервер был просканирован то получаем сообщение.

В 23:59 Администратор получает сообщение.

Естественно сообщения можно и не получать :-)

Все на усмотрение всемогущего Администртора :-)

Мне и моим коллегам данная поделка пригодилась, надесь будет полезно и другим.

Ссылка на Github

Теги:
Хабы:
Всего голосов 10: ↑7 и ↓3+4
Комментарии35

Публикации