Script : rapport détaillé des baux DHCP

Un petit script développer pour suivre les affectations d’adresses de plusieurs serveurs DHCP dans un environnement sensible. L’objectif était de fournir un rapport permettant d’identifier les machines se connectant à se réseau et de s’assurer que celle-ci ne change pas inopinément de site géographique (bastion).

Le script fonctionne avec un module complémentaire qui contient quelques fonctions pour transformer les masques de sous-réseaux du format binaire vers le format CIDR (la dernière fonction est de moi, les autres sont de Filip). Le module se nomme Library-IpConversionFunctions_v1.00.000.psm1 et contient le code suivant :

function Convert-CidrToBin ($cidr)
{
    if($cidr -le 32)
    {
        [Int[]]$array = (1..32)
        for($i=0;$i -lt $array.length;$i++)
        {
            if($array[$i] -gt $cidr){$array[$i]="0"}else{$array[$i]="1"}
        }
        $cidr =$array -join ""
    }
    return $cidr
}

function convert-IPv4fromBin($addressInBin)
{
    [string[]]$addressInInt32 = @()
    $addressInBin = $addressInBin.ToCharArray()
    for ($i = 0;$i -lt $addressInBin.length;$i++) 
    {
        $partAddressInBin += $addressInBin[$i] 
       if(($i+1)%8 -eq 0)
        {
            $partAddressInBin = $partAddressInBin -join ""
            $addressInInt32 += [Convert]::ToInt32($partAddressInBin -join "",2)
            $partAddressInBin = ""
        }
    }
    $addressInInt32 = $addressInInt32 -join "."
    return $addressInInt32
}

function Convert-IPv4toBin ($ipv4)
{
    $BinNum = $ipv4 -split '\.' | ForEach-Object {[System.Convert]::ToString($_,2).PadLeft(8,'0')}
    return $binNum -join ""
}

function convert-binToCidr ($subnetinBin)
{
    $cidr = 0
    for ($i=0;$i -lt 32;$i++)
    {
        if ($subnetinBin.substring($i,1) -eq 1) 
        { 
            $cidr++ 
        }
        else 
        { 
            $i = 32 #Sortie forcée
        }
    }
    return $cidr
}

Export-ModuleMember -Function *

le fichier module doit être placé au même niveau que le script lui-même. Vous devez également avoir créer un dossier output qui contiendra les fichiers de sortie.

Enfin, le script procède par analyse des sorties capturées depuis une commande NETSH ; il utilise également les zones reverses de votre DNS pour traduire l’IP en nom et les sites AD pour retrouver l’association géographique (ce point étant une particularité du contexte dans lequel il a été développé). A noté, l’envoie de mail ne fonctionne pas en l’état, nous utilisons un compilé particulier pour sécuriser les échanges…

<#  .SYNOPSIS
     Ce script parcours chaque scope des serveurs DHCP passes en argument et recupere les baux.
    
    .NOTES
     Date....: 09/05/2018
     Version.: 01.00.000
     Auteur..: Loic VEIRMAN
     Desc....: Creation

     Date....: 07/08/2018
     Version.: 01.01.000
     Auteur..: Loic VEIRMAN
     Desc....: Evolution pour inclure les données manquantes (nom de l'hote de chaque IP du lease, site AD associé)

    .DESCRIPTION
     Ce script se connecte avec la commande NETSH a chaque serveur DHCP passe en argument. Une fois la connexion etablie, il récupere tous les scopes du serveur, remet la sortie en forme et stocke les informations dans un tableau. Puis, pour chaque scope, il recupere la liste des baux qu'il stocke dans un nouveau tableau.
     
     Une fois la collecte terminee, les donnees sont utilisees pour generer un fichier CSV (option -CSV) et/ou un fichier HTLM (option -HTLM) qui sera envoye par email au(x) destinataire(s) specifie(s) (option -MailRecipients).

     Note importante : si aucune des options -CSV ou -HTML ne sont indiquees et que le script detecte des destinataires pour l'envoie par mail, un fichier CSV sera genere de force et envoye.

    .PARAMETER DHCPServers
     Liste des adresses ou noms des serveurs DHCP cible, separes par une virugle (,).

    .PARAMETER MailRecipients
     Liste des adresses mails des destinataire, separe par un point-virgule (;)
    
    .PARAMETER CSV
     Indique au script de generer le fichier CSV.
    
    .PARAMETER HTML
     Indique au script de generer le fichier HTML.
     
    .PARAMETER Progression
     Indique au script d'afficher la progression globale (non detaillee). Permet de visualiser le temps d'execution.     

    .PARAMETER Verbose
     Indique au script d'afficher les details de la progression. A utiliser a des fins de debug seulement.
#>

Param ([String]$ForestList="ms-sec.fr",
       [String]$MailRecipients=$null,
       [String]$MailObject,
       [Switch]$CSV,
       [Switch]$HTML,
       [Switch]$Progression,
       [Switch]$Verbose)

IPMO .\Library-IpConversionFunctions_v1.00.000.psm1

#/.DEBUG ----------------------------------------
#/.       ONLY ACTIVATE WHEN RUNNING THROUGH ISE
#/.      ----------------------------------------
#$CSV         = $True
#$HTML        = $True
#$Progression = $true
#$Verbose     = $True
#/.------ ----------------------------------------

#/.START
if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "Check des prerequis..." -ForegroundColor Yellow }
if (!(test-path .\Output)) 
{
    $null = mkdir .\Output
}

if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "Identification des serveurs DHCP..." -ForegroundColor Yellow }

#/.
#/....Le script va interroger chaque foret cible pour identifier les serveurs DHCP à joindre.
#/....Dans le même temps, il récupère les subnets et les sites de chaque foret et les stock dans un tableau.
#/....Pour retrouver le site depuis un subnet : ($siteSubnetCol | ? { $_.Subnets -eq '172.19.0.0/24' }).siteName
#/....Lors de la comparaison, il faudrait convertir le mask en notation Cidr.
#/.
$DHCPServers = $null

foreach ($forest in ($ForestList -split ","))
{
    $CnConfiguration   = (Get-ADRootDSE -Server $forest).ConfigurationNamingContext
    
    $ForestDhcpServers = Get-ADObject -Filter { objectclass -eq 'dhcpclass' -AND Name -ne 'dhcproot' } -SearchBase $CnConfiguration
    foreach ($ForestDhcpServer in $ForestDhcpServers) 
    {
        $DHCPServers += $ForestDhcpServer.Name + ","
    }

    #/.Sites AD
    if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "--> Recuperation des subnet pour les sites AD de $Forest..." -ForegroundColor Magenta }
    $siteContainerDN = "CN=Sites," + $CnConfiguration
    $siteObjs = Get-ADObject -SearchBase $siteContainerDN -filter { objectClass -eq "site" } -properties "siteObjectBL", name
    
    $siteSubnetCol =@()

    foreach ($siteObj in $siteObjs) 
    {
        $subnetArray = New-Object -Type string[] -ArgumentList $siteObj.siteObjectBL.Count
        $i = 0
        foreach ($subnetDN in $siteObj.siteObjectBL) 
        {
            $subnetName = $subnetDN.SubString(3, $subnetDN.IndexOf(",CN=Subnets,CN=Sites,") - 3)
            $subnetArray[$i] = $subnetName
            $i++
        }
    $siteSubnetObj = New-Object PSCustomObject | Select SiteName, Subnets
    $siteSubnetObj.SiteName = $siteObj.Name
    $siteSubnetObj.Subnets = $subnetArray
    $siteSubnetCol += $siteSubnetObj
    }
}
#/.
#/.....On nettoie l'input pour supprimer la derniere ","
#/.
$DHCPServers = $DHCPServers.Substring(0,$DHCPServers.length -1)

if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "Generation de la liste des cibles a auditer..." -ForegroundColor Yellow }
#/....Generation de la liste des cibles
#/....Les serveurs sont séparés par une virgule.
#/.
$DHCPSrvList = $DHCPServers -split ","

if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "Recuperation des scopes aupres des cibles..." -ForegroundColor Yellow }
#/....Recuperation des scopes
#/.
#/....La commande NETSH retourne un type String, il faut le diviser par ligne (`n)
#/....Le format de sortie fait que les 5 premieres et 3 dernières lignes ne sont pas utiles.
#/....Chaque ligne contient les infos sans espace, sauf le dernier champ (comment) : il faut
#/....Remplacer les chaines d'espace par rien, puis faire un split sur "-".
#/.
$TableScope = @()
foreach ($DHCPSrv in $DHCPSrvList)
{
    $DHCPSCope = (netsh dhcp server \\$DHCPSrv show scope) -Split "`n"
    for ($Line = 5 ; $Line -le ($DHCPScope.count - 4) ; $Line ++)
    {
        $LineContent = ($DHCPSCope[$Line] -replace '\s+','') -split '-'
        $LineScope   = $LineContent[0]
        $LineSMask   = $LineContent[1]
        $LineState   = $LineContent[2]

        $TmpPso = [PSCustomObject]@{"DHCPServer"  = $DHCPSrv
                                    "LeaseScope"  = $LineScope
                                    "LeaseSubnet" = $LineSMask
                                    "LeaseStatus" = $LineState }
        $TableScope += $TmpPso
    }
}

if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "Recuperation des baux pour chaque scope aupres des cibles..." -ForegroundColor Yellow }
#/....Recuperation des Leases
#.
#/....Pour chaque Scope, on interroge le serveur et on recupere les lease, que l'on ajoute
#/....a un tableau de collecte.
#/.
$TableLease = @()
foreach ($scope in $TableScope)
{
    if ($scope.LeaseStatus -eq "Active")
    {
        $TargetDhcp   = $scope.DHCPServer
        $TargetLease  = $Scope.LeaseScope
        
        $ActiveLease = (netsh dhcp server \\$TargetDhcp scope $TargetLease show clients) -split '`n'
        for ($Line = 8 ; $Line -le ($ActiveLease.count -5) ; $Line ++)
        {
            $LineData = $ActiveLease[$Line] -split "-"
            $ClientIP =  $LineData[0] -replace '\s+',''
            $ClientSN =  $LineData[1] -replace '\s+',''
            $ClientMC = ($LineData[2] -replace '\s+','') + ":" +  $LineData[3] + ":" + `
                         $LineData[4]                    + ":" +  $LineData[5] + ":" + `
                         $LineData[6]                    + ":" + ($LineData[7] -replace '\s+','')
            $LeaseExp =  $LineData[8] 
            Switch ($LineData[9])
            {
                "N"     { $NodeType = "N (NONE)"        }
                "D"     { $NodeType = "D (DHCP)"        }
                "B"     { $NodeType = "B (BOOTP)"       }
                "U"     { $NodeType = "U (UNSPECIFIED)" }
                "R"     { $NodeType = "R (RESERVATION)" }
                Default { $NodeType = $LineData[9] + " (Type is unknown from script)" }
            }
           
            #/.Update v01.01.000 : retrieve hostname from IP
            $ClientHostName = $null
            try     { $ClientHostName = ([system.net.dns]::GetHostEntry($ClientIP)).HostName }
            Catch   { $ClienthostName = "Unresolved" }
            Finally { if (-not($ClientHostName)) {$ClientHostName = "Unresolved" } }
            

            #/.Update v01.01.000 : on calcul le subnet AD puis on le retrouve dans le tableau
            $SearchSubnet = $scope.LeaseScope + "/" + (convert-binToCidr (Convert-IPv4toBin $scope.LeaseSubnet))
            $TargetAdSite = ($siteSubnetCol | ? { $_.Subnets -eq $SearchSubnet }).siteName
           
            if (-not($TargetAdSite)) { $TargetAdSite = "Unknown" }

            $TmpPso = [PSCustomObject]@{"DHCPServer"     = $scope.DHCPServer
                                        "LeaseScope"     = $scope.LeaseScope
                                        "LeaseSubnet"    = $scope.LeaseSubnet
                                        "AdSiteName"     = $TargetAdSite
                                        "ClientHostName" = $ClientHostName
                                        "ClientIP"       = $ClientIP
                                        "ClientMask"     = $ClientSN
                                        "ClientMac"      = $ClientMC
                                        "LeaseExpireOn"  = $LeaseExp
                                        "NodeType"       = $NodeType}
  
            $TableLease += $TmpPso
            $TargetAdSite = $null
        }       
    }
}

if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "Generation des fichiers CSV..." -ForegroundColor Yellow }
#/....Generation du fichier CSV
#/.
#/....On verifie si le fichier a ete demande. Si ce n'est pas le cas, on regarde si des 
#/....destinataires mails sont indique et on s'assure que l'option HTML est bien presente. Si
#/....le controle echoue, on force la generation du fichier CSV en forcant la variable $CSV.
if ($MailRecipients -ne $null -and !($CSV -or $HTML)) { $CSV = $True }
if ($CSV)
{
    #/....Deux fichiers CSV sont generes : un fichier pour les scopes et un fichier pour les 
    #/....baux.
    $TableScope | Sort-Object DHCPServer,LeaseScope          | Export-Csv .\Output\DHCP-ServersScope.csv -NoTypeInformation -Encoding UTF8 -Force
    $TableLease | Sort-Object DHCPServer,LeaseScope,ClientIP | Export-Csv .\Output\DHCP-ScopeIPLease.csv -NoTypeInformation -Encoding UTF8 -Force
}

if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "Generation du fichier HTML..." -ForegroundColor Yellow }
#/....Generation du fichier HTML
#/.
#/....Si demandé, un fichier HTML est genéré listant les scopes par serveur dans un premier 
#/....tableau (SCOPE - Scope For Server X - @ListScope@) puis les adresses IP delivrees dans 
#/....un second tableau (SCOPE - @ClientIPData@)
if ($HTML)
{
    #/.GLOBAL : pour tous les rapports
    $FormatHtml =  '<html><head><style>'
    $FormatHtml += '#LVE { font-family: Arial; border-collapse: collapse; width: 100%;}'
    $FormatHtml += '#LVE td,#LVE th { border: 1px solid #ddd;padding: 8px;}'
    $FormatHtml += '#LVE tr:nth-child(even){background-color: #f2f2f2;}'
    $FormatHtml += '#LVE tr:hover {background-color: #ddd;}'
    $FormatHtml += '#LVE th { padding-top: 12px;Padding-bottom: 12px; text-align: Left;background-color: #4CAF50;color:white;}'
    #/.SPECIFIQUE : SCOPE
    $FormatHtmA = $FormatHtml
    $FormatHtmA += '</style><p align="center"><font family="Arial" size="6"><b>EXTRACTION SCOPE DHCP DU ' + (Get-Date -Format "dd/MM/yyyy") + '</b></font></p>'
    $FormatHtmA += '</head><body>'
    $FormatHtmA | Out-File .\Output\Scope.html -Force
    foreach ($DHCPSrv in $DHCPSrvList)
    {
        $HtmlHeader = '<p align="left"><font family="arial" size="4" color="Blue"><b>SERVEUR : ' + $DHCPSrv + '</b></font></p>'
        $HtmlHeader | Out-File .\Output\Scope.html -Append

        $HtmlContent = $TableScope | ? { $_.DHCPServer -eq $DHCPSrv } | Select-Object LeaseScope,LeaseSubnet,LeaseStatus | Sort-Object LeaseScope | ConvertTo-Html 
        $HtmlContent = $HtmlContent -replace '<Table>','<Table id="LVE">'
        $HtmlContent = $HtmlContent -replace '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',''
        $HtmlContent = $HtmlContent -replace '<html xmlns="http://www.w3.org/1999/xhtml">',''
        $HtmlContent | Out-File .\Output\Scope.html -Append
        
        $HtmlJump_it = '<p><br /></p>'
        $HtmlJump_it | Out-File .\Output\Scope.html -Append
    }
    #/.SPECIFIQUE : LEASE
    $FormatHtmB = $FormatHtml
    $FormatHtmB += '</style><p align="center"><font family="Arial" size="6"><b>EXTRACTION BAUX DHCP DU ' + (Get-Date -Format "dd/MM/yyyy") + '</b></font></p>'
    $FormatHtmB += '</head><body>'
    $FormatHtmB | Out-File .\Output\IPAddressesList.html -Force
    foreach ($Scope in ($TableScope | Select-Object LeaseScope -Unique))
    {
        $HtmlHeader = '<p align="left"><font family="arial" size="4" color="Blue"><b>SCOPE : ' + $Scope.LeaseScope + '</b></font></p>'
        $HtmlHeader | Out-File .\Output\IPAddressesList.html -Append
        
        $HtmlContent = $TableLease | ? { $_.LeaseScope -eq $scope.LeaseScope } | Select-Object ClientHostName,AdsiteName,ClientIP,ClientMask,ClientMAC,DHCPServer,LeaseExpireOn,NodeType | Sort-Object ClientIP | ConvertTo-Html
        $HtmlContent = $HtmlContent -replace '<Table>','<Table id="LVE">'
        $HtmlContent = $HtmlContent -replace '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',''
        $HtmlContent = $HtmlContent -replace '<html xmlns="http://www.w3.org/1999/xhtml">',''
        if ($HtmlContent.count -le 8) 
            { 
                $HtmlContent = '<table><td colspan="6">No Data found.</td></table>'
            }
        $HtmlContent | Out-File .\Output\IPAddressesList.html -Append
        
        $HtmlJump_it = '<p><br /></p>'
        $HtmlJump_it | Out-File .\Output\IPAddressesList.html -Append
    }
}

if ($Progression -or $Verbose) { Write-Host (Get-Date -Format "hh:mm:ss`t") "Envoie du rapport par mail..." -ForegroundColor Yellow }
#/....Envoie du rapport par mail
#/.
if ($MailRecipients)
{
   if ($HTML) { $Msg = Get-Content .\Output\Scope.html }
         else { $Msg = "Veuillez trouver ci-joint l'extration au format CSV.`n`nCordialement,`tL'équipe DSV" }   
   $sendObj = $MailObject + " (scope)"
   #$null = .\smtpmail.exe -to $MailRecipients -Objet $sendObj -ContenuMsg $Msg -PJ '.\Output\DHCP-ServersScope.csv'

   if ($HTML) { $Msg = Get-Content .\Output\IPAddressesList.html }
         else { $Msg = "Veuillez trouver ci-joint l'extration au format CSV.`n`nCordialement,`tL'équipe DSV" }   
   $sendObj = $MailObject + " (Lease IP)"
   #$null = .\smtpmail.exe -to $MailRecipients -Objet $sendObj -ContenuMsg $Msg -PJ '.\Output\DHCP-ScopeIPLease.csv'
}

#/.END
Remove-Module Library-IpConversionFunctions_v1.00.000
Exit 0

J’espère qu’il vous sera utile !

Lien Permanent pour cet article : https://ms-sec.fr/?p=2765

Laisser un commentaire

Your email address will not be published.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.