Contenu | Rechercher | Menus

Annonce

Si vous avez des soucis pour rester connecté, déconnectez-vous puis reconnectez-vous depuis ce lien en cochant la case
Me connecter automatiquement lors de mes prochaines visites.

À propos de l'équipe du forum.

#1 Le 27/01/2012, à 01: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.stats

listeDemons=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, à 17:03)


-- Lucid Lynx --

Hors ligne

#2 Le 27/01/2012, à 01: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 smile


On ne peut pas mettre d'array dans un string!
https://sputnick.fr/

Hors ligne

#3 Le 28/01/2012, à 04: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 : GirollServices libres : TdCT.org
Hide in your shell, scripts & astuces :  applications dans un tunnelsmart wgettrouver des pdfinstall. auto de paquetssauvegarde auto♥ awk
  ⃛ɹǝsn xnuᴉꞁ uʍop-ǝpᴉsdn

Hors ligne

#4 Le 28/01/2012, à 23:00

Totor

Re : [script] fork de denyHosts

nesthib a écrit :

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 yikes
j'en ai pour un bon moment à l’apprivoiser wink

nesthib a écrit :

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 lol


-- Lucid Lynx --

Hors ligne