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.

É muito importante colocar a descrição da regra corretamente, seguindo o padrão acima descrito

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:

A ordem das regras será a sequência em que cada porta deverá ser tocada

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>
Note que em <dstport> foram colocadas apenas as últimas portas das sequências de cada liberação

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
Veja que, após os 3 toques feitos pelo comando curl, o prompt de autenticação SSH do pfSense é mostrado

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/

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *