Saltar al contenido
MS Scripts

Scripts de Exchange

Buzones con reenvío de correo

Encuentra los buzones que reenvían correo y marca los que lo mandan fuera de tus dominios aceptados. Un reenvío externo es señal de cuenta comprometida o de fuga de datos.

Solo lecturaSin permisos de GraphProbado con respuestas simuladas. Sin ejecutar en un tenant real.

Antes de ejecutarloRevisa solo el reenvío configurado en el buzón: las reglas de bandeja de entrada no se ven aquí. Si un buzón tiene ForwardingAddress y ForwardingSmtpAddress, Exchange usa ForwardingAddress; el otro se reporta como alerta. Una política de correo no deseado saliente con el reenvío automático en Off bloquea el reenvío externo aunque aparezca aquí, pero la configuración sigue siendo una señal que vale la pena investigar. "Revisar destino" significa que el reenvío apunta a algo que no es un buzón, como un usuario de correo o una lista, que puede entregar fuera de la organización aunque su dirección sea interna.

Ejemplo en México

Distribuidora de materiales en Tijuana, 90 personas

Un cliente recibió una factura con otra cuenta bancaria. Sospechan que alguien lee el correo de cobranza desde fuera de la empresa.

.\get-reenvios-de-correo.ps1 -RutaCsv .\reenvios.csv

Obtiene un reenvío de cobranza hacia una cuenta externa sin copia en el buzón, lo quita y abre la investigación de esa cuenta.

Cómo implementarlo

  1. Instala el módulo si no lo tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser.
  2. Ejecuta el script con -RutaCsv e inicia sesión con una cuenta que tenga el rol indicado.
  3. Filtra la columna Alcance por Externo y revisa también la columna Alertas.
  4. Pregunta a cada usuario si conoce ese reenvío. Si no lo reconoce, trátalo como incidente: cambia la contraseña, cierra las sesiones y quita el reenvío.
Ver el código completo de get-reenvios-de-correo.ps1
<#
.SYNOPSIS
    Responde qué buzones reenvían correo y cuáles lo mandan fuera de tu organización.

.DESCRIPTION
    Revisa ForwardingAddress, ForwardingSmtpAddress y DeliverToMailboxAndForward de cada buzón y marca como externos
    los destinos fuera de tus dominios aceptados, y como "Revisar destino" los que no son un buzón. Un reenvío externo es señal de cuenta comprometida o de fuga de datos.
    Si un destino no se puede resolver, queda en el reporte y el script termina con error al final, después de guardar el CSV.
    Solo lectura: no cambia nada en el tenant.

.PARAMETER Tipo
    Tipo de buzón: UserMailbox, SharedMailbox, RoomMailbox, EquipmentMailbox o Todos (por defecto).

.PARAMETER RutaCsv
    Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.

.EXAMPLE
    .\get-reenvios-de-correo.ps1 -RutaCsv .\reenvios.csv

.NOTES
    Permisos: ninguno de Graph; usa ExchangeOnlineManagement.
    Rol mínimo: Lector global (View-Only Organization Management en Exchange Online).
    Licencia: Exchange Online Plan 1 o superior, incluido en Microsoft 365 Business Premium.
    Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas.
#>
#Requires -Version 7.0
[CmdletBinding()]
param(
    [ValidateSet('UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox', 'Todos')]
    [string]$Tipo = 'Todos',

    [string]$RutaCsv
)

$ErrorActionPreference = 'Stop'

if (-not (Get-Command Connect-ExchangeOnline -ErrorAction SilentlyContinue)) {
    throw 'Falta el módulo. Instálalo con: Install-Module ExchangeOnlineManagement -Scope CurrentUser'
}

function Limpiar-Fila($fila) {
    foreach ($p in $fila.PSObject.Properties) {
        if ($p.Value -is [string] -and $p.Value -match '^[=+\-@\t\r]') { $p.Value = "'$($p.Value)" }
    }
    $fila
}

function Test-Externo($correo, $dominios) {
    $dominio = ("$correo" -split '@')[-1]
    -not ($dominios | Where-Object { $dominio -like $_ })
}

# El CSV se vacía desde el inicio: si algo truena o no hay resultados, no queda uno viejo que parezca actual.
if ($RutaCsv) { Set-Content -LiteralPath $RutaCsv -Value $null -Encoding utf8BOM }

$tipos = if ($Tipo -eq 'Todos') { 'UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox' } else { $Tipo }
$filas = [System.Collections.Generic.List[object]]::new()
$fallas = [System.Collections.Generic.List[string]]::new()

Connect-ExchangeOnline -ShowBanner:$false
try {
    $dominios = @(Get-AcceptedDomain -ErrorAction Stop | ForEach-Object { "$($_.DomainName)" })
    # Sin dominios aceptados todo destino parecería externo: mejor detenerse que dar una falsa alarma.
    if (-not $dominios) { throw 'No se pudieron leer los dominios aceptados del tenant.' }

    $buzones = Get-EXOMailbox -RecipientTypeDetails $tipos -ResultSize Unlimited -Properties ForwardingAddress, ForwardingSmtpAddress, DeliverToMailboxAndForward -ErrorAction Stop |
        Where-Object { $_.ForwardingAddress -or $_.ForwardingSmtpAddress }

    foreach ($b in @($buzones)) {
        $smtp = "$($b.ForwardingSmtpAddress)" -replace '^smtp:'
        $tipoDestino = ''
        $destino = $smtp
        $alcance = $null
        $alertas = @()
        try {
            if ($b.ForwardingAddress) {
                # ForwardingAddress manda sobre ForwardingSmtpAddress, y puede apuntar a un contacto con correo externo.
                $destinatario = @(Get-EXORecipient -Identity "$($b.ForwardingAddress)" -Properties PrimarySmtpAddress, RecipientTypeDetails -ErrorAction Stop)
                if ($destinatario.Count -ne 1) { throw "El destino $($b.ForwardingAddress) no corresponde a un solo destinatario" }
                $destino = "$($destinatario[0].PrimarySmtpAddress)"
                if (-not $destino) { throw "No se pudo resolver el destinatario $($b.ForwardingAddress)" }
                $tipoDestino = "$($destinatario[0].RecipientTypeDetails)"
                if ($smtp -and (Test-Externo $smtp $dominios)) { $alertas += "ForwardingSmtpAddress externo sin efecto hoy ($smtp): investiga quién lo puso" }
            }
            $alcance = if (Test-Externo $destino $dominios) { 'Externo' }
                # Un usuario de correo o una lista con dirección interna pueden entregar fuera de la organización.
                elseif ($b.ForwardingAddress -and $tipoDestino -notmatch 'Mailbox$') { 'Revisar destino' }
                else { 'Interno' }
            if ($alcance -eq 'Revisar destino') { $alertas += "El destino no es un buzón ($tipoDestino) y puede entregar fuera de la organización" }
        } catch {
            $fallas.Add("$($b.PrimarySmtpAddress)")
            $alcance = "No se pudo consultar: $_"
        }
        if ($alcance -eq 'Externo') { $alertas += 'Reenvío externo: revisa si la cuenta está comprometida o si hay fuga de datos' }
        if ($b.DeliverToMailboxAndForward -ne $true) { $alertas += 'No deja copia en el buzón' }

        $filas.Add([pscustomobject]@{
            Buzon                 = $b.DisplayName
            Correo                = $b.PrimarySmtpAddress
            Tipo                  = $b.RecipientTypeDetails
            DestinoEfectivo       = $destino
            Alcance               = $alcance
            GuardaCopia           = if ($b.DeliverToMailboxAndForward -eq $true) { 'Sí' } else { 'No' }
            ForwardingAddress     = "$($b.ForwardingAddress)"
            ForwardingSmtpAddress = "$($b.ForwardingSmtpAddress)"
            Alertas               = $alertas -join '; '
        })
    }
} finally {
    Disconnect-ExchangeOnline -Confirm:$false
}

$salida = @($filas | Sort-Object Alcance, Correo)
Write-Host "Buzones con reenvío: $($salida.Count). Externos: $(@($salida | Where-Object Alcance -EQ 'Externo').Count)."
if ($RutaCsv) {
    if ($salida.Count) {
        # UTF-8 con BOM para que Excel en español muestre bien los acentos.
        $salida | ForEach-Object { Limpiar-Fila $_ } | Export-Csv -LiteralPath $RutaCsv -NoTypeInformation -Encoding utf8BOM
    }
    Write-Host "CSV guardado en $RutaCsv"
} else {
    $salida | Format-Table -AutoSize
}

if ($fallas.Count) { throw "No se pudieron resolver $($fallas.Count) reenvíos: $($fallas -join ', '). Revisa las filas marcadas en el reporte." }

Parte del playbook

Relacionados