Pages : 1
#1 Le 27/01/2012, à 00:54
- Totor
[script] fork de denyHosts
Comme tout à chacun ayant un serveur ssh accessible depuis l'extérieur, j'ai été confronté à des tentatives de connexion malveillantes.
C'est pourquoi, sans le savoir j'ai créer un fork du soft denyHosts.
Le script produit analyse les logs auth.log et si quelqu'un tente de se connecter un certain nombre de fois sans succès à la machine, son IP / hostname sera interdite automatiquement (inscription dans le fichier /etc/hosts.deny) lors de toute tentative de connexion via ssh.
L'avantage de mon script est qu'il peut-être configuré pour vérifier d'autres types de connexion (ftp ...)
Le script se base sur un fichier de conf qui doit se trouver dans le dossier /etc/<nom_script_sans_extension>/<nom_script_sans_extension>.conf
Un exemple de conf :
############################################################
# Fichier de conf pour le script de détection de connexion #
############################################################fichHostDeny=/etc/hosts.deny
fichLogAuth=/var/log/auth.log
fichStats=/etc/forbid/forbid.statslisteDemons=sshd
# formalisme pour détecter une tentative de connexion
# demon=dernièreSeconde nombreMaxCnx N°colonne hostname/IP 'Pattern de détection'
sshd=0 5 -4 'Failed password for '
--> 0 = Valeur gérée par le script. Pour une 1ere utilisation, laisser 0
--> 5 = nombre de tentative max avant bannissement
--> 4 et le pattern --> Ne pas toucher
le fichier pointé par la clef fichStats est à adapter en fonction du nom du script que vous donnerez
le script (en zsh) :
#!/bin/zsh
# nom du script
myFullName="${0:t}"
myName="${0:t:fr}" # sans extension
(( $(id -u) > 0 )) && showError 1 "Vous devez être root pour lancer ${myFullName} !"
pids=( ${(f)$(ps -o pid -C "${myFullName}" --no-headers)} )
(( ${#pids[@]} > 1 )) && showError 1 "${myfullName} est déjà en cours d'exécution !"
# Affiche un message d'erreur formaté
showError()
{
# on affiche sur la sortie d'erreur
exec >&2
myDate="$(date +'%T %d/%m/%Y')"
# le statut est en 1er paramétre
statut=$1
shift
# formatage de l'affichage de l'erreur
erreur="$(printf "%s\n\t" "$@")"
cat <<EO_ERROR
${myDate} - Erreur : ${erreur%$'\n'*}
Satut : ${statut}
EO_ERROR
exit ${statut}
}
# Affiche un message d'erreur formaté
showWarning()
{
myDate="$(date +'%T %d/%m/%Y')"
# formatage de l'affichage du warning
warning="$(printf "%s\n\t" "$@")"
cat <<EO_WARN
${myDate} - Warning : ${warning%$'\n'*}
EO_WARN
}
# Affiche d'un message d'information formaté
showInfo()
{
myDate="$(date +'%T %d/%m/%Y')"
local infos="$(printf "%s\n\t" "$@")"
printf "${myDate} : %s" "${infos%$'\t'}"
}
# fichier de configuration du script
myConf="/etc/${myName}/${myName}.conf"
# Nom des clefs à charger
KEY_STAT=fichStats
KEY_HOSTS=fichHostDeny
KEY_AUTH=fichLogAuth
KEY_DEMONS=listeDemons
typeset -A keys
set -A keys ${KEY_STAT} "" ${KEY_HOSTS} "" ${KEY_AUTH} "" ${KEY_DEMONS} ""
#tableaux associatif contenant la liste des infos sur les démons
typeset -A dates # dernière date traitée
typeset -A maxCnx # max cnx authorisé
typeset -A hostPos # position du nom de l'hote ou de son IP dans la ligne
typeset -A patterns # pattern permettant de détecter une erreur de connexion
#tableau associatif contenant les compteurs de cnx par ip/host
typeset -A hostsCmpt
# lecture du fichier de configuration
readKeys()
{
fichier="$1"
# chargement du fichier dans un tableau
lignes=( ${(f)"$(<"${fichier}")"} )
shift
# pour chacune des clefs recherchées
for key in "${(k@)keys}"
do
# on recherche dans le fichier la première occurence de la clef
ligne="${lignes[(r)[[:space:]]#${key}=*]}"
[[ -z "${ligne}" ]] && showError 2 "Clef ${key} non trouvée dans le fichier ${fichier}."
# la clef est trouvée, on l'affecte
keys[${key}]="${ligne#*=}"
showInfo "Clef trouvée : ${key} (${keys[${key}]})"
done
# recherche des démons à traiter
for demon in ${=keys[${KEY_DEMONS}]}
do
ligne="${lignes[(r)[[:space:]]#${demon}[[:space:]]#=*]}"
[[ -z "${ligne}" ]] && {
showWarning "Démon ${demon} non trouvé dans le fichier ${fichier} >> Ignoré."
# on passe au démon suivant
continue
}
infos=( ${(z)"${ligne#*=}"} )
dates[${demon}]=${infos[1]}
maxCnx[${demon}]=${infos[2]}
hostPos[${demon}]=${infos[3]}
patterns[${demon}]=${(Q)infos[4]}
showInfo "Démon trouvé : ${demon} (${infos[*]})"
done
unset lignes
}
# effectue divers tests sur un fichier
# $1 = nom du fichier
# $2 = 1=le fichier doit exister (defaut). 0=pas forcément
# $3 = doit-on pouvoir y acceder en écriture (0=non, 1=oui=default)
# $4 = doit-on le créer s'il n'existe pas (0=non (defaut), 1=oui)
testFichier()
{
local fichier="$1"
local exist="${2-1}"
local writable="${3-1}"
local create="${3-0}"
[[ -a "${fichier}" ]] && {
# si le fichier existe ce doit être un fichier régulier
[[ ! -f "${fichier}" ]] && showError 1 "${fichier} n'est pas un fichier."
}
if [[ -f "${fichier}" ]]; then
# il s'agit d'un fichier régulier, il doit être accessible en lecture
[[ ! -r "${fichier}" ]] && showError 1 "Le fichier ${fichier} n'est pas accessible en lecture."
# doit-on pouvoir y acceder en écriture
((${writable} > 0)) && {
[[ ! -w "${fichier}" ]] && showError 1 "Le fichier ${fichier} n'est pas accessible en écriture."
}
else
# le fichier n'existe pas mais doit exister
[[ ${exist} -eq 1 ]] && showError 1 "${fichier} n'existe pas."
fi
[[ ${create} -eq 1 ]] && {
# on demande la création du fichier, on le fait uniquement si le fichier n'existe pas
[ ! -f "${fichier}" ] && {
error="$(touch "${fichier}" 2>&1)" || showError 1 "Impossible de créer le fichier ${fichier}." "${error}"
# seul root peut le modifier et le lire
chmod og-rwx,u+rw "${fichier}"
}
}
}
# chargement des statistiques de connexion refusées
initStats()
{
local fichier="${keys[${KEY_STAT}]}"
testFichier "${fichier}" 0 1 1
# chargement du fichier
lignes=( ${(f)"$(<"${fichier}")"} )
for ligne in ${lignes[@]}
do
# formalisme attendue : <demon sans #>:<quelquechose sans #>:<nombre>
[[ ${ligne} == ([[:WORD:]]~\#)##:([[:WORD:]]~\#)##:[[:digit:]]## ]] && {
infos=( ${(s@:@)ligne} )
hostsCmpt[${infos[1,2]}]=${infos[3]}
}
done
unset lignes
}
# vérification qu'une ligne correspond à une connexion refusée
# si oui, on effectue les traitements de statistiques
checkLigne()
{
local demon="$1"
local ligne="$2"
[[ ${ligne} == *${demon}\[[[:digit:]]##\]:*${patterns[${demon}]}* ]] && {
# il s'agit d'une ligne d'intrusion, on vérifie que la date ne soit pas déjà traitée
# récupération de la date
maDate=${ligne%% ${HOST}*}
maDate=$(date -u +'%s' -d"${maDate}")
((maDate > dates[${demon}])) && {
# tentative d'intrusion postérieur au dernier traitement,
# il faut mettre les indicateurs à jour et vérifier si plafond atteint
finLigne="${ligne##*${demon}\[[[:digit:]]##\]:[[:space:]]##}"
leHost=${${=finLigne}[${hostPos[${demon}]}]}
lesHosts="${leHost}"
# s'il s'agit d'une @ip, on recherche l'host correspondant
[[ "${leHost}" == [[:digit:]]##.[[:digit:]]##.[[:digit:]]##.[[:digit:]]## ]] && {
unHost="${${=$(host ${leHost})}[-1]%.}" 2>/dev/null && { lesHosts="${unHost} ${leHost}"; leHost="${unHost}"; }
}
# mise à jour de la dernière date traitée pour ce démon
dates[${demon}]=${maDate}
# mise à jour du fichier de configuration
sed -i "/[[:space:]]*${demon}[[:space:]]*=/ s@.*@${demon}=${dates[${demon}]} ${maxCnx[${demon}]} ${hostPos[${demon}]} ${(qq)patterns[${demon}]}@" "${myConf}"
# on incrémente le compteur pour le host concerné
((hostsCmpt[${demon} ${leHost}]++))
# mise à jour du fichier stat
if grep -q "^[[:space:]]*${demon}[[:space:]]*:[[:space:]]*${leHost}[[:space:]]*:[[:space:]]*" "${keys[${KEY_STAT}]}"; then
# l'entrée existe, on la met à jour
showInfo "Tentative de connexion de ${lesHosts} sur le demon ${demon} (${hostsCmpt[${demon} ${leHost}]})"
sed -i "/^[[:space:]]*${demon}[[:space:]]*:[[:space:]]*${leHost//\//\/}[[:space:]]*:[[:space:]]*/ s@.*@${demon}:${leHost}:${hostsCmpt[${demon} ${leHost}]}@" "${keys[${KEY_STAT}]}"
else
# il n'y a aucune entrée dans le fichier des stats, on l'ajoute
showInfo "Ajout de '${demon}:${lesHosts}:${hostsCmpt[${demon} ${leHost}]}' dans le fichier ${keys[${KEY_STAT}]}..."
printf "%s:%s:%s\n" "${demon}" "${leHost}" "${hostsCmpt[${demon} ${leHost}]}" >> "${keys[${KEY_STAT}]}"
fi
(( hostsCmpt[${demon} ${leHost}] > maxCnx[${demon}])) && {
# ce host a atteint le max de connexion, il faut le bannir uniquement s'il ne l'est pas déjà
grep -q "^[[:space:]]*${demon}[[:space:]]*:[[:space:]]*${leHost}" "${keys[$KEY_HOSTS]}" || {
showInfo "Ajout de '${lesHosts}' pour le démon ${demon} dans ${keys[$KEY_HOSTS]}..."
printf "%s: %s\t#%s\n" "${demon}" "${lesHosts}" "$(date +'%T %d/%m/%Y')" >> "${keys[$KEY_HOSTS]}"
sync
}
}
}
return 1
}
}
# lance le traitement de surveillance
run()
{
local fichier="${keys[${KEY_AUTH}]}"
testFichier "${fichier}" 1 0 0
while read
do
for demon in ${=keys[${KEY_DEMONS}]}
do
# vérification de la ligne pour le démon donné. Si tentative connexion, on passe à la ligne suivante
checkLigne "${demon}" "${REPLY}" || break
done
done < <(tail -f -n +1 "${fichier}")
}
# on log tout
rm /var/log/${myName}.*.log
exec >/var/log/${myName}.$$.log 2>&1
# activation du glob etendu
setopt EXTENDED_GLOB
# vérification de l'existence du fichier de conf
testFichier "${myConf}" 1 1
readKeys "${myConf}"
testFichier "${keys[${KEY_HOSTS}]}" 1 1
initStats
run
A utiliser en tant que root ...
enjoy
EDIT :
- 11/02/2012 : Correction d'un bug suite upgrade zsh en 4.3.15
Dernière modification par Totor (Le 11/02/2012, à 16:03)
-- Lucid Lynx --
Hors ligne
#2 Le 27/01/2012, à 00:59
- sputnick
Re : [script] fork de denyHosts
Pour info il existe déjà fail2ban qui a l'avantage d'être utilisé par des milliers de users de par le monde
Il existe un tunnel obscur dans la lumière infinie. Lao-Tseu
https://sputnick.fr
Hors ligne
#3 Le 28/01/2012, à 03:55
- nesthib
Re : [script] fork de denyHosts
plop les amis o/
tiens tu es passé à zsh Totor ?
sinon +1 pour fail2ban qui fonctionne vraiment très bien et gère plein de situations différentes
GUL Bordeaux : Giroll – Services libres : TdCT.org
Hide in your shell, scripts & astuces : applications dans un tunnel – smart wget – trouver des pdf – install. auto de paquets – sauvegarde auto – ♥ awk
⃛ɹǝsn xnuᴉꞁ uʍop-ǝpᴉsdn
Hors ligne
#4 Le 28/01/2012, à 22:00
- Totor
Re : [script] fork de denyHosts
plop les amis o/
tiens tu es passé à zsh Totor ?
exactement...
je voulais découvrir autre chose et je ne suis pas déçu...
nettement plus complet et nettement plus puissant ce zsh
j'en ai pour un bon moment à l’apprivoiser
sinon +1 pour fail2ban qui fonctionne vraiment très bien et gère plein de situations différentes
+1 aussi mais bon ... à ma grande habitude, je ne cherche pas les outils, je préfère les créer ... ça m'occupe, je n'ai que ça à faire
-- Lucid Lynx --
Hors ligne
Pages : 1