¿Cómo prohibir palabras en contraseñas en un AD Onpremise?

En consultorías y evaluaciones de Ethical Hacking en entornos Active Directory, una recomendación común es prohibir el uso de palabras clave en las contraseñas de los usuarios del dominio, como el nombre de la empresa, nombre de ciudades, etc. Si bien esto es relativamente sencillo de implementar en Azure, la mitigación en instalaciones On-Premise puede ser más compleja. Aunque existen soluciones comerciales, en este artículo explicaremos cómo realizar esta implementación de forma autónoma y sin costos adicionales. Es fundamental recordar que cualquier cambio en entornos productivos debe ser precedido por una fase de pruebas exhaustiva.

HOW TO

En microsoft podemos encontrar documentación de como implementar una política de contraseñas customizada, generando una DLL (c++) para incluirla en la llave de registro de LSA\Notification Package en nuestro Controlador de dominio en el siguiente enlace:

https://learn.microsoft.com/en-us/windows/win32/secmgmt/installing-and-registering-a-password-filter-dll

Utilizando la documentación y Visual Studio, vamos a crear un nuevo proyecto seleccionando la plantilla de "Biblioteca de vínculos dinámicos (DLL)" Para esto debes tener instalado en visual studio el soporte para proyectos c++

Luego de haber creado el proyecto, sin tomar encuenta el archivo principal generado por Visual Studio, creamos un nuevo archivo, que contendrá la lógica de nuestras validaciones:

En nuestro caso lo nombramos como PasswordPolicy.cpp

y agregamos el siguiente código fuente:

#include <windows.h>
#include "pch.h"
#include <string>
#include <iostream>
#include <fstream>
#include <ntsecapi.h>
#include <algorithm>
#define STATUS_PASSWORD_CONTAINS_FORBIDDEN_WORD 0xC00000E0
 
std::string UnicodeToString(PUNICODE_STRING unicodeString) {
    if (unicodeString == nullptr || unicodeString->Length == 0) {
        return "";
    }
    int length = unicodeString->Length / sizeof(WCHAR);
    std::wstring wideString(unicodeString->Buffer, length);
    std::string result(wideString.begin(), wideString.end());
    return result;
}
extern "C" __declspec(dllexport) BOOLEAN __stdcall PasswordFilter(
    PUNICODE_STRING AccountName,
    PUNICODE_STRING FullName,
    PUNICODE_STRING Password,
    BOOLEAN SetOperation
) {
    std::string password = UnicodeToString(Password);
    std::string passwordLower = password;
    std::transform(passwordLower.begin(), passwordLower.end(), passwordLower.begin(), ::tolower);
    std::string line;
    std::ifstream file("blacklist_pwd.txt");
    if (file.is_open()) {
        while (getline(file, line)) {
            std::string forbiddenWordLower = line;
            std::transform(forbiddenWordLower.begin(), forbiddenWordLower.end(), forbiddenWordLower.begin(), ::tolower);
            if (passwordLower.find(forbiddenWordLower) != std::string::npos) {
                SetLastError(LsaNtStatusToWinError(STATUS_PASSWORD_CONTAINS_FORBIDDEN_WORD));
                return FALSE;
            }
        }
        file.close();
    }
    int passwordLength = Password->Length / sizeof(WCHAR);
    if (passwordLength < 11) {
        return FALSE;
    }
    return TRUE;
}
extern "C" __declspec(dllexport) BOOLEAN __stdcall InitializeChangeNotify(void) {
    return TRUE;
}

En la primera sección, la contraseña recibida como parámetro al invocar la DLL se convierte a una cadena de texto y posteriormente a minúsculas. Este paso asegura que la comparación con las palabras prohibidas (también en minúsculas) sea insensible a mayúsculas y minúsculas. Después se realizara un loop del archivo "blacklist_pwd.txt" leyendo linea a linea el contenido y buscando el contenido dentro de la nueva contraseña, si se encuentra en alguna linea de las palabras prohibidas, el loop se cortara provocando un Return False, indicando que la contraseña no cumple con la política de credenciales.

La segunda sección solo verifica el largo de la contraseña, esperando que esta sea mayor a 11 y se incorpora solo como prueba de que se pueden establecer los controles que queramos en la DLL mediante lógicas de programación.

Despues de compilar la DLL en modo Release, crearemos nuestro archivo "blacklist_pwd.txt" en la carpeta system32 del controlador de dominio

En este caso agregamos las siguientes palabras:

  • Santiago
  • Colocolo
  • password
  • Banco
  • Empresaname
  • causa

Ahora unos últimos ajustes en la configuración del controlador de dominio:

1. Agregamos nuestra DLL compilada a system32 en el controlador de dominio.

2. Abrimos el editor de registros en nuestro controlador de dominio, dirigiéndonos a la siguiente ruta "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa" y editamos la llave de registro "Notificacion Packages", dentro de la llave agregamos el nombre de nuestra DLL, sin la extension .dll, en nuestro caso queda como "DllPassword_v3".

Finalmente realizamos las pruebas para ver si nuestra política esta filtrando las palabras

como podemos ver, las credenciales que contenían palabras prohibidas no cumplen con la política y no se permite el cambio o generación de credencial del usuario. :)

Así es como podemos ir implementado controles customizados sin recurrir a herramientas de pago, mas adelante podríamos incluir la verificación de contraseñas filtradas en base a algún diccionario como rockyou u otras alternativas.

Recuerden que estos cambios deben ser testeados en ambientes de prueba antes de pasar a cambios en productivo, ademas de incluir algunas recomendaciones de microsoft en el manejo de las credenciales en los filtros Consideraciones en filtros de contraseñas.

Referencias

-Instalando y registrando un filtro de contraseñas

-Gracias a Chixo por las pruebas y el entorno