1. Lo primero que hay que tener claro
Cuando se crea una aplicación o un servicio de Windows, surge la necesidad de “ejecutar solo esta parte del procesamiento como otro usuario”.
Por ejemplo, en situaciones como estas:
- Un servicio de Windows necesita acceder a un servidor de archivos con los privilegios del propio usuario
- Una aplicación de administración necesita comprobar únicamente lo que un usuario concreto puede ver
- En Named Pipe, RPC, COM, IIS, ASP.NET Core, etc., se necesita realizar parte del procesamiento con los privilegios del usuario que hizo la llamada
- Por motivos de aprovechamiento de activos existentes, se necesita cambiar la cuenta de Windows utilizada según la unidad de procesamiento
Aquí es donde aparecen la suplantación, el token de acceso y el token de suplantación.
Sin embargo, antes de nada quiero subrayar lo siguiente.
La suplantación en Windows no es “una magia para convertirse en administrador”. Es un mecanismo que cambia, principalmente por hilo, el contexto de seguridad que se usa en las comprobaciones de acceso.
Si se implementa sin tener clara esta diferencia, aparecen problemas como estos:
- Se cree haber suplantado, pero el acceso a archivos devuelve
Access denied - Se pueden leer los archivos locales, pero solo falla el recurso compartido de red
- En algún punto de
Task.Runo de unasync, sin darse cuenta se vuelve a ejecutar como el usuario original - La suplantación sigue activa durante el registro de logs o el procesamiento posterior, y el límite de privilegios se vuelve ambiguo
- Se confunde el token primario con el token de suplantación y falla el inicio del proceso
- Se llega a un punto muerto con “el usuario está en Administrators, entonces por qué no puede escribir”
En este artículo se ordenan las ideas necesarias para manejar con seguridad, en el trabajo real, los tokens de suplantación de Windows.
No es un artículo sobre técnicas de ataque ni sobre apropiación de privilegios. Es un artículo sobre cómo manejar correctamente los límites de privilegios en aplicaciones de Windows, servicios de Windows y aplicaciones .NET.
El código que aparece en este artículo se publica en GitHub como un conjunto de ejemplos que se puede compilar (una biblioteca, una demo para ejecutar en Windows y pruebas unitarias de validación de argumentos y de comportamiento de las protecciones).
windows-impersonation-token - komurasoft-blog-samples (GitHub)
2. Qué es un token de acceso
En Windows, el token de acceso se usa para representar el contexto de seguridad de un usuario o de un proceso.
El token de acceso incluye, a grandes rasgos, esta información. Como son términos que aparecen repetidamente en los capítulos siguientes, se añade el significado línea por línea.
| Elemento | Significado |
|---|---|
| SID del usuario | SID es la abreviatura de Security Identifier (identificador de seguridad), un identificador único de un usuario o un grupo. Las comprobaciones de acceso se hacen con este valor, no con el nombre que se muestra |
| Grupos a los que pertenece | El conjunto de SID de los grupos a los que pertenece el usuario. Aquí también se refleja si pertenece a Administrators |
| Privilegios (Privilege) | Privilegios como “operador de copia de seguridad” o “apagar el sistema”, que se otorgan aparte de la ACL de cada objeto individual. Tienen un estado habilitado o deshabilitado |
| Propietario predeterminado | El SID que será el propietario de los objetos nuevos creados con este token |
| DACL predeterminada | DACL es la abreviatura de Discretionary Access Control List (lista de control de acceso discrecional), una lista que indica a quién se le permite qué. Se asigna por defecto a los objetos recién creados |
| SID restringidos | La lista de SID que se usa en los tokens restringidos. Sobre esta lista también se realiza una comprobación de acceso adicional, por lo que puede denegarse el acceso aunque se pertenezca al grupo |
| Nivel de integridad | Una jerarquía como Low, Medium o High. Desde un nivel de integridad bajo no se puede escribir en objetos de un nivel más alto. Se trata en el capítulo 17 |
| Estado de elevación | En un entorno con UAC activado, indica si ese token está elevado o no. Se trata en el capítulo 17 |
| Nivel de suplantación | Un valor que solo tienen los tokens de suplantación. Determina si solo se puede identificar, si realmente se puede acceder, o si se puede delegar de forma remota. Se trata en el capítulo 7 |
| Tipo de token | La distinción entre token primario y token de suplantación. Se trata en el capítulo 6 |
Aquí conviene tener presente que lo que importa en la comprobación de acceso no es el nombre, sino el SID, los grupos y el nivel de integridad; esto facilita entender, en capítulos posteriores, por qué “el nombre es correcto pero aun así aparece Access denied”.
Muchos objetos de Windows —archivos, el registro, servicios, canalizaciones con nombre, procesos, hilos, eventos, mutexes, etc.— tienen un descriptor de seguridad.
Cuando un hilo intenta abrir un objeto protegido, Windows coteja la información del token con la ACL del objeto de destino.
Hilo que realiza la operación
↓
Con qué contexto de seguridad se accede
↓
Se examinan el usuario, los grupos y los privilegios del token
↓
Se coteja con la ACL del objeto de destino
↓
Se decide permitir / denegar
Comprender este “con qué contexto de seguridad se accede” es la puerta de entrada para entender los tokens de suplantación.
3. El proceso tiene un token primario
Cada proceso de Windows suele tener un token de acceso primario.
Por ejemplo, cuando un usuario inicia una aplicación desde el escritorio, ese proceso recibe un token primario que representa el contexto de seguridad de ese usuario.
En el caso de un servicio de Windows, se le asigna el token primario de la cuenta con la que se ejecuta el servicio.
Por ejemplo, algo así:
MyService.exe
Primary Token: DOMAIN\svc-app
Si un hilo dentro de este servicio no está suplantando a nadie en particular, al acceder a archivos o al registro se usa el token primario del proceso.
Es decir, por defecto es así:
Thread A
Impersonation Token: ninguno
↓
La comprobación de acceso usa el Primary Token del Process
En este estado, al abrir C:\Data\foo.txt, se comprueba si DOMAIN\svc-app tiene permiso de acceso.
4. El token de suplantación se asocia al hilo
Cuando comienza la suplantación, al hilo se le asocia un token de suplantación. Este es el punto importante: es más preciso pensar que la suplantación no hace que todo el proceso se convierta en otro usuario, sino que ese hilo en concreto recibe las comprobaciones de acceso bajo otro contexto de seguridad.
MyService.exe
Primary Token: DOMAIN\svc-app
Thread A
Impersonation Token: DOMAIN\alice
Thread B
Impersonation Token: ninguno
En este caso, cuando Thread A abre un archivo, la comprobación de acceso se hace con los privilegios de DOMAIN\alice.
Por otro lado, Thread B no está suplantando a nadie, así que la comprobación de acceso se hace con los privilegios de DOMAIN\svc-app.
Si no se entiende esta diferencia, se produce esta clase de confusión:
// Se cree haber suplantado en Thread A
StartImpersonation(token);
// Pero el procesamiento se envía a otro hilo
Task.Run(() =>
{
File.ReadAllText(path);
});
// Y se revierte enseguida
RevertToSelf();
En este caso, no hay garantía de que el hilo que realmente lee el archivo se ejecute en el estado de suplantación esperado.
La suplantación debe manejarse dejando claro su ámbito, su relación con el hilo y con el procesamiento asíncrono.
5. La “suplantación” no es una escalada de privilegios
La palabra suplantación suena un poco fuerte, pero lo importante en el trabajo real es no confundirla con una “escalada de privilegios”.
Básicamente, lo que permite hacer la suplantación es esto:
Procesar con los privilegios del proceso servidor
↓
Solo una parte del procesamiento recibe la comprobación de acceso con los privilegios del usuario cliente
Por ejemplo, si se quiere usar tal cual la ACL de un servidor de archivos como control de permisos, y la aplicación servidora siempre lee los archivos con la cuenta de servicio, no se puede reflejar la ACL específica de cada usuario.
Por eso, solo una parte del procesamiento de la solicitud se suplanta como el usuario que hizo la llamada, y así se realiza el acceso al archivo.
Solicitud HTTP / RPC / Named Pipe
User: DOMAIN\alice
↓
Aplicación servidora
Process: DOMAIN\svc-app
↓
Solo la parte de acceso a archivos se suplanta como DOMAIN\alice
↓
Se permite / deniega según la ACL del servidor de archivos
Esto resulta útil cuando se quiere aprovechar la ACL existente de Windows en lugar de una lógica de autorización propia de la aplicación.
Sin embargo, aunque la suplantación es útil, si el diseño es incorrecto el límite de privilegios se vuelve difícil de entender.
- Qué procesamiento se ejecuta como quién
- Dónde se inició la suplantación
- Dónde se revierte con seguridad
- Con qué privilegios de usuario se emite cada log
- Si se revierte también en caso de excepción
- Si la suplantación sigue vigente hasta el final del procesamiento asíncrono
Es importante dejar todo esto claro en el código.
6. Distinguir el token primario del token de suplantación
Entre los tokens de Windows, lo que más se confunde es el token primario y el token de suplantación.
A grandes rasgos, se puede pensar así:
| Token | Uso principal | Ejemplo representativo |
|---|---|---|
| Token primario | Representa el contexto de seguridad del proceso | Inicio de procesos, CreateProcessAsUser |
| Token de suplantación | Se usa para que un hilo se ejecute bajo otro contexto de seguridad | ImpersonateLoggedOnUser, SetThreadToken, suplantación del cliente en Named Pipe |
Es especialmente importante tener claro que para iniciar un proceso, en principio se necesita un token primario.
El hecho de disponer de un token de suplantación no significa que se pueda usar directamente para iniciar el proceso de otro usuario.
Normalmente, el flujo es este:
Se suplanta al cliente
↓
Se obtiene el token de suplantación con OpenThreadToken
↓
Se crea un token primario con DuplicateTokenEx
↓
Se pasa a CreateProcessAsUser, etc.
Al contrario, si lo que se quiere es “hacer solo el acceso a archivos de este hilo como otro usuario”, no se trata de iniciar un proceso, sino de usar un token de suplantación.
Si se mezclan estos dos conceptos, aunque los argumentos de la API sean correctos, se acaba perdido en errores como Access denied o The parameter is incorrect.
7. Entender el nivel de suplantación
El token de suplantación tiene un nivel de suplantación.
Hay, típicamente, 4 niveles.
| Nivel de suplantación | Significado aproximado |
|---|---|
| Anonymous | El servidor no puede obtener información de identificación del cliente |
| Identification | El servidor puede identificar al cliente, pero no puede usar ese nivel para acceder a objetos con sus privilegios |
| Impersonation | El servidor puede actuar con los privilegios del cliente en el sistema local |
| Delegation | El servidor puede delegar los privilegios del cliente también hacia sistemas remotos |
En el trabajo real, lo que más suele atascar es la diferencia entre Identification e Impersonation.
Como su nombre indica, Identification es un nivel que sirve para saber quién es la otra parte.
No es suficiente para usos como abrir un archivo con los privilegios de ese usuario.
Por eso ocurre lo siguiente:
WindowsIdentity.GetCurrent().Name muestra el nombre de usuario esperado
↓
Pero el acceso al archivo devuelve Access denied
En este caso, no basta con mirar solo el nombre; también hay que comprobar el nivel de suplantación.
En .NET, WindowsIdentity.ImpersonationLevel da una pista.
using System.Security.Principal;
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
Console.WriteLine(identity.ImpersonationLevel);
El acceso a través de la red requiere aún más atención.
En configuraciones donde “un servidor web suplanta a un usuario y, como ese usuario, accede a otro servidor de archivos o de base de datos”, se puede tropezar con el llamado problema de doble salto (double hop).
En este caso, no siempre basta con suplantar desde el código de la aplicación. Hay que diseñar teniendo en cuenta Kerberos, el SPN, la delegación, la delegación restringida, la cuenta de servicio y el método de autenticación del destino de conexión.
8. La forma básica de la suplantación
La forma conceptual de manejar la suplantación con la API Win32 es esta:
1. Obtener el token que se usará para suplantar
2. Suplantar el hilo actual con ese token
3. Ejecutar solo el procesamiento necesario
4. Volver siempre al contexto de seguridad original
5. Cerrar el identificador del token
En forma de código, siempre debe ir en un try / finally.
if (!ImpersonateLoggedOnUser(tokenHandle))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
// Solo esta parte se ejecuta como el usuario suplantado
DoWorkAsImpersonatedUser();
}
finally
{
if (!RevertToSelf())
{
// Un estado en el que no se puede revertir es peligroso: como mínimo, no se debe continuar el procesamiento
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
En una línea de tiempo, queda así:
sequenceDiagram
participant App as Llamador
participant Th as Hilo de procesamiento
participant Win as Windows
participant FS as Archivo
App->>Th: Solicita el procesamiento
Th->>Win: Asocia el token con ImpersonateLoggedOnUser
Note over Th: A partir de aquí las comprobaciones de acceso<br/>se hacen como el usuario alice
Th->>FS: Abre el archivo
FS-->>Th: La ACL se evalúa con los privilegios de alice
Th->>Win: Quita el token con RevertToSelf
Note over Th: A partir de aquí vuelve a la cuenta<br/>de servicio svc-app
Th-->>App: Devuelve el resultado
La suplantación solo está activa en el tramo que va de ImpersonateLoggedOnUser a RevertToSelf. La E/S realizada fuera de este tramo se evalúa con la cuenta del proceso, no con la del usuario suplantado. La trampa del procesamiento asíncrono que se trata en el capítulo 14 se entiende mejor si se piensa como el problema de que la E/S real se sale de este tramo.
Lo importante no es tanto el inicio de la suplantación como asegurarse de revertirla. Si se olvida revertir, el procesamiento posterior en ese hilo sigue ejecutándose como el usuario suplantado.
En aplicaciones que usan un pool de hilos en particular, lo que se pensaba como un único procesamiento puede terminar afectando a otra solicitud u a otro procesamiento distinto.
Por eso, la suplantación no debe pensarse como “empezar y luego revertir”, sino como algo que hay que confinar en un ámbito pequeño.
9. En .NET, usar WindowsIdentity.RunImpersonated
En .NET, siempre que sea posible conviene usar WindowsIdentity.RunImpersonated, que facilita expresar el ámbito de la suplantación directamente en el código.
Si se dispone de un SafeAccessTokenHandle, se puede escribir así:
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static string ReadFileAsUser(SafeAccessTokenHandle token, string path)
{
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
Lo bueno de esta forma es que el ámbito de la suplantación queda contenido dentro de la expresión lambda.
RunImpersonated(token, () =>
{
// Solo aquí hay suplantación
});
// A partir de aquí, fuera, es el contexto original
Para acceso a archivos, acceso al registro, llamadas a bibliotecas existentes y otros casos en los que solo se quiere suplantar un procesamiento concreto, esta forma es legible y segura.
Para procesamiento asíncrono, se usa RunImpersonatedAsync.
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static Task WriteFileAsUserAsync(
SafeAccessTokenHandle token,
string path,
string text,
CancellationToken cancellationToken)
{
return WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
await File.WriteAllTextAsync(path, text, cancellationToken);
});
}
Lo que hay que evitar es lanzar una tarea fire-and-forget desde dentro del ámbito de suplantación.
// Mal ejemplo
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(() =>
{
File.WriteAllText(path, text);
});
});
Con este código resulta difícil saber cuándo, y bajo qué contexto de ejecución, se ejecuta realmente “el procesamiento que escribe el archivo”.
El procesamiento asíncrono que se quiere ejecutar con suplantación debe esperarse (await) dentro de RunImpersonatedAsync, de modo que se salga del ámbito solo una vez completado el procesamiento.
10. Cuando se obtiene el token con LogonUser
LogonUser es una API representativa para obtener un token a partir de las credenciales de otro usuario, pero hay que manejarla con cuidado.
A LogonUser se le pasan el nombre de usuario, el dominio y la contraseña.
Es decir, la propia aplicación pasa a manejar credenciales.
En el trabajo real, hay que prestar atención a esto:
- No dejar la contraseña en texto plano en el código ni en archivos de configuración
- Si es posible, usar la autenticación del sistema operativo, cuentas de servicio, delegación o la autenticación de Windows ya existente
- Gestionar la información secreta en un almacén de secretos adecuado o en la plataforma operativa
- Cerrar siempre el identificador del token
- No emitir en los logs información secreta más allá del nombre de usuario
- Minimizar el ámbito en el que se suplanta
Más adelante se muestra un ejemplo mínimo, pero evite, por favor, copiarlo tal cual y escribir la contraseña como un literal de cadena en password. Las credenciales escritas en el código fuente permanecen en el historial del repositorio, en los artefactos de compilación y en cualquier resultado de descompilación. Dejarlas en texto plano en un archivo de configuración tiene el mismo problema. Se retoma este punto en 21.6.
Entonces, ¿dónde guardar la contraseña? Se organizan las opciones a continuación.
| Método de almacenamiento | Cuándo usarlo | Puntos a tener en cuenta |
|---|---|---|
| No tener contraseña en absoluto | Primera opción. Resolverlo con autenticación de Windows, cuentas de servicio o delegación | Hay que decidirlo en la fase de diseño. Quitarlo después es difícil |
| Pedirla de forma interactiva en tiempo de ejecución | Herramientas operativas que un administrador ejecuta manualmente | No sirve para ejecución desatendida ni para servicios. La cadena leída debe soltarse justo después de usarla |
| Administrador de credenciales de Windows | Uso repetido con la misma cuenta y el mismo PC | Es un almacén por usuario. Si se usa con una cuenta de servicio, hay que registrarlo con esa cuenta |
| DPAPI | Se quiere proteger el valor de un archivo de configuración dentro de un único PC | Hay que prestar atención al ámbito de ProtectedData. Con CurrentUser, solo puede descifrarlo el usuario que cifró; con LocalMachine, también pueden descifrarlo otros usuarios de ese mismo PC |
| Plataformas de gestión de secretos como Key Vault | Hay varios servidores, CI o integración con la nube | Requiere autenticación en la propia plataforma. El manejo en memoria del secreto obtenido hay que pensarlo aparte |
La forma concreta de usar DPAPI se explica en otro artículo, cómo guardar de forma segura la información sensible de un archivo de configuración.
Sea cual sea el método, algo común a todos es plantearse, antes de elegir el método de almacenamiento, si la aplicación realmente necesita recibir la contraseña.
Se muestra un ejemplo con la configuración mínima.
using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;
internal static class NativeMethods
{
private const int LOGON32_LOGON_INTERACTIVE = 2;
private const int LOGON32_PROVIDER_DEFAULT = 0;
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool LogonUser(
string lpszUsername,
string? lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out SafeAccessTokenHandle phToken);
public static SafeAccessTokenHandle Logon(
string userName,
string? domain,
string password)
{
bool ok = LogonUser(
userName,
domain,
password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
out SafeAccessTokenHandle token);
if (!ok)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return token;
}
}
public static string ReadFileWithExplicitCredential(
string userName,
string? domain,
string password,
string path)
{
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
Este ejemplo solo pretende mostrar la forma de la API.
En el trabajo real, primero hay que plantearse si el diseño en el que la aplicación recibe la contraseña es realmente adecuado.
En muchos casos, es más seguro considerar alternativas como estas.
| Lo que se quiere hacer | Alternativa |
|---|---|
| Que todo el servicio acceda a un recurso concreto | Asignar a una cuenta de servicio dedicada la ACL mínima necesaria |
| Acceder a un archivo con los privilegios del propio usuario | Usar autenticación de Windows y un diseño de delegación |
| Realizar una operación que requiere privilegios de administrador | Establecer en el lado del servicio una API de administración clara y controlarla con autorización en el lado de la aplicación |
| Usar otra cuenta solo para una parte del procesamiento | Confinar el ámbito de suplantación al nivel de método y dejar registro de auditoría |
11. Prestar atención al tipo de inicio de sesión de LogonUser
Las propiedades del token obtenido con LogonUser cambian según el tipo de inicio de sesión.
Si no se entiende esta diferencia y simplemente se copia y pega, el comportamiento no será el esperado.
| Tipo de inicio de sesión | Puntos a tener en cuenta |
|---|---|
| Interactive | Similar a un inicio de sesión interactivo. Fácil de usar para operaciones locales, pero depende del entorno de ejecución y de los privilegios |
| Network | Pensado para inicio de sesión por red. El token devuelto puede no servir directamente para iniciar un proceso |
| NewCredentials | En local se comporta como las credenciales actuales, y se usa a veces para que, al conectar de forma remota, se empleen las credenciales indicadas |
Lo que se quiere transmitir aquí no es memorizar un tipo de inicio de sesión concreto.
Lo importante son estos dos puntos:
- Según el tipo de inicio de sesión, cambia el comportamiento en el acceso local, en el acceso por red y en el inicio de procesos
- Hay que comprobar si el token devuelto es un token primario o un token de suplantación
Por ejemplo, es un error típico obtener un token con LOGON32_LOGON_NETWORK y pasarlo tal cual a CreateProcessAsUser, lo cual falla.
Si el objetivo es iniciar un proceso, se necesita un token primario.
Según convenga, el diseño pasa por crear un token primario con DuplicateTokenEx.
12. No subestimar RevertToSelf
Cuando se inicia la suplantación con la API Win32, RevertToSelf la finaliza. Esta operación de reversión no es una simple tarea de limpieza; es un procesamiento importante para devolver el límite de seguridad a su estado original.
Un mal ejemplo:
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
A primera vista parece no tener problemas, pero si DoWork() lanza una excepción, RevertToSelf() no llega a ejecutarse.
Siempre debe ir en un finally.
if (!ImpersonateLoggedOnUser(token))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
DoWork();
}
finally
{
if (!RevertToSelf())
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
Continuar el procesamiento tal cual cuando RevertToSelf falla también es peligroso. Si se continúa el procesamiento posterior sin haber vuelto realmente a los privilegios originales, ese procesamiento sigue con los privilegios de un usuario no previsto. Como mínimo, esa unidad de procesamiento debe tratarse como un fallo y actuar del lado seguro.
RunImpersonated / RunImpersonatedAsync de .NET son útiles como forma de expresar el ámbito precisamente para evitar este olvido de revertir.
13. Reducir todo lo posible el ámbito de la suplantación
El principio de diseño más importante en la suplantación es suplantar solo el procesamiento necesario.
Un mal ejemplo:
WindowsIdentity.RunImpersonated(token, () =>
{
ValidateRequest();
LoadConfiguration();
WriteDebugLog();
ReadUserFile();
UpdateDatabase();
SendNotification();
});
Suplantar un ámbito tan amplio como este hace difícil saber con qué privilegios se realiza cada operación.
Por ejemplo, el acceso al destino de los logs podría hacerse con los privilegios del usuario suplantado y fallar la escritura del log. La conexión a la base de datos podría intentarse con las credenciales del usuario suplantado en lugar de las de la cuenta de servicio. Incluso el procesamiento de notificaciones o la creación de archivos temporales podrían verse afectados por un contexto de privilegios innecesario.
Un buen ejemplo es aislar solo las operaciones que necesitan suplantación.
ValidateRequest();
LoadConfiguration();
string content = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(userFilePath);
});
UpdateDatabase(content);
WriteAuditLog(userName, userFilePath, success: true);
Con esta forma queda claro que lo único que necesita suplantación es la parte de File.ReadAllText.
La suplantación es útil, pero cuanto más se amplía, más difícil de leer y más propensa a errores se vuelve.
14. En procesamiento asíncrono, comprobar “si sigue suplantado hasta el final”
En las aplicaciones .NET actuales, gran parte del procesamiento —archivos, HTTP, base de datos, colas, almacenamiento, etc.— es asíncrono.
Por eso, la combinación de suplantación con async / await requiere cuidado.
La política básica es solo esta:
El procesamiento asíncrono que necesita suplantación se espera (await) dentro de RunImpersonatedAsync
Se muestra, con el mismo formato que el diagrama del capítulo 8, qué ocurre si no se respeta esto. Pero antes conviene tener claro algo: no siempre es cierto que “enviar el trabajo a otro hilo hace que se pierda la suplantación”. Según con qué mecanismo se suplantó, la forma de romperse es la opuesta.
| Forma de suplantar | El procesamiento enviado a Task.Run |
Qué ocurre |
|---|---|---|
RunImpersonated / RunImpersonatedAsync de .NET |
sigue ejecutándose suplantado | La suplantación se extiende más allá del ámbito que muestra el código. Quien lanzó la tarea no recibe ni su finalización ni sus excepciones |
Llamar directamente a ImpersonateLoggedOnUser de Win32 |
se ejecuta con la cuenta del proceso | La E/S real queda fuera del tramo de suplantación y se produce Access denied |
En el lado de .NET ocurre esto porque RunImpersonated transporta el token de suplantación en un AsyncLocal. El valor de AsyncLocal viaja junto con el ExecutionContext, y Task.Run captura el ExecutionContext vigente en el momento de la llamada y lo restaura en el hilo del pool. El controlador de cambios que se ejecuta en cada restauración vuelve a llamar a ImpersonateLoggedOnUser en ese hilo, así que el hilo del pool queda con la misma suplantación. Así es como está implementado en el runtime (s_currentImpersonatedToken y CurrentImpersonatedTokenChanged de WindowsIdentity).
En cambio, el token de Win32 solo está asociado al hilo y no se copia por sí solo a otro hilo.
Si se sigue la línea de tiempo del lado de .NET, queda así:
sequenceDiagram
participant Th as Hilo que llama
participant Pool as Otro hilo del pool de hilos
participant FS as Archivo
Th->>Th: Inicia la suplantación con RunImpersonated
Th->>Pool: Envía la escritura con Task.Run
Note over Pool: El token de suplantación viaja<br/>junto con el ExecutionContext
Th->>Th: Sale del ámbito y revierte la suplantación
Note over Th: Solo se revierte<br/>la suplantación de este hilo
Pool->>FS: La escritura real se ejecuta después de esto
Note over Pool: Sigue como el usuario suplantado.<br/>Quien lo lanzó no sabe cuándo termina
FS-->>Pool: Ni el resultado ni la excepción los recibe nadie
En el diagrama del capítulo 8, la E/S quedaba dentro del tramo de suplantación. En este diagrama, en el momento en que se envía a Task.Run el procesamiento pasa a otro hilo, y la escritura real queda fuera del tramo que muestra el código. El peligro no está en que se pierda la suplantación, sino en que no se pierda y, además, deje de poder rastrearse: ese es el problema de esta forma. En concreto, ocurren tres cosas a la vez:
- Deja de poder leerse el alcance de la suplantación. El ámbito escrito como “desde aquí hasta aquí” y el ámbito que realmente se ejecuta suplantado no coinciden
- Los fallos quedan silenciados. Como nadie hace
awaitde la tarea lanzada, aunque se produzcaAccess deniedla excepción no llega a ningún sitio - No queda claro cuándo se puede cerrar el token. Si se cierra
SafeAccessTokenHandleconusing, se acaba cerrando mientras el procesamiento todavía sigue en marcha
Si se suplanta directamente con Win32, ocurre lo contrario: el procesamiento lanzado se ejecuta con la cuenta del proceso. Esto tiende a manifestarse como que en el entorno de pruebas la cuenta de servicio también tiene privilegios y todo funciona, y solo en producción aparece por primera vez el Access denied.
En cualquiera de los dos casos, esperar (await) hasta la finalización dentro de RunImpersonatedAsync evita que ocurra.
Un buen ejemplo:
await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
await using FileStream stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string text = await reader.ReadToEndAsync();
await ProcessTextAsync(text);
});
Sin embargo, incluso con esta forma hay algo que considerar.
Si conviene que ProcessTextAsync también se ejecute como el usuario suplantado.
Si basta con suplantar solo la parte que lee el archivo, es más seguro separarlo así:
string text = await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
return await File.ReadAllTextAsync(path);
});
await ProcessTextAsync(text);
El hecho de poder hacer await dentro del ámbito de suplantación no significa que se pueda meter cualquier cosa ahí dentro.
También en el procesamiento asíncrono, el ámbito de la suplantación debe minimizarse.
15. ASP.NET Core y la suplantación
Al usar autenticación de Windows en ASP.NET Core, también hay que prestar atención al manejo de la suplantación.
Es peligroso dar por hecho que “como se ha iniciado sesión con autenticación de Windows, todo el procesamiento de la solicitud se ejecuta como ese usuario”.
En general, el propio proceso de la aplicación se ejecuta con la identidad del grupo de aplicaciones o con la cuenta de ejecución del servicio. La identidad de Windows del usuario se obtiene como información de autenticación, pero eso no significa que todo el procesamiento pase automáticamente a tener los privilegios del usuario.
Si se necesita realizar una acción concreta con los privilegios del usuario, hay que crear explícitamente el ámbito con RunImpersonated / RunImpersonatedAsync.
La idea del código es esta:
app.MapGet("/download", async (HttpContext context) =>
{
if (context.User.Identity is not WindowsIdentity user)
{
return Results.Unauthorized();
}
string path = GetPathFromRequest(context);
byte[] bytes = await WindowsIdentity.RunImpersonatedAsync(
user.AccessToken,
async () => await File.ReadAllBytesAsync(path));
return Results.File(bytes, "application/octet-stream");
});
También en este ejemplo, lo único que se suplanta es la parte de lectura del archivo.
Hay que pensar con cuidado si es necesario meter en el ámbito de suplantación también la generación de la respuesta, el registro de logs y la lógica de autorización propia de la aplicación.
16. Cómo interpretar Access denied
En implementaciones que usan suplantación, el error que aparece con frecuencia es Access denied.
Si al ver este error solo se piensa que “ha fallado la suplantación”, se toma un camino más largo de lo necesario.
Se separan los puntos de vista a comprobar.
| Punto de vista | Qué comprobar |
|---|---|
| Si realmente se está suplantando | Comprobar WindowsIdentity.GetCurrent().Name dentro del ámbito de suplantación |
| Si el nivel de suplantación es suficiente | Si es Identification en lugar del nivel necesario |
| Si la ACL del recurso de destino es correcta | Si el usuario suplantado tiene permiso de lectura / escritura |
| Si es local o remoto | Si funciona con archivos locales pero falla solo con UNC |
| Si es un problema de doble salto | Si se intenta ir del servidor web al servidor de archivos con los privilegios del usuario |
| Si se ha salido del ámbito de suplantación | Si la E/S real se ejecuta fuera del ámbito o en otra tarea |
| Si el tipo de token es el correcto | Si se está pasando un token de suplantación para iniciar un proceso |
| Si hay influencia de UAC / nivel de integridad | Si, aunque se pertenezca a Administrators, es un token no elevado |
Es especialmente importante no quedarse tranquilo con solo comprobar el nombre.
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
Este log es útil, pero no es suficiente.
Como mínimo, conviene revisar también esto:
Console.WriteLine(identity.ImpersonationLevel);
Console.WriteLine(identity.IsAuthenticated);
Además, si el destino es un recurso compartido de red, hay que revisar no solo el código de la aplicación, sino también el método de autenticación, la configuración de delegación, el SPN, la cuenta de servicio y la ACL del lado del servidor de archivos.
17. UAC y el problema de “falla aunque sea administrador”
En Windows, que un usuario pertenezca al grupo Administrators y que el token actual esté elevado no es lo mismo.
En un entorno con UAC activado, incluso un usuario administrador suele ejecutar procesos con un token restringido, y las operaciones que requieren privilegios de administrador necesitan una elevación.
Por eso ocurre esto:
El usuario pertenece a Administrators
↓
Pero el token actual no está elevado
↓
Access denied al escribir en Program Files o en HKLM
Con la suplantación pasa lo mismo.
En lugar de pensar “el usuario suplantado es administrador, así que debería poder escribir”, hay que comprobar en qué estado se encuentra realmente el token que se está pasando.
En la depuración, se revisan estos puntos:
- Grupos a los que pertenece
- Si los privilegios (Privilege) están habilitados o deshabilitados
- Nivel de integridad
- Estado de elevación
- Si es un token restringido
- Si existe un token de elevación vinculado
Con la API Win32 se puede usar GetTokenInformation para comprobar TokenType, TokenImpersonationLevel, TokenElevationType, TokenIntegrityLevel, etc.
Sin embargo, como diseño práctico, es más seguro confinar las operaciones mínimas necesarias en un servicio o una API de administración dedicados, en lugar de “suplantar a un usuario administrador para hacer cualquier cosa”.
18. Recursos compartidos de red y el doble salto
Una consulta muy habitual sobre suplantación es el acceso a recursos compartidos de red.
PC del cliente
↓ Autenticación de Windows
Servidor web / servidor de API
↓ Se quiere acceder suplantando
Servidor de archivos
En esta configuración, puede pasar que “en el servidor web se obtiene el nombre de usuario, pero al llegar al servidor de archivos falla”.
Esto es un problema de si se puede redelegar las credenciales del usuario a otro servidor.
La suplantación en el propio servidor local y la delegación hacia otro servidor no son lo mismo.
El nivel Impersonation puede servir para operaciones locales, pero puede no ser suficiente para actuar como cliente frente a un servidor remoto.
Si se quiere usar los privilegios del propio usuario a través de la red, hace falta diseñar la delegación Kerberos, la delegación restringida, el SPN, la cuenta de servicio y el método de autenticación.
Aquí es fácil llegar a un punto muerto, así que se muestran a continuación las opciones para el siguiente paso.
| Dirección | Qué hacer | Casos en que conviene | Puntos a tener en cuenta |
|---|---|---|---|
| Delegación restringida de Kerberos | Configurar en la cuenta del servidor intermedio “se permite delegar solo a este servicio” | El servidor intermedio y el servidor de archivos están en el mismo dominio y se cuenta con la colaboración del administrador de dominio | La configuración se hace en el dominio, no en la aplicación. Hay que enumerar explícitamente el SPN de destino de la delegación |
| Delegación restringida basada en recursos | Poner la configuración que permite la delegación en la cuenta del lado al que se delega, es decir, el servidor de archivos | Cuando se cruzan dominios o cuando el administrador del recurso quiere llevar la iniciativa | Es un mecanismo de Windows Server 2012 en adelante. El lugar donde se configura es el inverso de la delegación restringida tradicional |
| Pasar credenciales de forma explícita | Conectarse con las credenciales de una cuenta dedicada de uso limitado, en lugar de las del propio usuario | Cuando no se puede configurar la delegación, o cuando “ser el propio usuario” no es un requisito de negocio | Se necesita almacenar las credenciales. Consulte la tabla del capítulo 10 |
| Diseñar sin retransmisión | Concentrar el acceso al servidor de archivos en la cuenta de servicio y realizar la autorización en el lado de la aplicación | Cuando las reglas de negocio están del lado de la aplicación | La ACL deja de ser la decisión final. El diseño del registro de auditoría se vuelve importante |
| Dejar que el cliente acceda directamente | Sin pasar por el servidor, dejar que el propio PC cliente acceda directamente al servidor de archivos | Cuando basta con abrir la carpeta compartida desde la pantalla | Se pierde el control centralizado y la recopilación de logs desde el lado del servidor |
En cuanto al orden de las decisiones, primero hay que determinar si realmente hace falta llegar al servidor de archivos con los privilegios propios de Windows del usuario. Si hace falta, se entra en el diseño de la delegación; si no hace falta, se opta por un diseño sin retransmisión. Como la configuración de la delegación no la puede completar solo el desarrollador de la aplicación, es realista consultar cuanto antes con el administrador de dominio en el momento en que se determina que es necesaria.
Por otro lado, según los requisitos de negocio, también hay casos en los que no hace falta llegar al servidor de archivos con los privilegios propios de Windows del usuario.
En ese caso, un diseño como este es más simple.
La autenticación y autorización del usuario las hace la aplicación
↓
El acceso al servidor de archivos se hace con una cuenta de servicio dedicada
↓
En el log de operaciones se registran el ID del usuario y el archivo de destino
Esto es un diseño en el que la decisión final no es “la ACL del sistema operativo”, sino “la autorización de la aplicación”.
Cuál de los dos es correcto depende de los requisitos de negocio.
Sin embargo, si no queda claro cuál de los dos métodos se ha elegido, la suplantación, la delegación, la ACL y la autorización de la aplicación se mezclan y se vuelven difíciles de entender.
19. El ciclo de vida del identificador del token
Un token es un identificador (handle) hacia un objeto del kernel, así que hay que cerrarlo en cuanto deja de ser necesario tras obtenerlo.
En .NET, lo básico es usar SafeAccessTokenHandle y gestionar el ámbito con using.
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
string result = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
Un mal ejemplo:
// Mal ejemplo: mantener el token de forma global
private static SafeAccessTokenHandle? _cachedToken;
Mantener un token durante mucho tiempo lleva a problemas como estos:
- Fuga de identificadores (handle leak)
- Dejar de saber qué procesamiento usa qué token
- Perder claridad sobre la coherencia con la desactivación de la cuenta o el cambio de privilegios
- Tender a un diseño que mantiene las credenciales de autenticación durante mucho tiempo
- Ser difícil de justificar en una auditoría
El principio es este:
Obtenerlo cuando se necesita
↓
Usarlo en el ámbito mínimo
↓
Cerrarlo siempre
Por supuesto, según el coste de autenticación o los requisitos operativos, en algunos casos se puede plantear el uso de una caché. Pero incluso en ese caso, hay que incluir en el diseño el tiempo de vida, la eliminación, el cambio de cuenta, el registro de auditoría y el tratamiento en un cambio de privilegios.
20. Qué dejar en el registro de auditoría
En el procesamiento que usa suplantación, el diseño del log también es importante.
Como mínimo, dejar disponible la información de esta tabla facilita la investigación posterior.
| Elemento | Ejemplo |
|---|---|
| Usuario que hizo la solicitud | DOMAIN\alice |
| Cuenta del proceso en ejecución | DOMAIN\svc-app |
| Cuenta suplantada | DOMAIN\alice o una cuenta dedicada |
| Recurso de destino | Ruta de archivo, nombre del recurso compartido, clave del registro, etc. |
| Operación | Read, Write, Delete, CreateProcess, etc. |
| Resultado | Success, AccessDenied, Timeout, UnexpectedError |
| Código de error | Código de error Win32, HRESULT, tipo de excepción |
| Ámbito de suplantación | En qué método y en qué unidad de operación se suplantó |
Sin embargo, también hay cosas que no deben aparecer en el log.
- Contraseñas
- El valor del token de acceso
- Cabeceras de autenticación
- Tickets Kerberos o las propias credenciales
- Contenido de archivos que incluya información personal
El objetivo del log es poder rastrear después “a petición de quién, como qué cuenta, qué se intentó y cómo terminó, con éxito o con fallo”.
No hace falta registrar las propias credenciales.
21. Antipatrones habituales
Se organizan aquí las implementaciones peligrosas que se ven con frecuencia en torno a la suplantación.
21.1 Suplantar toda la aplicación
WindowsIdentity.RunImpersonated(token, () =>
{
RunEntireApplication();
});
Suplantar toda la aplicación hace imposible saber con qué privilegios se ejecuta cada operación.
La suplantación debe limitarse a la E/S necesaria o a llamadas concretas a la API.
21.2 Revertir sin finally
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
Es peligroso porque, si hay una excepción, no se revierte.
Siempre hay que usar try / finally, o bien RunImpersonated.
21.3 Hacer fire-and-forget durante la suplantación
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(DoWorkAsync);
});
Se sale del ámbito de suplantación antes de que el procesamiento termine. Con esta forma de escribirlo, la tarea lanzada (como se vio en el capítulo 14) sigue ejecutándose suplantada, así que el tramo de suplantación que muestra el código y el tramo real de suplantación quedan desalineados. Además, como nadie hace await, si falla no aparece ninguna excepción. Si se suplanta directamente con Win32, ocurre lo contrario: se ejecuta con la cuenta del proceso.
Si se necesita suplantación, hay que hacer await dentro de RunImpersonatedAsync.
21.4 Dar por exitoso solo con el nombre de usuario
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
Aunque el nombre de usuario sea el esperado, puede faltar el nivel de suplantación o los privilegios necesarios.
También hay que revisar ImpersonationLevel, la ACL de destino, el tipo de inicio de sesión y la delegación de red.
21.5 Suplantar una cuenta de administrador como “cuenta cómoda”
Un diseño como “no quiero que falle este procesamiento, así que lo suplanto con una cuenta de administrador” es peligroso.
Es más seguro preparar una cuenta dedicada con los privilegios mínimos necesarios y limitar el alcance de la operación.
21.6 Poner la contraseña en un archivo de configuración
{
"UserName": "DOMAIN\\admin",
"Password": "P@ssw0rd!"
}
Esto debe evitarse.
Si se manejan credenciales, hay que usar un mecanismo adecuado al entorno: un almacén de secretos, el Administrador de credenciales de Windows, DPAPI, un Key Vault en la nube, o la gestión de secretos de la plataforma operativa.
21.7 Tratar como el mismo tema el inicio de procesos y el acceso a archivos
Para el simple acceso a archivos, a veces basta con un token de suplantación.
Por otro lado, si se quiere iniciar un proceso como otro usuario, aparecen otros aspectos: el token primario, el perfil, el escritorio, las variables de entorno, la sesión, los privilegios, etc.
Si se usa CreateProcessAsUser o CreateProcessWithTokenW, hay que tratarlo como un diseño distinto al de la suplantación.
22. Puntos de vista para las pruebas
El procesamiento con suplantación, si solo se comprueba en un entorno de administrador local, deja pasar casos por alto.
Como mínimo, hay que preparar estos casos de prueba.
| Caso | Qué comprobar |
|---|---|
| Usuario con permisos | Que se puede leer / escribir el archivo de destino |
| Usuario sin permisos | Que falla correctamente como Access denied |
| Usuario inexistente | Que se trata como un fallo de autenticación |
| Contraseña incorrecta | Que falla sin exponer información secreta en el log |
| Recurso compartido de red | Comprobar la diferencia de comportamiento entre local y UNC |
| Procesamiento asíncrono | Que, incluso después del await, funciona dentro del ámbito esperado |
| Excepción producida | Que la suplantación siempre se revierte |
| Solicitudes concurrentes | Que la suplantación de un usuario no se mezcla con la de otro |
| Ejecución como servicio | Que funciona con la cuenta de servicio real, no solo en el entorno de inicio de sesión interactivo del desarrollador |
En particular, estas dos no se pueden dejar fuera.
Que el éxito funcione
Que el fallo, cuando debe fallar, falle
En el procesamiento con suplantación, no basta con probar los casos de éxito; también hay que probar que un usuario sin permisos sea rechazado con seguridad.
Si una operación que debería rechazarse termina teniendo éxito, es posible que el diseño de la suplantación o de la autorización esté equivocado.
23. Lista de verificación de implementación
Antes y después de implementar, se revisa esta lista de verificación.
| Punto de vista | Qué comprobar |
|---|---|
| Objetivo | Se puede explicar por qué hace falta la suplantación |
| Alternativas | Se ha considerado si no basta con una cuenta de servicio o con la autorización de la aplicación |
| Alcance | El ámbito de suplantación es mínimo |
| Reversión | Se revierte con seguridad incluso en caso de excepción |
| Asíncrono | Se hace await hasta la finalización dentro de RunImpersonatedAsync |
| Token | No se confunde el token primario con el token de suplantación |
| Nivel de suplantación | Se distingue entre Identification e Impersonation / Delegation |
| Red | Se ha comprobado si hace falta UNC, doble salto o delegación Kerberos |
| UAC | No se confunde pertenecer a Administrators con estar elevado |
| Información secreta | No se guarda la contraseña en texto plano |
| Identificador | SafeAccessTokenHandle se cierra con using |
| Log | Se puede rastrear el usuario, la cuenta suplantada, el destino y el resultado |
| Pruebas | Se han comprobado los casos con permisos / sin permisos / excepción / concurrencia |
Si hay muchos elementos que fallan en esta lista, conviene revisar el diseño antes de escribir el código.
24. Identificar dónde conviene usarla
El token de suplantación es potente, pero no siempre es la primera opción que hay que elegir.
Como diseño, conviene pensarlo separando los casos de uso, así se pierde menos.
24.1 Cuando se quiere usar tal cual la ACL del sistema operativo
Si la ACL del servidor de archivos o de la carpeta compartida es el centro de la regla de negocio, y la aplicación también debe seguir esa decisión, tiene sentido suplantar como el propio usuario.
Acceder como el propio usuario
↓
La ACL de Windows es la decisión final
En este caso, hay que diseñar teniendo en cuenta la autenticación de Windows, el nivel de suplantación, la delegación y la configuración de red.
24.2 Cuando se quiere autorizar del lado de la aplicación
Si las reglas de negocio están del lado de la aplicación y el servidor de archivos o la base de datos están bajo el control de la aplicación, a veces es más claro acceder con una cuenta de servicio y autorizar del lado de la aplicación.
Se autentica al usuario
↓
Se autoriza en la aplicación
↓
Se accede al recurso con la cuenta de servicio
↓
Se deja el ID del usuario en el log de auditoría
En este esquema, en lugar de usar suplantación, cobra importancia la lógica de autorización de la aplicación y el registro de auditoría.
24.3 Cuando se quiere realizar operaciones administrativas
En lugar de ejecutar las operaciones administrativas directamente con el token del usuario, suele ser más seguro preparar un servicio o una API dedicados a las operaciones administrativas, donde se realicen la autorización, la validación de entrada, la auditoría y la posibilidad de revertir.
Cliente
↓
Solicita a la API de administración
↓
La API de administración autoriza
↓
Opera con los privilegios mínimos necesarios
↓
Registro de auditoría
“Suplantar al administrador por ahora” parece cómodo a corto plazo.
Pero a largo plazo, se vuelve doloroso en la auditoría, la investigación de incidentes, el cambio de privilegios y las revisiones de seguridad.
25. Resumen
El token de suplantación de Windows es un mecanismo importante para usar correctamente la gestión de privilegios de Windows.
Sin embargo, también es un área en la que, si se usa sin entenderla, es fácil acabar con código que parece funcionar pero es peligroso.
Se enumeran los puntos que hay que tener claros.
- El token de acceso representa el contexto de seguridad: usuario, grupos, privilegios, etc.
- El proceso tiene un token primario
- El token de suplantación se asocia principalmente al hilo y se usa en las comprobaciones de acceso
- La suplantación no es una escalada de privilegios
- El token primario y el token de suplantación tienen usos distintos
- Según el nivel de suplantación cambia si solo se puede identificar, si se puede acceder realmente, o si se puede delegar de forma remota
- Al suplantar con la API Win32, siempre hay que llamar a
RevertToSelfdentro de untry/finally - En .NET, hay que expresar el ámbito de forma reducida con
WindowsIdentity.RunImpersonated/RunImpersonatedAsync - Al usar
LogonUser, hay que prestar atención a la gestión de credenciales y al tipo de inicio de sesión - En los recursos compartidos de red, no solo importa la suplantación; también entran en juego la delegación Kerberos y el diseño de la cuenta de servicio
- El identificador del token se gestiona con
SafeAccessTokenHandleyusing - Hay que probar no solo los casos de éxito, sino también los casos que deben ser rechazados
Lo importante en una implementación con tokens de suplantación no es el único punto de “se ejecutó como otro usuario”, sino dejar la implementación en un estado en el que se pueda explicar todo esto.
Qué procesamiento
A petición de quién
Como qué cuenta
Con qué alcance se ejecutó
Dónde se revirtió
Cómo se registran el éxito y el fallo
Si se tiene esto ordenado, la suplantación deja de ser un mecanismo temible.
Se convierte en una herramienta práctica para aprovechar la ACL de Windows, las cuentas de servicio, los servidores de archivos existentes y los activos del dominio corporativo.
Referencias
- El conjunto completo de código de ejemplo de este artículo (biblioteca, demo, pruebas unitarias)
https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/windows-impersonation-token - Microsoft Learn: Access Tokens
https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens - Microsoft Learn: Impersonation Tokens
https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-tokens - Microsoft Learn: Impersonation Levels
https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-levels - Microsoft Learn:
SECURITY_IMPERSONATION_LEVELenumeration
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level - Microsoft Learn:
ImpersonateLoggedOnUserfunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-impersonateloggedonuser - Microsoft Learn:
RevertToSelffunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-reverttoself - Microsoft Learn:
LogonUserfunction
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera - Microsoft Learn:
DuplicateTokenExfunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-duplicatetokenex - Microsoft Learn:
TOKEN_INFORMATION_CLASSenumeration
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-token_information_class - Microsoft Learn:
WindowsIdentity.RunImpersonated
https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonated - Microsoft Learn:
WindowsIdentity.RunImpersonatedAsync
https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonatedasync - Implementación del runtime de .NET:
WindowsIdentity.cs(elAsyncLocalque mantiene el token de suplantación yCurrentImpersonatedTokenChanged, que vuelve a establecer la suplantación al cambiar de hilo)
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs - Microsoft Learn: Configure Windows Authentication in ASP.NET Core
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth - Microsoft Learn: Kerberos Constrained Delegation Overview
https://learn.microsoft.com/en-us/windows-server/security/kerberos/kerberos-constrained-delegation-overview - Microsoft Learn:
ProtectedDataClass
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Las profundidades de la E/S de Windows (parte 4) — El administrador de caché: ¿cuándo llega su WriteFile al disco?
Cuarta entrega de la serie que explica con diagramas el administrador de caché de Windows: la caché implementada como asignación de archi...
Los entresijos de E/S de Windows (parte 3) — Puertos de finalización de E/S (IOCP) y el grupo de subprocesos de .NET: el sótano de async/await
Parte 3 de una serie que explica con diagramas el puerto de finalización de E/S (IOCP): la cola de finalización integrada con el control ...
Las profundidades de la E/S de Windows (Parte 2) ── E/S síncrona y E/S asíncrona: el verdadero significado de OVERLAPPED
Segunda entrega de la serie que explica con diagramas la E/S síncrona y la E/S asíncrona (E/S superpuesta) de Windows: el significado de ...
Las profundidades de la E/S de Windows (parte 1) — Toda lectura y escritura se convierte en un IRP: panorama general del sistema de E/S
Primera entrega de la serie que explica desde la base el sistema de E/S de Windows: el espacio de nombres del Administrador de objetos, l...
¿Qué es un PDB (Program Database)? — Cómo entender la información de depuración, los símbolos y Source Link
Qué es un PDB: qué contiene, qué no, y su relación con Debug/Release, Portable PDB, Source Link, servidores de símbolos y el análisis de ...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué es un token de suplantación en Windows?
- Un token de suplantación es el token que se asocia a un hilo para que ese hilo reciba las comprobaciones de acceso bajo un contexto de seguridad distinto. No es que todo el proceso pase a ser otro usuario: solo el hilo que está suplantando recibe las comprobaciones de acceso a archivos, al registro, etc., con los privilegios de ese usuario. No se trata de una "magia para convertirse en administrador", es decir, no es una escalada de privilegios, sino un mecanismo para que solo una parte del procesamiento del proceso servidor reciba las comprobaciones de acceso con los privilegios del usuario cliente.
- ¿En qué se diferencian el token primario y el token de suplantación?
- El token primario representa el contexto de seguridad del proceso y se usa para iniciar procesos, por ejemplo con CreateProcessAsUser. El token de suplantación se usa para que un hilo se ejecute bajo otro contexto de seguridad y aparece en escenarios como ImpersonateLoggedOnUser o la suplantación del cliente en Named Pipe. Si lo que se quiere es iniciar un proceso, en principio se necesita un token primario; si solo se dispone de un token de suplantación, el flujo habitual es crear un token primario con DuplicateTokenEx. Confundir ambos tipos lleva a errores como Access denied que resultan difíciles de diagnosticar.
- ¿Por qué aparece Access denied aunque se esté suplantando?
- Hay varios puntos que conviene revisar. Dentro del alcance de suplantación, hay que comprobar no solo el Name de WindowsIdentity.GetCurrent(), sino también el ImpersonationLevel, para verificar que sea Impersonation o superior y no solo Identification. También hay que revisar la ACL del recurso objetivo, si la E/S real se está ejecutando fuera del alcance de suplantación o en otra tarea, si se trata del problema de doble salto (donde el acceso local funciona pero el acceso por UNC falla), y el efecto de un token no elevado por UAC. El nombre puede ser el esperado y, aun así, faltar privilegios.
- ¿Cómo se implementa la suplantación de forma segura en .NET?
- Lo básico es usar WindowsIdentity.RunImpersonated / RunImpersonatedAsync y confinar el alcance de la suplantación dentro de una expresión lambda. El procesamiento asíncrono que necesite suplantación debe esperarse (await) dentro de RunImpersonatedAsync, evitando lanzar tareas fire-and-forget desde dentro del alcance de suplantación. Cuando se suplanta con la API Win32, siempre hay que llamar a RevertToSelf dentro de un try/finally. El alcance de la suplantación debe reducirse al mínimo necesario de E/S, y el identificador del token debe gestionarse con SafeAccessTokenHandle y using.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.