Port Knocking no pfSense usando Wazuh
O cenário é o seguinte:
- Debian Bullseye versão 5.10.84-1 com wazuh-manager 4.2.6-1 e elasticsearch, kibana e filebeat nas versões 7.14.2
- pfSense 2.6.0-RELEASE
- Ambos os servidores acima foram virtualizados em VirtualBox 6.1.32 r149290 em Arch Linux 5.16.13-arch1-1
- O artigo parte do princípio que o wazuh-manager, wazuh-agent e o pfSense já estejam instalados, configurados e funcionais, como descrito acima.
- IP do servidor com wazuh-manager: 10.0.2.201/24
- IP do servidor com pfSense: 10.0.2.200/24 (LAN) e 10.0.0.200/24 (WAN)
..::| Objetivo
A técnica de Port Knocking consiste em estabelecer 3 ou mais portas aleatórias que, se “tocadas” (ou seja, se “brevemente conectadas”) por um IP na sequência correta, fará com que um script seja executado para liberar outra porta para o IP de origem desses toques. Mais informações sobre essa técnica você encontra aqui.
Nesse artigo a intenção é liberar o acesso ao pfSense, na porta “wan” dele, somente para um específico IP de origem quando esse conseguir tocar em 3 portas configuradas como “combinação de portas para liberação de conexão”, e na sequência correta. Aqui descreverei a liberação das portas 80/TCP e 22/TCP, porém só uma delas poderá ser liberada para um IP de origem enquanto não expirar essa liberação.
Essa liberação externa para acessar o pfSense possibilitará o administrador de redes a configurar remotamente o gateway de uma LAN, pois possibilitará acesso tanto via SSH quanto ao webadmin do pfSense. Como essas portas serão liberadas somente se o Port Knocking for acertado, evita-se diversos tipos de ataques a esse gateway, como o de força bruta, por exemplo. No caso desse artigo utilizei a porta 80, porém é sempre recomendado ativar o HTTPS do pfSense e desativar o acesso HTTP, para maior segurança ainda mais quando se está acessando-o da internet.
..::| Enviando registros ao wazuh-manager
Caso não tenha o acesso SSH ao servidor do pfSense habilitado, faça-o indo no menu System -> Advanced -> marque a opção “Enable Secure Shell” e depois salve.
Configure o agente do Wazuh no pfSense para ler e encaminhar o conteúdo do log /var/log/filter.log. Abra o arquivo /var/ossec/etc/ossec.conf no pfSense e, depois do último </localfile> , adicione:
<localfile>
<log_format>syslog</log_format>
<location>/var/log/filter.log</location>
</localfile>
Reinicie o agent:
/usr/local/etc/rc.d/wazuh-agent restart
..::| Script portknocking.sh
Esse script será o responsável por verificar a sequência de portas tocadas, criar a regra de firewall para liberar acesso a porta solicitada caso a sequência de toques esteja correta, e também para excluir essa regra após um período pré-determinado a ser configurado no active-response do wazuh-manager.
Crie o arquivo /var/ossec/active-response/bin/portknocking.sh com o seguinte conteúdo:
#!/bin/sh
# Script para liberar porta atraves da tecnica port knocking
# Expect: srcip
# Author: Marcius da C. Silveira
# Last modified: Apr 1, 2022
# Publicado: https://marcius.pro
ACAO=$1
USUARIO=$2
IP=$3
REGRA=$5 # Regra no wazuh referente a esse port knocking especifico
INTWAN=`echo $9 | cut -d ',' -f1` # Nome da interface de rede da porta wan que sera acessada externamente para liberacao da ${PORT} configurada abaixo
TABWAN=`echo $9 | cut -d ',' -f2` # Nome que o pfsense da a interface de rede da porta wan configurada em ${INTWAN}
PROTO=`echo $9 | cut -d ',' -f3` # Protocolo que sera liberado apos a sequencia de portas serem tocadas
PORT=`echo $9 | cut -d ',' -f4` # Porta que sera liberada no port knocking
TIMEOUT=`echo $9 | cut -d ',' -f5` # Tempo de espera entre o toque na primeira porta do port knocking e a ultima da sequencia, se passar desse valor em segundos a operacao nao se realizara
EXTRAIGNOREPORTS=`echo $9 | cut -d ',' -f6 ` # Portas a serem ignoradas tambem
ARLOG=/var/ossec/logs/active-responses.log
# logando as atividades desse script
echo "`date` $0 $1 $2 $3 $4 $5 $9" >> ${ARLOG}
# Adicionar ou remover IP
# Adicionando
if [ ${ACAO} == "add" ]; then
# Separando as entradas no log de filtragem do pfSense referente ao IP que esta acessando esse sistema
LOGFILTERORIGINAL=/var/log/filter.log
LOGFILTER=/tmp/conexoes_${IP}_${PORT}_${PROTO}
cat ${LOGFILTERORIGINAL} | grep ","${IP}"," > ${LOGFILTER}
# Se foram encontradas conexoes desse IP no log de filtragem, executa esse script
if [ `wc -l ${LOGFILTER} | awk '{print $1}'` -gt 0 ]; then
# Portas do port knocking, na sequencia que devem ser tocadas
PORTASK="`/usr/local/bin/xmllint --xpath "pfsense/filter/rule[starts-with(descr, 'Port Knocking ${REGRA}') and interface = '${TABWAN}']" /conf/config.xml | grep "<port>" | grep -v ^$ | cut -d ">" -f 2 | cut -d "<" -f1 | tr "\n" "|" | sed 's/.$//'`"
# Portas que serao ignoradas na contagem, tiradas das regras na tab ${TABWAN}.
# A intencao aqui e evitar que conexoes legitimas sejam consideradas erros de sequencia de toque no port knocking desse script.
IGNOREPORTS="`/usr/local/bin/xmllint --xpath "pfsense/filter/rule[interface = '${TABWAN}']/destination/port" /conf/config.xml | cut -d ">" -f2 | cut -d "<" -f1 | tr "\n" "|" | sed 's/.$//'`"
# Removendo as portas do port knocking das portas encontradas na tab ${TABWAN} caso haja outras
QTDIGNOREPORTS=`echo $IGNOREPORTS | tr "|" " " | wc -w | sed 's/ //g'`
QTDPORTASK=`echo $PORTASK | tr "|" " " | wc -w | sed 's/ //g'`
if [ ${QTDIGNOREPORTS} != ${QTDPORTASK} ]; then
while [ ${QTDIGNOREPORTS} != 0 ]
do
QTDPRTSK=${QTDPORTASK}
while [ ${QTDPRTSK} != 0 ]
do
IPORT=`echo $IGNOREPORTS | cut -d "|" -f${QTDIGNOREPORTS}`
KPORT=`echo $PORTASK | cut -d "|" -f${QTDPRTSK}`
if [ ! -z ${IPORT} ]; then
if [ ${IPORT} = ${KPORT} ]; then
IGNOREPORTS=`echo $IGNOREPORTS | sed 's/^/|/' | sed 's/$/|/' | sed "s/|${IPORT}|/|/" | sed 's/|$//' | sed 's/||/|/' | sed 's/^|//'`
fi
fi
QTDPRTSK=$((QTDPRTSK-1))
done
QTDIGNOREPORTS=$((QTDIGNOREPORTS-1))
done
fi
# Adicionando a porta que sera aberta pelo port knocking e as ${EXTRAIGNOREPORTS} a variavel de portas a serem ignoradas
QTDEXTRAIGNOREPORTS=`echo ${EXTRAIGNOREPORTS} | tr "|" " " | wc -w | sed 's/ //g'`
while [ ${QTDEXTRAIGNOREPORTS} != 0 ]; do
EIPORT=`echo ${EXTRAIGNOREPORTS} | cut -d "|" -f${QTDEXTRAIGNOREPORTS}`
if [ ${EIPORT} == ${PORT} ]; then
EXTRAIGNOREPORTS=`echo ${EXTRAIGNOREPORTS} | sed "s/${EIPORT}//"`
fi
QTDEXTRAIGNOREPORTS=$((QTDEXTRAIGNOREPORTS-1))
done
if [ ${QTDIGNOREPORTS} != ${QTDPORTASK} ]; then
IGNOREPORTS=`echo -n ${IGNOREPORTS} | sed 's/^|//' ; echo "|"${PORT}"|"${EXTRAIGNOREPORTS} | sed 's/||/|/' | sed 's/|$//'`
else
IGNOREPORTS=`echo -n ${IGNOREPORTS} | echo ${PORT}"|"${EXTRAIGNOREPORTS} | sed 's/||/|/' | sed 's/|$//'`
fi
# Coletando informacoes do registro mais recente de conexao na primeira porta da sequencia do port knocking
PRIMEIRAPORTAK=`echo ${PORTASK} | awk -F "|" '{print $1}'`
REGPRIMEIRAPORTAK=`cat $LOGFILTER | grep -n ${IP} | grep ",${PRIMEIRAPORTAK}," | tail -1 | sed 's/:/ /' | cut -d " " -f1-4`
LINHAPRIMEIRAPORTAK=`echo ${REGPRIMEIRAPORTAK} | awk '{print $1}'`
# Analisando se o tempo entre o toque na primeira porta da sequencia e a ultima esta dentro de ${TIMEOUT} segundos
TSATUAL=`date +%s`
TSTIMEOUT=`echo "${TSATUAL} - ${TIMEOUT}" | bc`
TZANO=`date +%Z" "%Y`
DATAPRIMEIRAPORTAK=`echo ${REGPRIMEIRAPORTAK} | awk -v tzano="${TZANO}" '{print $2" "$3" "$4" "tzano}'`
TSPRIMEIRAPORTAK=`echo ${DATAPRIMEIRAPORTAK} | awk '{system("date -jf \"%a %b %d %t %z %Y\" \"Mon "$1" "$2" "$3" "$4" "$5"\" \"+%s\"")}'`
# Diferenca em segundos do momento em que esse script esta rodando e o toque na primeira porta da sequencia
# Lembrando que esse script so sera executado quando a ultima porta da sequencia do port knocking for tocada
DIFTS=`echo ${TSATUAL} - ${TSPRIMEIRAPORTAK} | bc `
# Separando a parte do log desde o ultimo toque dado na primeira porta da sequencia ate o final do mesmo
TOTALLINHASLOG=`cat $LOGFILTER | wc -l | awk '{print $1}'`
LINHASATEPRIMEIRAPORTAK=`echo "("${TOTALLINHASLOG} - ${LINHAPRIMEIRAPORTAK}") + 1" | bc`
QTDPORTAS=`cat $LOGFILTER | grep $IP | tail -${LINHASATEPRIMEIRAPORTAK} | cut -d "," -f 22 | egrep -vw $IGNOREPORTS | sort | uniq | wc -l`
# Coletando as portas acessadas.
PORTASACESSADAS=`cat $LOGFILTER | grep $IP | tail -${LINHASATEPRIMEIRAPORTAK} | cut -d "," -f 22 | egrep -vw $IGNOREPORTS | awk -v RS="[ \n]+" '!n[$0]++' | sed 's/ //'`
# Checando sequencia de portas acessadas
if [ ${QTDPORTAS} -eq ${QTDPORTASK} -a ${DIFTS} -le ${TIMEOUT} ]; then
while [ ${QTDPORTAS} != 0 ]; do
if [ `echo ${PORTASACESSADAS} | cut -d " " -f${QTDPORTAS}` = `echo ${PORTASK} | cut -d "|" -f${QTDPORTASK}` ]; then
RESULTCHECK=1
QTDPORTAS=$((QTDPORTAS-1))
QTDPORTASK=$((QTDPORTASK-1))
else
echo "`date` Sequencia invalida de portas acessadas" >> ${ARLOG}
RESULTCHECK=0
QTDPORTAS=0
QTDPORTASK=0
fi
done
if [ ${RESULTCHECK} == 1 ]; then
echo "pass in quick on ${INTWAN} proto ${PROTO} from ${IP} to port ${PORT}" | pfctl -a "userrules/${IP}_${PORT}_${PROTO}" -f -
echo "`date` Liberacao da porta ${PORT}/${PROTO} para o IP ${IP} ativada!" >> ${ARLOG}
fi
elif [ ${DIFTS} -gt ${TIMEOUT} ]; then
echo "`date` Primeira porta (${PRIMEIRAPORTAK}) foi acessada a mais de ${TIMEOUT} segundos. Operacao nao realizada." >> ${ARLOG}
else
echo "`date` Sequencia invalida de portas acessadas" >> ${ARLOG}
fi
else
echo "`date` Sem registros de conexoes desse IP ${IP}. Nada a fazer" >> ${ARLOG}
fi
# Removendo ${IP}
elif [ ${ACAO} == "delete" ]; then
pfctl -a "userrules/${IP}_${PORT}_${PROTO}" -F rules
if [ $? = 0 ]; then
echo "`date` IP ${IP} removido com sucesso!" >> ${ARLOG}
else
echo "Erro ao remover o IP ${IP}" >> ${ARLOG}
fi
else
echo "`date` Operacao invalida" >> ${ARLOG}
fi
Dê permissão de execução para o script:
chmod +x /var/ossec/active-response/bin/portknocking.sh
..::| Portas para liberação de conexão
Agora vamos criar as regras no pfSense para as portas que, quando tocadas na sequência correta, liberarão as portas do serviço SSH e a do HTTP. Essas regras serão de bloqueio, pois o que queremos é apenas logar os toques nelas.
Escolhi a sequência de portas 1111, 1000 e 2000, para liberar a porta 22/TCP, e as portas 11, 52 e 40 para liberar a porta 80/TCP. Todas com o protocolo TCP.
É recomendado usar, no mínimo, 3 portas e que não sejam sequênciais (por exemplo: 1000, 1001 e 1002) para cada liberação pois, em casos de descoberta da primeira por um atacante, as demais seriam meio óbvias de adivinhar. Também é necessário marcar a opção de logar essas regras, para que os toques sejam registrados no /var/log/filter.log.
Caso queira criar mais do que 3 portas de toques, basta criá-la(s) no pfSense. O script reconhece automaticamente quantas portas são necessárias nessa técnica.
Na descrição de cada regra há um padrão extremamente importante para que o script funcione corretamente. Sempre escreva “Port Knocking (número da regra no wazuh)” pois o script irá procurar nesse campo as portas correspondentes ao Port Knocking em questão. Cada regra no wazuh (que serão criadas mais tarde) corresponde a uma liberação.

Crie todas as regras tal como a que foi criada na imagem acima, para cada uma das 6 portas (1111, 1000, 2000 com a descrição “Port Knocking 100001” e 11, 52 e 40 com a descrição “Port Knocking 100002“), ficando a tabela de regras WAN assim:

Aqui você também define a ordem em que as portas devem ser tocadas. Então após a criação das regras, coloque-as na ordem em que elas devem ser tocadas. Mesmo que os toques acertem as 3 portas, se não forem tocadas na ordem correta o script não irá liberar a porta do serviço solicitado.
No exemplo acima não há outras regras além das desse artigo, mas se houvessem o script iria ignorá-las automaticamente para evitar que uma conexão legítima interfira na verificação dos toques.
No servidor Debian crie as seguintes regras no arquivo /var/ossec/etc/rules/local_rules.xml:
<rule id="100001" level="5">
<if_sid>87701</if_sid>
<dstport>2000</dstport>
<description>Port knocking da porta SSH padrao</description>
</rule>
<rule id="100002" level="5">
<if_sid>87701</if_sid>
<dstport>40</dstport>
<description>Port knocking da porta HTTP padrao</description>
</rule>

Acima foram criadas duas regras, uma para cada liberação (SSH e HTTP). Elas serão ativadas quando a última porta, da sequência correspondente ao serviço a ser liberado, for tocada pois assim significa que todas as anteriores necessárias já foram acessadas e, portanto, já logadas para serem lidas pelo script. Note que o número de cada regra (dentro de <rule id=”) corresponde a descrição das regras criadas no pfSense anteriormente.
Acrescente o comando e o active-response a seguir no arquivo /var/ossec/etc/ossec.conf, logo após o último </command>:
<command>
<name>portknocking-ssh</name>
<executable>portknocking.sh</executable>
<!-- Adicione as seguintes informacoes separadas por virgula: nome da interface no SO, nome da interface wan no pfsense, protocolo, porta que sera liberada,
tempo (em segundos) entre os toques das portas, portas extras a ignorar (separadas por pipe |) -->
<extra_args>em0,wan,tcp,22,120,22|80</extra_args>
<timeout_allowed>yes</timeout_allowed>
</command>
<command>
<name>portknocking-http</name>
<executable>portknocking.sh</executable>
<!-- Adicione as seguintes informacoes separadas por virgula: nome da interface no SO, nome da interface wan no pfsense, protocolo, porta que sera liberada,
tempo (em segundos) entre os toques das portas, portas extras a ignorar (separadas por pipe |) -->
<extra_args>em0,wan,tcp,80,120,80|22</extra_args>
<timeout_allowed>yes</timeout_allowed>
</command>
<active-response>
<command>portknocking-ssh</command>
<location>defined-agent</location>
<agent_id>002</agent_id>
<rules_id>100001</rules_id>
<disabled>no</disabled>
<timeout>60</timeout>
</active-response>
<active-response>
<command>portknocking-http</command>
<location>defined-agent</location>
<agent_id>002</agent_id>
<rules_id>100002</rules_id>
<disabled>no</disabled>
<timeout>60</timeout>
</active-response>

Veja que em <extra_args></extra_args> há informações importantes (separadas por vírgula) que precisam ser passadas ao portknocking.sh, que são:
- Nome da interface WAN reconhecida pelo FreeBSD

- Nome da interface WAN dada pelo pfSense

- Protocolo do serviço que será liberado
- Porta que será liberada
- Tempo máximo (em segundos) entre o toque na primeira porta da sequência e a última
- Portas que serão ignoradas na verificação dos toques. Deve-se colocar as portas que forem usadas em outras liberações.
A ordem em que essas informações são colocadas é vital para o correto funcionamento do script de liberação.
Além disso, outra configuração muito importante é quanto ao tempo em que a regra de liberação de conexão ao serviço ficará ativa. Dentro da tag <timeout></timeout> você estipula esse tempo em segundos.
Reinicie o wazuh-manager:
systemctl restart wazuh-manager
Se o pfSense foi recém instalado, os horários no /var/log/filter.log estarão diferentes da data do sistema operacional, gerando erro no script. Pra resolver, basta reiniciar o pfSense.
Lembrando que a regra de liberação criada após a sequência correta de toques nas portas do Port Knocking só é visível pelo comando pfctl, portanto não aparecerá no painel administrativo do pfSense, já que ela não foi criada via easyrules (porque através desse comando não é possível remover uma regra criada, por isso tive que usar o pfctl).
..::| Limitações
Quando o active-response do Wazuh é ativado para um determinado IP de origem, esse mesmo IP não consegue ativar outro active-response enquanto o primeiro ainda estiver ativo. Como o script é executado no pfSense, o mesmo não tem como avisar o Wazuh quando, por exemplo, uma sequência de portas forem tocadas na ordem errada, e assim evitar a execução do active-response. Ele é executado quando a última porta da sequência é tocada, mesmo que ela esteja errada, e ficará ativo até o seu timeout expirar. Portanto, caso erre a sequência uma vez, deve-se esperar o tempo configurado em que a liberação estivesse ativa, para então fazer uma nova tentativa (tags <timeout></timeout> no /var/ossec/etc/ossec.conf na seção do <active-response></active-response>).
Por causa dessa limitação, recomenda-se um timeout pequeno, de uns 5 minutos, só para fazer uma manutenção rápida remotamente. Caso precise de mais tempo, o recomendado é criar uma regra diretamente no pfSense liberando a conexão para o seu IP de origem.
Por conta do que foi dito acima, também não é possível liberar as portas 22/TCP e 80/TCP ao mesmo tempo via Port Knocking. Quando uma for ativada, deve-se esperar o timeout dela passar para fazer o Port Knocking na outra.
..::| Testes
De um sistema linux, instale o curl e dê o seguinte comando (certifique-se que esse linux alcance a interface WAN do pfSense):
curl --connect-timeout 1 http://10.0.0.200:1111 ; curl --connect-timeout 1 http://10.0.0.200:1000 ; curl --connect-timeout 1 http://10.0.0.200:2000 ; ssh root@10.0.0.200

Para verificar se a regra de liberação da porta do SSH foi criada com sucesso via pfctl, execute o comando no shell do pfSense:
pfctl -a "userrules/IP_PORTA_PROTOCOLO" -sr
Por exemplo, se o IP 10.0.0.182 pediu liberação da porta 22 do protocolo TCP, o comando acima ficará:
pfctl -a "userrules/10.0.0.182_22_tcp" -sr

No Windows, pode-se usar o PowerShell com o comando Test-NetConnection. Exemplo para a liberação da porta 22/TCP desse artigo:
Test-NetConnection -Port 1111 10.0.0.200
Test-NetConnection -Port 1000 10.0.0.200
Test-NetConnection -Port 2000 10.0.0.200
..::| Conclusão
Port Knocking é uma técnica que insere uma camada a mais de segurança para o acesso externo a sua rede. Com ele podemos evitar ataques de força bruta, por exemplo, e até ataques a aplicativos web em vulnerabilidades ainda não corrigidas. Não deixando uma porta sempre aberta para qualquer IP de origem bloqueia a detecção delas por escaneadores.
Se um administrador de rede está em uma situação em que não pode realizar uma conexão VPN a sua rede, ele poderá acessar o roteador se lembrar das 3 (ou mais) portas que devem ser tocadas antes da liberação de uma porta para um serviço administrativo desse roteador, e assim poderá criar outras regras para uma outra conexão, entre outras manutenções possíveis. O fato dessa porta administrativa ser liberada apenas para o IP de origem que efetuou os toques do Port Knocking, e com tempo dessa liberação ser pré-determinado, torna essa técnica excelente para que o seu firewall seja mais eficaz.
Em casos onde é possível estabelecer uma VPN, o Port Knocking também é útil pois poderá proteger a porta de conexão desse serviço. Não haveria necessidade de deixar essa porta sempre aberta para conexões. Nesse caso é recomendado criar um script, no lado do cliente, com os 3 (ou mais) toques já configurados para que se possa colocar um tempo maior de liberação (timeout) sem temer errar a sequência de portas, o que faria com que a porta do serviço solicitado ficasse inacessível pelo mesmo período em que estaria liberada. Geralmente uma VPN é necessária para períodos maiores de uso, por isso o timeout maior, nesse caso. Lembrando que o timeout vale para todos os active-response referente ao IP de origem.
O Wazuh tem a capacidade de enviar e-mails de alertas para o administrador quando uma regra é acionada, mediante configuração nela. Ou seja, é possível monitorar os acessos por Port Knocking, bastando apenas configurar as regras no Wazuh referente a essa técnica para que enviem e-mails sempre que ativadas.
Fontes:
https://documentation.wazuh.com/current/user-manual/capabilities/active-response/custom-active-response.html
https://forum.netgate.com/topic/147951/create-firewall-rules-by-script/4
https://www.openbsd.org/faq/pf/anchors.html
http://woshub.com/checking-tcp-port-response-using-powershell/