Antes de empezar

El presente artículo no es de mi autoría si no de Pantelis Roditis (proditis@echothrust.com), esto es solamente una traducción realizada con el permiso del autor. El artículo original en inglés lo puedes encontrar en el siguiente enlace: Environment variables and how to get them - Pantelis Roditis.

Me pareció buena idea traducirlo dado que gracias a su lectura me permitió encontrar las banderas de entorno de la plataforma echoCTF.RED, además de aprender más acerca del comportamiento de las variables de entorno.

Introducción

Una variable de entorno es un valor nombrado que puede ser accedido y puede afectar el comportamiento de un proceso ejecutado en una computadora.

  • wikipedia

Piensa en las variables como nombres simbólicos que damos a los valores para evitar recordarlos. Imagina que usas el número 3.1415926535897932384626433 constantemente, a menos que seas una especie de robot, va a ser difícil recordarlo. Entonces, le asignas un nombre al valor pi=3.1415926535897932384626433 y ahora cada vez que quieras usar el valor simplemente usas el nombre simbólico pi, en vez de la secuencia gigante de números.

En este documento, se describirá el uso de variables de entorno, cómo se puede acceder a ellas, manipularlas y aprovecharlas para ingenieros de seguridad tanto ofensivos como defensivos.

Porque nos importa

Entonces, ¿por qué son importantes las variables de entorno desde la perspectiva de seguridad?

Solía ​​​​ser (en los viejos tiempos lol) que las variables de entorno eran visibles para todos y, como regla, la mayoría de los desarrolladores no las usaban para almacenar información confidencial. Sin embargo, a medida que crecía la adopción de variables de entorno, también aumentaba la necesidad de comenzar a almacenar información un poco más confidencial. Por lo tanto, se agregó la capacidad de ocultar las variables de entorno de otros usuarios y procesos a algunos, si no a todos, los sistemas UNIX y similares a UNIX (como Linux) y, por lo tanto, se limitó su superficie de ataque.

Ahora, nos hemos mudado a la era de los contenedores y las variables de entorno tienen un nuevo significado. Echa un vistazo rápido a Docker Hub y verás millones de imágenes que usan variables de entorno que contienen información confidencial. Desde nombre de usuario, contraseñas, claves de cifrado, tokens de autenticación, claves del sistema, hay una imagen con un uso de variable de entorno para adaptarse a cualquier imaginación…

Como ingeniero de seguridad ofensivo o defensivo, saber cómo encontrar lo que está expuesto y poder acceder a esta información es una habilidad casi necesaria.

Y no nos engañemos, esta es definitivamente una habilidad que apreciarás mientras juegas en nuestra plataforma en línea echoCTF.RED, ya que las variables de entorno parecen más difíciles de obtener que el acceso del usuario root…

Como funcionan

Usaremos una variedad de sistemas para examinar cómo se comportan las variables de entorno. Cuando no se menciona un sistema o shell, se puede asumir con seguridad que se trata de bash.

Comencemos definiendo y usando algunas variables:

$ myint=1
$ mystring="This is a long string"
$ echo $myint
1
$ echo $mystring
This is a long string

Si desde la misma terminal vuelves a ejecutar bash notarás que las variables han desaparecido.

$ echo $mystring
This is a long string

$ bash
$ echo $mystring

$

Lo mismo sucederá si abres otra terminal e intentas acceder a la variable. Entonces, ¿por qué pasa eso?

Las variables tienen efecto solo en la sesión actual. Para que una variable esté disponible en los comandos y sesiones subsecuentes que se generan a partir de uno existente, tenemos que “exportarlos” (usando export en la terminal).

$ export mystring
$ bash
$ echo $mystring
This is a long string

Muchos comandos y la mayoría de las shells usan archivos de configuración cuando se inician (por ejemplo, ~/.bashrc) para instruirles sobre la configuración de variables. Además, algunos comandos establecen sus propias variables para ayudar a los comandos subsecuentes (por ejemplo, USER establecido por el inicio de sesión, etc.).

Variables Docker

Comprender el concepto de visibilidad de variables es especialmente importante en situaciones en las que las variables de entorno se configuran desde el inicio del sistema (como los contenedores docker), lo que, en determinadas condiciones, hace que se exporten al espacio de proceso global.

No profundizaremos en los detalles específicos de docker, sólo en la medida en que sea necesario para comprender las variables.

Veamos un ejemplo con variables docker:

$ docker run -it -e "myvar=myvalue" bash 
root@envlab: / # echo $myvar
myvalue
root@envlab: / # 

Una cosa que notamos es que esta variable se exporta:

root@envlab: / # bash 
root@envlab: / # echo $myvar
myvalue

Incluso si comenzamos un inicio de sesión bash, la variable todavía está allí:

root@envlab: / # bash -l
4df007c4e761:/# echo $myvar
myvalue
4df007c4e761:/# 

Incluso si cambiamos de usuario y shell, la variable se encuentra ahí:

4df007c4e761:/# su -s /bin/sh bin
4df007c4e761:/$ id
uid=1(bin) gid=1(bin) groups=1(bin),1(bin),2(daemon),3(sys)
4df007c4e761:/$ echo $myvar
myvalue

Sin embargo, como podemos entender, esto podría tener algunos efectos secundarios muy peligrosos, por esta razón, muchos demonios que se ejecutan en el sistema eligen borrar el entorno para procesos posteriores (por ejemplo, php-fpm que tiene clear_env = yes por defecto). Esta es la razón por la que no se puede acceder a ETSCTF_FLAG cuando se obtiene shell como www-data.

Como obtenerlas

Entonces, vayamos a la parte jugosa sobre cómo encontrar y mostrar los valores de estas variables.

printenv - imprime todo o parte del entorno

El comando printenv se puede usar para ver las variables actuales definidas en el entorno.

root@envlab: / # printenv
HOSTNAME=envlab
PWD=/
HOME=/root
_BASH_VERSION=5.1.8
_BASH_BASELINE=5.1
_BASH_LATEST_PATCH=8
TERM=xterm
SHLVL=1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/bin/printenv
root@envlab: / # 

env - imprime el entorno o ejecuta un programa con un entorno modificado

Los comandos env y printenv se comportan exactamente igual cuando se usan sin parámetros. Sin embargo, env también se puede usar para ejecutar un comando con un entorno modificado.

  1. Imprimir le ambiente actual.
root@envlab: / # env
HOSTNAME=envlab
PWD=/
HOME=/root
_BASH_VERSION=5.1.8
_BASH_BASELINE=5.1
_BASH_LATEST_PATCH=8
TERM=xterm
SHLVL=1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env
root@envlab: / #
  1. Reiniciar el entorno y ejecutar /usr/bin/env para imprimir el entorno y ver que está vacío.
root@envlab: / # env -i /usr/bin/env 
root@envlab: / # 
  1. Reiniciar el entorno y ejecutar /usr/local/bin/bash. Lo que obtenemos son las variables que bash define por defecto.
root@envlab: / # env -i /usr/local/bin/bash
root@envlab: / # env
PWD=/
SHLVL=1
_=/usr/bin/env
root@envlab: / # 
  1. Ejecutar bash con la variable $HOME modificada.
root@envlab: / # env -i HOME=/home /usr/local/bin/bash
root@envlab: / # echo $HOME
/home
root@envlab: / # env
PWD=/
HOME=/home
SHLVL=1
_=/usr/bin/env

set BUILTIN - muestra o establece variables y funciones de shell

Sin opciones, el nombre y el valor de cada variable se muestran en un formato que se puede reutilizar como entrada para configurar o restablecer las variables configuradas actualmente. Las variables de solo lectura no se pueden restablecer.

Without options, the name and value of each shell variable are displayed in a format that can be reused as input for setting or resetting the currently-set variables. Read-only variables cannot be reset.

Este es un comando incorporado que se usa para definir y/o imprimir variables y funciones específicas de la shell.

Ejecutando solo set, devuelve muchas más variables que antes.

root@envlab: / # set
BASH=/usr/local/bin/bash
BASHOPTS=checkwinsize:cmdhist:complete_fullquote:expand_aliases:extquote:force_fignore:globasciiranges:hostcomplete:interactive_comments:progcomp:promptvars:sourcepath
BASH_ALIASES=()
BASH_ARGC=([0]="0")
BASH_ARGV=()
BASH_CMDS=()
BASH_LINENO=()
BASH_SOURCE=()
BASH_VERSINFO=([0]="5" [1]="1" [2]="8" [3]="1" [4]="release" [5]="x86_64-pc-linux-musl")
BASH_VERSION='5.1.8(1)-release'
COLUMNS=211
DIRSTACK=()
EUID=0
GROUPS=()
HISTFILE=/root/.bash_history
HISTFILESIZE=500
HISTSIZE=500
HOME=/root
HOSTNAME=envlab
HOSTTYPE=x86_64
IFS=$' \t\n'
LINES=55
MACHTYPE=x86_64-pc-linux-musl
MAILCHECK=60
OPTERR=1
OPTIND=1
OSTYPE=linux-musl
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PPID=0
PS1='\s-\v\$ '
PS2='> '
PS4='+ '
PWD=/
SHELL=/bin/ash
SHELLOPTS=braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor
SHLVL=1
TERM=xterm
UID=0
_=bash
_BASH_BASELINE=5.1
_BASH_LATEST_PATCH=8
_BASH_VERSION=5.1.8

Estas son variables que están definidas por nuestra instancia de la shell actual y, a menos que se modifiquen de otra manera, no serán heredadas por los procesos secundarios. Por ejemplo, podemos ver que las variables relacionadas con BASH_ no están configuradas en la invocación de la siguiente shell (ash en el siguiente ejemplo).

root@envlab: / # /bin/ash
/ # set
HISTFILE='/root/.ash_history'
HOME='/root'
HOSTNAME='envlab'
IFS=' 	
'
LINENO=''
OPTIND='1'
PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
PPID='1'
PS1='\w \$ '
PS2='> '
PS4='+ '
PWD='/'
SHLVL='2'
TERM='xterm'
_='/bin/sh'
_BASH_BASELINE='5.1'
_BASH_LATEST_PATCH='8'
_BASH_VERSION='5.1.8'
/ # 

Una ventaja adicional de usar set es que también muestra funciones que pueden haber sido definidas.

root@envlab: / # function mytest() { ls; }
root@envlab: / # set|tail
TERM=xterm
UID=0
_=set
_BASH_BASELINE=5.1
_BASH_LATEST_PATCH=8
_BASH_VERSION=5.1.8
mytest () 
{ 
 ls
}
root@envlab: / # mytest
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var

declare / typeset BUILTINS - declarar variables y/o darles atributos

Declarar variables y/o darles atributos. Si no se dan nombres, se muestran los valores de las variables. Este comando tiene la capacidad de mostrar y manipular variables, así como sus atributos (por ejemplo, int, array, exported, etc.).

Sin argumentos, funciona como set, mostrando las variables y funciones de la shell y del entorno.

envlab:/# declare
BASH=/usr/local/bin/bash
BASH_ALIASES=()
BASH_ARGC=([0]="0")
BASH_ARGV=()
BASH_CMDS=()
BASH_LINENO=()
BASH_SOURCE=()
.
.
.

Sin embargo, si ejecutamos declare -p también podemos ver sus atributos.

envlab:/# declare -p
declare -- BASH="/usr/local/bin/bash"
declare -i BASHPID
declare -A BASH_ALIASES=()
declare -a BASH_ARGC=([0]="0")
declare -x PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
declare -ir UID="0"

Lo siguiente es lo que significan los atributos de declare:

  • -a Cada nombre es una variable indexada en un arreglo.
  • -A Cada nombre es una variable de un arreglo asociativo.
  • -f Usar solo nombres de funciones.
  • -i La variable se trata como un entero.
  • -l Cuando se asigna un valor a la variable, todos los caracteres en mayúsculas se convierten en minúsculas.
  • -n Da a cada nombre el atributo nameref, convirtiéndolo en una referencia de nombre a otra variable.
  • -r Hacer nombres de solo lectura.
  • -t Dar a cada nombre el atributo de seguimiento.
  • -u Cuando se asigna un valor a la variable, todos los caracteres en minúsculas se convierten en mayúsculas.
  • -x Marcar nombres para exportar a comandos subsecuentes a través del entorno.

Para obtener más detalles sobre el comando declare, consulte las páginas del manual bash(1) en la sección SHELL BUILTIN COMMANDS.

El comando typeset se comporta de manera muy similar a declare y está allí principalmente para la compatibilidad con otros shells. Sin embargo, typeset sin opciones, devuelve todas las variables y funciones de la shell integradas.

ps

Todos los comandos anteriores que vimos funcionan en la sesión de la shell actual, sin embargo, muchas veces nos gustaría ver qué variables de entorno se definieron para una aplicación que ya se está ejecutando, que puede que nosotros hayamos iniciado o no.

El comando ps puede ayudar con eso. Este comando proporciona información sobre los procesos en ejecución en un sistema, como PID, nombre, tiempo de ejecución, memoria utilizada, etc. Al agregar algunas opciones adicionales, podemos ver mucha más información que solo estas. Las opciones que son de nuestro interés, junto con sus significados se pueden encontrar a continuación:

  • a: Selecciona todos los procesos excepto los dos líderes de sesión.
  • e: Muestra el entorno después del comando.
  • w: Amplia salida. Utilice esta opción dos veces para un ancho ilimitado.
  • f: Jerarquía de procesos en arte ASCII (forest).
  • u: Muestra formato orientado al usuario.
root@105b97a5ef81:/# ps -afeww
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 08:48 pts/0 00:00:00 /bin/bash /entrypoint.sh bash
root 17 1 0 08:48 pts/0 00:00:00 bash
root 93 17 0 08:55 pts/0 00:00:00 ps -feww

root@105b97a5ef81:/# ps -aufeww
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 17964 2900 pts/0 Ss 08:48 0:00 /bin/bash /entrypoint.sh bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=envlab TERM=xterm DEBIAN_FRONTEND=noninteractive HOME=/root
root 17 0.0 0.0 18180 3348 pts/0 S 08:48 0:00 bash HOSTNAME=envlab PWD=/ HOME=/root DEBIAN_FRONTEND=noninteractive TERM=xterm SHLVL=1 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin _=/bin/bash
root 94 0.0 0.0 36632 2908 pts/0 R+ 08:55 0:00 \_ ps -ufeww HOSTNAME=envlab PWD=/ HOME=/root DEBIAN_FRONTEND=noninteractive TERM=xterm SHLVL=2 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin _=/bin/ps

Podemos ver en el resultado anterior que obtenemos las variables de entorno tal como existen para cada uno de los comandos en ejecución. Sin embargo, hay algunas limitaciones. Si ejecutamos los mismos comandos que un usuario no root, podemos ver que no podemos ver el entorno de los procesos de otros usuarios.

databus@63f7264d374e:/$ ps -aueww
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 17964 2992 pts/0 Ss 09:04 0:00 /bin/bash /entrypoint.sh bash
root 18 0.0 0.0 18184 3280 pts/0 S 09:04 0:00 bash
root 67 0.0 0.0 46844 2772 pts/0 S 09:09 0:00 su - databus
databus 68 0.0 0.0 4276 708 pts/0 S 09:09 0:00 -su TERM=xterm PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games MAIL=/var/mail/databus HOME=/ SHELL=/bin/sh USER=databus LOGNAME=databus
databus 71 0.0 0.0 18188 3292 pts/0 S 09:09 0:00 bash -l MAIL=/var/mail/databus USER=databus HOME=/ LOGNAME=databus TERM=xterm PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games SHELL=/bin/sh PWD=/
sampleu+ 152 0.8 0.0 18196 3056 pts/1 Ss+ 09:13 0:00 bash
databus 160 0.0 0.0 36632 2888 pts/0 R+ 09:14 0:00 ps -aueww USER=databus PWD=/ HOME=/ MAIL=/var/mail/databus SHELL=/bin/sh TERM=xterm SHLVL=1 LOGNAME=databus PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games _=/bin/ps

Como podemos ver, los tres primeros procesos pertenecientes a root y sampleuser (una línea antes de la última) no tienen información de entorno incluida en la salida de ps.

/proc

Hay otra forma de obtener el entorno de un proceso en ejecución y esto es a través de /proc, el pseudo-sistema de archivos de información del proceso.

El sistema de archivos proc es un pseudo-sistema de archivos que proporciona una interfaz para las estructuras de datos del núcleo. Es comúnmente montado en /proc, automáticamente por el sistema.

El sistema de archivos proporciona una consulta fácil para consultar y manipular las estructuras del kernel como si fueran archivos simples. Para cada proceso en el sistema, hay un directorio correspondiente en /proc/<pid>/ con la información del kernel exportado. Los archivos ubicados debajo de esa carpeta corresponden a diferentes tipos de información del kernel, pero los que nos interesan son environ y cmdline.

/proc/[pid]/environ: Este archivo contiene el entorno inicial que se estableció cuando se inició el programa que se está ejecutando actualmente a través de execve(2). Las entradas están separadas por bytes nulos (\0), y puede haber un byte nulo en el fin. Por lo tanto, para imprimir el entorno del proceso 1, lo haría.

root@63f7264d374e:/# strings /proc/1/environ 
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=envlab
TERM=xterm
DEBIAN_FRONTEND=noninteractive
HOME=/root

/proc/[pid]/cmdline: Este archivo de solo lectura contiene la línea de comando completa para el proceso, a menos que el proceso sea un zombi. En el último caso, no hay nada en este archivo: es decir, una lectura de este archivo devolverá 0 caracteres. Los argumentos de la línea de comandos aparecen en este archivo como un conjunto de cadenas separadas por bytes nulos (\0), con un byte nulo adicional después de la última cadena.

root@63f7264d374e:/# strings /proc/1/cmdline 
/bin/bash
/entrypoint.sh
bash

La razón por la que queremos examinar el environ es bastante clara, sin embargo, cmdline puede no serlo. La razón por la que examinamos ambos archivos es que el comando que se inició puede incluir variables de entorno en su línea de comandos.

Ten en cuenta que, dependiendo de la opción de montaje hidepid=n, el sistema de archivos /proc puede comportarse de manera diferente:

  • 0: Todo el mundo puede acceder a todos los directorios /proc/[pid]. Este es el comportamiento tradicional y el predeterminado si no se especifica la opción de montaje.
  • 1: Los usuarios no pueden acceder a archivos y subdirectorios dentro de ningún directorio /proc/[pid] excepto el suyo propio (los directorios /proc/[pid] permanecen visibles). Los archivos confidenciales como /proc/[pid]/cmdline y /proc/[pid]/status ahora están protegidos contra otros usuarios. Esto hace que sea imposible saber si algún usuario está ejecutando un programa específico (siempre y cuando el programa no se revele por su comportamiento).
  • 2: Como en el modo 1, pero además los directorios /proc/[pid] que pertenecen a otros usuarios se vuelven invisibles. Esto significa que las entradas /proc/[pid] ya no se pueden usar para descubrir los PID en el sistema. Esto no oculta el hecho de que existe un proceso con un valor PID específico (se puede aprender por otros medios, por ejemplo, mediante kill -0 $PID), pero oculta el UID y el GID de un proceso, que de otro modo podrían aprenderse empleando stat(2) en un directorio /proc/[pid]. Esto complica enormemente la tarea de un atacante de recopilar información sobre procesos en ejecución (por ejemplo, descubrir si algún demonio se está ejecutando con privilegios elevados, si otro usuario está ejecutando algún programa confidencial, si otros usuarios están ejecutando algún programa, etc.).

¿Qué sigue?

Espero que hayas disfrutado de la lectura y que hayas podido aprender algunos trucos adicionales para encontrar variables de entorno para tu sesión, así como para ejecutar procesos. Todo lo que tienes que hacer ahora es volver a echoCTF.RED y ver si puedes obtener las variables de entorno de esos objetivos que aún no has terminado 😂.

Ten en cuenta que otros shells (ash, ksh, csh, etc.) pueden comportarse de manera diferente con respecto al entorno y las variables y funciones integradas. Los comandos descritos deben ser los mismos en la mayoría, sin embargo, la salida puede diferir. Siempre es una buena idea consultar las páginas del manual del shell respectivo con el que se está trabajando antes de probar cosas al azar.

Referencias