One shell to HANDLE them all
Tabla de contenidos
Introducción
En un ejercicio de Red Team, la explotación de vulnerabilidades en aplicaciones web puede ser una oportunidad a la hora de obtener acceso inicial a la infraestructura o comprometer un activo interno como movimiento lateral. A partir de ahí, un procedimiento habitual consiste en subir una webshell para después llevar a cabo una escalada de privilegios que permita obtener control total sobre el activo. Cuando nos encontramos en un entorno Windows, es común abusar de los privilegios SeImpersonate y/o SeAssignPrimaryToken a través de los diversos exploits de la “familia Potato” para este propósito.
Sin embargo, en entornos con un grado de monitorización elevado puede que no sea lo más indicado, ya que se trata de técnicas muy conocidas. En estos casos, así como en general, resulta interesante tener alternativas a la hora de modificar el contexto de seguridad inicial para llevar a cabo la escalada de privilegios o ejecutar acciones en nombre de otros usuarios.
En este artículo, vamos a proponer un enfoque diferente a la hora de ejecutar el cambio de contexto de seguridad a través de una webshell: el abuso de handles filtrados; en concreto, handles de token de usuario. La reutilización de handles no es una técnica nueva y ha sido explorada en diferentes escenarios y con diferentes propósitos (e.g. volcar la memoria del proceso LSASS sin abrir un nuevo handle al proceso). No obstante, esta técnica aplicada a la post-explotación de una aplicación web cuenta con diversas ventajas:
- Por defecto, la cuenta de usuario utilizada para ejecutar el proceso del servidor cuenta con privilegios de suplantación, lo que permite aprovechar de manera directa los handles de tipo token para modificar el contexto de seguridad.
- Dado que los servidores web tienen como objetivo ejecutar aplicaciones de una enorme variedad, origen y complejidad, son más propensos a sufrir fallos en el código web alojado que generen un filtrado indeseado de handles. En otros casos, este filtrado de handles no se produce debido a errores de programación sino por motivos de eficiencia que requieran reutilizar una misma sesión para acceder a diversos recursos locales.
- Los servidores IIS (sobre todo en un entorno de Directorio Activo) cuentan con diversas características que los convierte en objetivos ideales para esta técnica: autenticación Windows integrada, suplantación de usuarios, delegación, directorios virtuales en carpetas remotas, etc. Como veremos en este artículo, todas estas características legítimas pueden desembocar en un filtrado de handles sin que existan errores de programación como los mencionados anteriormente.
A lo largo del artículo comentaremos diversas situaciones en las que se producen filtrados de handles, tanto por requisitos del software como por el funcionamiento interno del propio servidor web o derivados de fallos de programación. Estos fallos de programación no son excepcionales y podemos encontrarlos tanto en ejemplos de código de foros como StackOverflow (seguro que nadie copiaría código de Internet sin previamente investigar en profundidad sus implicaciones de seguridad, ¿verdad?) como en la propia documentación de Microsoft.
Contexto
Para ejemplificar las diversas situaciones en las que se puede dar el filtrado de handles de tipo token vamos a ver diferentes fragmentos de código escritos en ASP.NET en un servidor IIS, combinación habitual en entornos Windows.
Antes de entrar en materia, es importante tener nociones básicas sobre el funcionamiento interno del servidor IIS, así como de las particularidades del framework ASP.NET que favorecen la aparición del filtrado de handles.
Un mismo servidor IIS puede alojar diversas aplicaciones web, y cada una de estas aplicaciones web es ejecutada por lo que se conoce como un Application Pool. Un AP es una forma de aislar las diferentes aplicaciones web alojadas en un mismo servidor, dado que cada Application Pool es ejecutado en un “worker process” (w3wp.exe) diferente. De esta manera, podemos tener dos aplicaciones web ejecutadas por un mismo AP, lo cual significará que el código web de ambas se ejecutarán en el espacio de memoria del mismo proceso, o podemos aislar las aplicaciones utilizando Application Pool diferentes.
Esto es importante de cara al abuso de handles filtrados debido al aislamiento que proporcionan los AP. Si subimos una shell a una aplicación web ejecutada en el Application Pool A, únicamente seremos capaces de acceder a los handles que hayan sido filtrados desde esta aplicación o desde cualquier otra que esté siendo ejecutada en el mismo AP A. Si, por ejemplo, existe un filtrado de handles en otra aplicación web alojada en el mismo servidor pero ejecutada en el Application Pool B, desde nuestra shell ejecutada en el AP A no seremos capaces de acceder directamente a estos handles dado que ambos AP estarán siendo ejecutados en procesos distintos.
Además, cada Application Pool ejecuta su proceso en el contexto de seguridad de su propio Application Pool Identity, de tal manera que cada uno utiliza una cuenta de usuario distinta, aumentando el nivel de aislamiento entre los procesos. Previamente a la existencia de esta característica, era habitual que varios o todos los servicios de un equipo se ejecutasen en el contexto de seguridad de la misma cuenta de usuario (NetworkService, LocalService), por lo que el compromiso de un servicio permitía el acceso a los procesos de los demás y no se podía dar este aislamiento entre diferentes aplicaciones web.
En cuanto a ASP.NET, al ser un framework para crear aplicaciones web utilizando .NET, el principal motivo que genera situaciones de filtrado de handles es su carácter administrado. Debido a su naturaleza, es común que los desarrolladores de .NET deleguen en características como el Colector de Basura la gestión y liberación automatizada de los recursos utilizados por sus programas. Sin embargo, el problema aparece cuando se hace uso de recursos no administrados (como, por ejemplo, los handles del sistema operativo), los cuales no pueden ser liberados por el CB de manera automática y requieren de una gestión manual poco habitual en este tipo de lenguajes, dando lugar a fallos de seguridad como los que veremos en este artículo.
Para solventar el problema de la gestión de recursos no administrados a través de .NET Microsoft propone el uso de la interfaz IDisposable
. Cualquier clase que implemente esta interfaz deberá contar con un método Dispose
que se encargará de liberar los recursos no administrados utilizados por el objeto en cuestión. Este método Dispose
deberá ser llamado siempre al terminar de utilizar dicho objeto, bien en un bloque try/finally
o utilizando la instrucción using
, tal y como se indica en la documentación oficial.
Clasificación
A continuación, vamos a explorar algunos de los distintos escenarios en los que se pueden filtrar handles de token de usuario en una aplicación web en ASP.NET. Para ello, vamos a realizar una clasificación sencilla atendiendo a las características del token que se esté filtrando y el nivel de compromiso que su uso nos proporcione como atacantes:
- Cambio de contexto de seguridad: Se filtra un token que permite escapar del contexto de seguridad en el que se ejecuta el proceso del IIS, pero este token apunta a una sesión sin credenciales en memoria y, por lo tanto, no permite el acceso a recursos de red. Por lo general, hemos observado que este token estará elevado en caso de que el usuario pertenezca al grupo local de Administradores. De esta manera, el uso de este tipo de token permite escapar del contexto de seguridad del Application Pool Identity y llevar a cabo una escalada local de privilegios.
- Acceso a recursos de red: Se filtra un token de usuario que tiene credenciales cacheadas en memoria y que, por lo tanto, puede ser utilizado para acceder a recursos de red y llevar a cabo movimientos laterales.
Por supuesto, existen muchas situaciones en las que se pueden llegar a filtrar handles de token de usuario que no veremos en este artículo. Un ejemplo es el uso de Autenticación Básica en un servidor IIS, lo cual por defecto filtrará tokens con acceso a red de todos aquellos usuarios que inicien sesión en la aplicación web.
El objetivo en este caso es demostrar, tanto de manera teórica como práctica, las distintas vertientes de este comportamiento y cómo podemos abusar de él durante un ejercicio.
Cambio de contexto de seguridad
WindowsIdentity
Cuando se tiene habilitada la autenticación Windows para acceder a una aplicación web, puede ser necesario en muchas ocasiones obtener desde el código de servidor la identidad del usuario que ha iniciado sesión. Para ello, la manera más común de hacerlo en ASP.NET es a través de la clase HttpContext
.
Esta clase nos permite acceder al contexto de la petición HTTP y obtener el WindowsIdentity
asociado. En este caso, el WindowsIdentity
devuelto representa al usuario de Windows que ha iniciado sesión en la aplicación y, como podemos comprobar en la documentación oficial, esta clase implementa la interfaz IDisposable
, lo que significa que al utilizarla debemos llamar al método Dispose
para liberar los recursos no administrados que maneja.
A pesar de añadir al final de la página una advertencia sobre la necesidad de llamar al método Dispose
de un objeto de la clase WindowsIdentity
cuando se haya terminado de utilizar, en el propio ejemplo que publica Microsoft no se llama a este método de ninguna manera, lo cual de por sí ya induce a errores.
En cualquier caso, digamos que la web necesita por cualquier motivo obtener los grupos a los que pertenece el usuario que ha iniciado sesión. Una rápida búsqueda en Google nos devuelve resultados del foro de Microsoft, de codeproject o de c-sharpcorner entre otros muchos, donde el código propuesto en líneas generales es siempre el siguiente:
ArrayList groups = new ArrayList(); foreach (System.Security.Principal.IdentityReference group in System.Web.HttpContext.Current.Request.LogonUserIdentity.Groups) { groups.Add(group.Translate(typeof(System.Security.Principal.NTAccount)).ToString()); }
Al llamar a System.Web.HttpContext.Current.Request.LogonUserIdentity
estamos creando una instancia de la clase WindowsIdentity
que contiene información del usuario que ha iniciado sesión, generando un nuevo handle que apunta al token de usuario. Sin embargo, al no estar asignando el objeto a una variable que podamos manipular al salir del bucle no podemos llamar al método Dispose
, impidiendo de esta manera que se pueda cerrar el handle. Este código efectivamente devuelve todos los grupos del usuario que ha iniciado sesión, sin embargo, también filtra un handle que apunta al token de dicho usuario, el cual quedará abierto permanentemente y podrá ser utilizado para llevar a cabo la suplantación de identidad:
Al hacer la búsqueda anterior en Google aparecen otras alternativas para conseguir este resultado, pero todas las que aparecen en la primera página (y probablemente en las siguientes también) o al hacer búsquedas parecidas cometen este mismo error: instancian la clase WindowsIdentity
y no liberan los recursos llamando a Dispose
. Por lo tanto, da igual la alternativa por la que el programador se decante, que a menos que se sumerja en la documentación de Microsoft (para llegar a WindowsIdentity desde HttpContext es necesario seguir unos cuantos enlaces y tener muy claro qué estás buscando) al final su código terminará filtrando el token de los usuarios que inicien sesión en la aplicación.
Suplantación con excepción no controlada
La suplantación es una característica de los servidores IIS que permite ejecutar el código web en el contexto de seguridad de un usuario distinto al del Application Pool Identity. Lo más habitual es combinar esta característica con la autenticación de Windows, permitiendo de esta manera que el código web se ejecute en el contexto de seguridad del usuario que ha iniciado sesión, facilitando de esta manera el control de acceso al sistema de ficheros o a otros servicios.
Para llevar a cabo esta suplantación, el propio IIS lleva a cabo una manipulación del token del usuario que ha iniciado sesión para asignárselo a un nuevo hilo del proceso, y se encarga de su correcta liberación al terminar la ejecución del código web que se haya solicitado. Sin embargo, si se produce cualquier excepción no controlada en el código web que finalice de manera abrupta la ejecución, el IIS no podrá liberar correctamente el handle que apunta al token del usuario, el cual se filtrará y permanecerá abierto hasta que termine el proceso:
Esta combinación de autenticación de Windows, suplantación de identidad y excepción no controlada puede llevar al filtrado masivo de handles de token de usuarios.
Activación reflectiva de privilegios
Aunque esta técnica no se encuentra documentada por Microsoft, se incluye para ejemplificar las innumerables situaciones en .NET donde se pueden producir filtrados de recursos no administrados.
En este caso, una manera poco convencional de habilitar un privilegio en el token de usuario del proceso sin necesidad de hacer uso de la plataforma P/Invoke es utilizar reflexión sobre la clase System.Security.AccessControl.Privilege
:
Type privilegeType = Type.GetType("System.Security.AccessControl.Privilege"); object privilege = Activator.CreateInstance(privilegeType, "SeDebugPrivilege"); privilegeType.GetMethod("Enable").Invoke(privilege, null);
Esta técnica es bastante sencilla de implementar desde el punto de vista del desarrollador pero, al igual que ocurría en los ejemplos anteriores, se filtra el handle del token de usuario sin que el desarrollador tenga constancia de esta situación ni pueda hacer nada para evitarlo. Aquí podemos ver por qué se produce el filtrado del handle (se manipula el token de usuario asignado al hilo, pero no se libera al finalizar la ejecución del método), aunque no se puede considerar un fallo de programación dado que probablemente el método Enable
no esté diseñado para ser llamado directamente.
Si se hace uso de este código en un IIS que esté suplantando al usuario que ha iniciado sesión en la aplicación web a través de autenticación de Windows el handle filtrado apuntará al token de dicho usuario, permitiendo el cambio de contexto de seguridad por parte de un atacante.
Acceso a recursos de red
Interacción con el API de Windows
A pesar de la enorme abstracción que .NET proporciona a la hora de interactuar con el sistema operativo, existen multitud de situaciones donde es necesario llamar directamente a funciones de la API de Windows para ejecutar una acción determinada. Esta interacción se lleva a cabo en la mayoría de los casos a través de P/Invoke, que permite acceder a funcionalidades de código no administrado desde código administrado.
Además de añadir complejidad a la lógica del programa, una de las mayores desventajas de utilizar P/Invoke es que genera una situación parecida a la que hemos descrito al hablar de los objetos IDisposables: se crean referencias a objetos no administrados que no pueden ser eliminados por el Colector de Basura, por lo que se requiere de una liberación manual por parte del desarrollador.
No es de extrañar que la liberación manual de recursos pueda suponer un problema para los desarrolladores de .NET, dado que el propio concepto de un lenguaje administrado implica que ciertas tareas puedan ser abstraídas y obviadas por parte del programador.
En este contexto, tener una característica como el Colector de Basura y, a pesar de ello, tener que llevar a cabo la liberación manual de un handle es realmente una situación contradictoria que puede inducir al filtrado indeseado de recursos.
Llevando esta situación al tema que nos ocupa, existen infinidad de casos en los que se pueden abrir handles a tokens de usuario a través de P/Invoke, pero en cualquier caso esto implica que se ha de hacer una llamada a CloseHandle
por cada handle abierto previamente. En el caso contrario ya sabemos lo que ocurre: se filtrará un handle que permanecerá abierto permanentemente y podrá ser utilizado posteriormente por parte de un atacante.
El ejemplo más sencillo es una simple llamada a LogonUser
. Una llamada satisfactoria a esta función generará una nueva sesión y un token asociado a la misma será devuelto al programa para su posterior manipulación.
public string LeakToken() { IntPtr hToken = IntPtr.Zero; string username = "mdiaz.adm"; string password = "SuperStrongPassw0rd!"; string domain = "BLACKARROW"; var result = LogonUser(username, domain, password, 8, 0, ref hToken); WindowsIdentity id = new WindowsIdentity(hToken); WindowsImpersonationContext impersonatedUser = id.Impersonate(); var directoryPath = @"\\172.16.100.1\C$\RT"; var localPatt = @"C:\RT\file.txt"; directoryPath = directoryPath + @"\file.txt"; File.Copy(directoryPath, localPatt); impersonatedUser.Undo(); return "Leaked!"; }
En general, este token permitirá acceder a los recursos de red a los que tenga acceso el usuario en cuestión (a menos que el inicio de sesión sea de tipo Network) pero, además, si el usuario es administrador local podemos llegar a obtener un token con máximos privilegios en el equipo.
Una vez terminada la ejecución del fichero de ASP.NET y siempre teniendo en cuenta que no se ha producido la pertinente llamada a CloseHandle
, el resultado será un handle filtrado apuntando a un token potencialmente elevado y con acceso a la red:
En nuestra experiencia, es raro encontrar fragmentos de código en lenguajes como C o C++ donde no se produzcan las correspondientes llamadas a CloseHandle
para cerrar cada uno de los handles abiertos. Sin embargo, sí que es común encontrar esta situación en desarrollos para .NET, tal vez debido a la manera en la que se enseña a usar lenguajes administrados o porque la interacción directa con el API de Windows es menos habitual y no se tiene constancia de la filtración de recursos no administrados.
En cualquier caso, esta interacción directa con la API de Windows en aplicaciones web escritas en ASP.NET puede ser una verdadera mina de handles filtrados.
Directorios web
A la hora de desplegar un nuevo sitio web en un servidor IIS, uno de los requisitos que se nos solicita es indicar la ruta física en la que se encuentra el directorio web. Este directorio web se indicará con una ruta UNC, por lo que es posible añadir tanto rutas locales como remotas:A través de la opción “Connect as…” se le puede indicar al IIS con qué identidad ha de intentar acceder a este directorio web. Por defecto, el servidor intentará acceder con su Application Pool Identity, lo que significa que si la ruta física insertada corresponde a una ruta remota se intentará acceder al contenido del directorio web con la cuenta de máquina del equipo donde se aloja el IIS.
Sin embargo, dado que puede darse el caso en el que el Application Pool Identity no cuente con permisos suficientes para acceder al directorio web, la opción “Connect as…” te permite añadir de manera explícita credenciales alternativas que el IIS utilizará únicamente para acceder al contenido de este directorio:Estas credenciales únicamente se utilizan para acceder al directorio web. Es decir, el proceso del IIS seguirá ejecutándose en el contexto de seguridad de su Application Pool Identity, y se utilizarán las credenciales añadidas a través de esta opción para acceder al directorio web y hacer una copia local en un directorio temporal de todos aquellos ficheros que se necesiten para responder a las diferentes peticiones web que se reciban.
Lo interesante de esta opción es la manera en la que el IIS gestiona el acceso al directorio web. Dado que se ha indicado que el sitio web tiene su contenido alojado en un directorio al cual se ha de acceder con unas credenciales alternativas (en nuestro caso, van a ser las del usuario de dominio blackarrow\mdiaz.adm) el IIS tendrá que generar una nueva sesión para el usuario mdiaz.adm llamando a LogonUser
, y posteriormente utilizar el nuevo token generado para acceder a la ruta especificada.
En la configuración avanzada del sitio web vemos que el IIS nos indica justamente esto, dándonos la posibilidad además de indicar el tipo de inicio de sesión que queremos utilizar:Dependiendo del tipo de inicio de sesión asignado a esta configuración, el token resultante será distinto (se considera que las credenciales utilizadas pertenecen a un usuario miembro del grupo local de Administradores):
- Interactive: Se genera tanto un token elevado como uno sin privilegios de administrador. En ambos casos se cuenta con acceso a la red.
- Network: Token con privilegios de administrador, pero sin acceso a la red. Este tipo de inicio de sesión no se va a dar realmente cuando se pretenda acceder a un directorio web remoto.
- Batch y ClearText (por defecto): Token con privilegios de administrador y con acceso a la red.
Una vez que cualquier cliente accede por primera vez a la web, se crea la nueva sesión para mdiaz.adm y se generan varios handles de tipo token que quedan abiertos de manera indefinida:
En la imagen anterior podemos comprobar cómo efectivamente, dado que mdiaz.adm es miembro del grupo local Administradores y el tipo de sesión seleccionado es ClearText, el token que se filtra está elevado, además de proporcionar acceso a la red.
No sabemos el motivo concreto por el que el IIS mantiene referencias abiertas a estos tokens de manera indefinida, aunque suponemos que es por motivos de eficiencia, evitando de esta manera crear una nueva sesión para acceder al directorio web cada vez que se recibe una petición HTTP a un nuevo recurso.
Por otro lado, cada sitio web puede tener varios directorios web virtuales adicionales en los cuales se pueden añadir credenciales alternativas de acceso del mismo modo que lo hemos hecho con el directorio web principal, filtrándose así tokens de otros usuarios que podemos utilizar para escalar privilegios.
Este escenario no requiere de fallos de programación para filtrar handles y además parece un comportamiento “deseado” por parte de Microsoft.
Prueba de concepto
Una vez comentados los distintos escenarios, únicamente queda juntar todas las piezas del puzzle para obtener una shell web que nos permita localizar y utilizar los handles de token filtrados. Para ello, nuestra primera aproximación para localizar y utilizar los handles desde la shell fue la siguiente:
- Realizar una llamada a
NtQueryInformationProcess
solicitando todos los handles existentes en el proceso actual. Una vez obtenidos los handles, se itera sobre cada uno de ellos aplicando los pasos 2 y 3. - Llamar a
NtQueryObject
para determinar si el handle apunta a un token. - Obtener la información del usuario propietario del token llamando a
GetTokenInformation
yLookupAccountSid
. - Una vez el operador determina qué token quiere utilizar para la suplantación de identidad, se hace uso de
CreateProcessWithTokenW
para crear un nuevo proceso ejecutado en el contexto de seguridad del usuario a suplantar.
Si bien este enfoque funcionaba medianamente bien, en seguida nos percatamos de que la llamada a NtQueryInformationProcess
era muy inestable, y en muchos casos no devolvía todos los handles abiertos por el proceso, por lo que había que refrescar constantemente la shell para poder encontrar los handles que nos interesaban.
Por este motivo, modificamos ligeramente el comportamiento de la shell. Para ello, primero hay que entender que un handle en espacio de usuario no dejar de ser un índice que hace referencia a una entrada de la tabla de handles del proceso que se almacena en el kernel y, por lo tanto, es un número entero. Estos índices además se asignan más o menos de manera consecutiva y, además, el kernel tiende a reutilizar los índices de handles liberados previamente.
Con esta información, lo que podemos hacer es sustituir la llamada inestable a NtQueryInformationProcess
por un bucle que vaya iterando sobre todos los números en el rango [0,1000000] y cree una variable de tipo IntPtr
(que es como se suele encapsular a los handles en C#) por cada uno de estos índices. El rango lo hemos seleccionado en base a nuestras pruebas, dado que consideramos que es poco probable encontrar un proceso con más de 1 millón de handles abiertos y, además, a pesar de ser un número considerablemente elevado la operación sigue siendo rápida.
Evidentemente, no todos los índices dentro del rango seleccionado corresponderán a handles válidos, lo cual se soluciona con la llamada a NtQueryObject
. En el caso de que el índice sobre el que se itera no se corresponda con ningún handle existente en el proceso esta llamada devolverá el valor C0000008, que significa STATUS_INVALID_HANDLE
según la tabla de valores para la estructura NTSTATUS
, en cuyo caso se continuará con la siguiente iteración.
De esta manera, conseguimos una shell web que nos permite abusar de los handles filtrados por el proceso para escapar del contexto de seguridad del Application Pool Identity.
Para obtener la información sobre el nivel de integridad del token y si potencialmente nos otorga acceso a recursos de red, simplemente hacemos dos llamadas a la función GetTokenInformation
.
En concreto, para conocer si la sesión a la que apunta el token tiene credenciales cacheadas llamamos a GetTokenInformation
solicitando el TokenOrigin
. Según aparece en la documentación de Microsoft, si el token ha sido generado a través de un inicio de sesión con credenciales explícitas el TokenOrigin
devuelto contendrá el id de la sesión, y en caso de que se haya producido como consecuencia de una autenticación de red su valor será cero:
Salvo contadas excepciones, hemos comprobado que si el TokenOrigin es distinto de cero la sesión a la que apunta el token cuenta con credenciales cacheadas y tendrá acceso a recursos de red. Por lo tanto, consideramos que, aunque no sea infalible, es un método muy decente para averiguar si tenemos acceso a red sin necesidad de ser administrador del equipo.
En nuestro repositorio se puede encontrar una prueba de concepto completamente funcional de esta shell. Como mejores opcionales a implementar, nosotros proponemos principalmente las siguientes:
- Implementar la posibilidad de abusar también del privilegio SeAssignPrimaryToken llamando a
CreateProcessAsUser
. - Habilitar la carga dinámica de ensamblados tras llevar a cabo la suplantación, evitando así crear un nuevo proceso.
Conclusión
A lo largo de este post hemos visto diversas situaciones en las que se pueden filtrar handles de token de usuario en una aplicación web sobre un servidor IIS. Sin embargo, esto es simplemente la punta del iceberg y somos conscientes de que pueden existir una gran cantidad de aplicaciones y servicios en los que se esté dando esta situación.
Como ejemplos de escenarios que no hemos entrado a detallar podemos comentar nuevamente el uso de Autenticación Básica en un servidor IIS o los propios servicios web del AD CS, los cuales mantienen de manera permanente handles apuntando a tokens de todos los usuarios que inicien sesión en los mismos.
En cualquier caso y teniendo en cuenta la gran cantidad y diversidad de las aplicaciones web que las empresas ejecutan en sus servidores, esta técnica puede ser una nueva herramienta a la hora de escalar privilegios tanto a nivel local como de Directorio Activo tras comprometer una aplicación web durante un ejercicio.