Desofuscando JavaScript con HackTheBox Academy

La desofuscación de código es una habilidad importante a aprender en el análisis de código e ingeniería inversa. Podemos encontrar malware que utilice JavaScript ofuscado para ocultar el payload de turno. Sin entender el código, no sabremos lo que hace.
En este módulo comenzaremos por ver la estrucura de una página HTML y localizaremos el código JavaScript en ella. Después aprenderemos qué es la ofuscación y cómo se hace, siguiendo con cómo desofuscar el código. Una vez desofuscado, intentaremos entender su uso para replicar su funcionalidad y descubrir manualmente qué hace.

Código Fuente

Mientras que HTML se usa para determinar los campos principales y parámetros de un sitio web y CSS para diseñarlo, JavaScript realiza las funciones necesarias para correr el sitio web, todo en segundo plano, por lo que solo vemos el front-end del sitio web, que es con lo que interactuamos.
Aunque el código esté disponible en el lado del cliente, será renderizado por el navegador, por lo que a menudo no nos fijaremos en el código HTML. Si queremos entender las funcionalidades de una página del lado del cliente, comenzaremos por mirar su código.

HTML

Comenzaremos visitando la siguiente URL:
http://SERVER_IP:PORT

Imagen de la web de Generador de Seriales

En ella no observamos ninguna funcionalidad. El próximo paso es ver su código fuente con CTRL + U, lo que debería abrir la vista de código del sitio web:
view-source:http://SERVER_IP:PORT

Código fuente de la web anterior

Aquí podemos ver el código HTML del sitio web.

CSS

Es código definido de forma interna dentro del mismo archivo HTML entre las etiquetas style, o de forma externa en un archivo .css separado y referenciado en el código HTML.

    <style>
        *,
        html {
            margin: 0;
            padding: 0;
            border: 0;
        }
        ...SNIP...
        h1 {
            font-size: 144px;
        }
        p {
            font-size: 64px;
        }
    </style>

Si se referencia un archivo .css externo, se hace mediante la etiqueta link dentro de la cabecera del HTML:

<head>
    <link rel="stylesheet" href="style.css">
</head>

JavaScript

Lo mismo se aplica para JavaScript. Se puede incluir en el HTML con la etiqueta script o externamente con un archivo .js separado y referenciado en el código HTML.

<script src="secret.js"></script>

Podemos comprobar el script en secret.js, el cual debería llevarnos directamente al script. Cuando lo visitamos, podemos ver que el código es complicado e incomprensible:

eval(function (p, a, c, k, e, d) { e = function (c) { '...SNIP... |true|function'.split('|'), 0, {}))

Esto se debe a la ofuscación del código. ¿Qué es eso?¿Cómo se hace?¿Dónde se usa?

Ofuscación de Código

Antes de comenzar con la desofuscación, debemos aprender sobre la ofuscación. Sin entender cómo se ofusca un código, probablemente no seremos capaces de desofuscarlo, especialmente si se hizo de forma personalizada.

¿Qué es la ofuscación?

Es una técnica usada para hacer que un script sea más difícil de leer para el ojo humano pero que funciona de la misma forma desde el punto de vista técnico. Se puede hacer de forma automática con herramientas de ofuscación, las cuales toman el código como entrada e intentan reescribirlo de forma que sea mucho más complicado de leer.
A menudo convierten el código en un diccionario de todas las palabras y símbolos usados dentro del código e intentan reconstruir el código original durante la ejecución mediante referencias a cada palabra y símbolo del diccionario.

Ejemplo de ofuscación de código JavaScript

Se publican y ejecutan códigos en muchos lenguajes sin ser compilados en lenguajes interpretados, como Python, PHP y JavaScript. Python y PHP suelen estar en el lado del servidor, JavaScript suele usarse en el navegador del lado del cliente, y el código se envía al usuario y se ejecuta en texto plano. Es por eso que se suele usar la ofuscación en JavaScript.

Casos de uso

Un motivo frecuente es ocultar el código original y sus funciones para prevenir su reutilización o copia sin el permiso del desarrollador. Otro motivo es proporcionar una capa extra de seguridad cuando tratamos con autenticación o cifrado para prevenir ataques sobre vulnerabilidades encontradas dentro del código.
No se recomienda la autenticación o cifrado en el lado del cliente, ya que el código es más propenso a ataques de esta forma.
El uso más común es en acciones maliciosas. Es común en los ataques ofuscar los scripts maliciosos para evitar que los sistemas de detección y prevención de intrusiones (IDS e IPS) detecten los scripts.

Ofuscación Básica

Normalmente no se realiza de forma manual. Se pueden encontrar muchas herramientas online y offline para ello, aunque algunos actores maliciosos y desarrolladores crean sus propias herramientas de ofuscación para ponerlo más difícil.

Ejecutando código JavaScript

Veamos la siguiente línea de código como ejemplo e intentemos ofuscarla:

console.log('HTB JavaScript Deobfuscation Module');

Lo primero será probar a ejecutar el código en texto claro, para verlo en acción. Podemos ir a JSConsole, pegar el código y pulsar Enter, y veremos su salida:

Captura de JSConsole

Vemos que se escribe el texto HTB JavaScript Deobfuscation Module, que es lo que hace la función console.log().

Minificando el código JavaScript

Una forma de reducir su legibilidad es mediante minificación. La minificación de código significa poner todo el código en una sola línea. Es muy útil para códigos largos, aunque si nuestro código consiste en una sola línea, no habrá mucha diferencia.
Algunas herramientas pueden ayudarnos a minificar el código JavaScript, como JavaScript Minifier. Copiamos el código y clicamos en Minify para obtener el resultado deseado:

Captura de JavaScript Minifier

De nuevo, podemos copiar el código minificado en JSConsole y ejecutarlo. Normalmente, el código JavaScript minificado se guarda como .min.js.

NOTA: La minificación de código no es exclusiva de JavaScript y se puede aplicar a otros lenguajes de la misma forma.

Empaquetando código JavaScript

Ahora ofuscaremos nuestra línea de código para hacerla más oscura y difícil de leer. Probaremos BeautifyTools para ofuscar el código:

Captura de BeautifyTools
eval(function(p,a,c,k,e,d){e=function(c){return c};if(!''.replace(/^/,String)){while(c--){d[c]=k[c]||c}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('5.4(\'3 2 1 0\');',6,6,'Module|Deobfuscation|JavaScript|HTB|log|console'.split('|'),0,{}))

Ahora nuestro código está mucho más ofuscado y es mucho más difícil de leer. Si lo probamos en JSConsole veremos que realiza la misma función:

Captura de JSConsole ejecutando código ofuscado

NOTA: Este método se conoce como «empaquetado» y es reconocible por la función de seis argumentos usada al principio «function(p,a,c,k,e,d)».

Normalmente, las herramientas de ofuscación por empaquetado intentan convertir todas las palabras y símbolos del código en una lista o diccionario y se refieren a ellos usando la función (p,a,c,k,e,d) para reconstruir el código original durante la ejecución. Esta función puede ser distinta dependiendo del empaquetador. Aún así, suelen mantener un orden a la hora de empaquetar las palabras y símbolos, para así conocer cómo ordenarlos durante la ejecución.
Aún podemos ver las cadenas principales escritas en texto plano, lo que puede revelar parte de su funcionalidad. Es por ello por lo que podemos querer buscar mejores formas de ofuscar nuestro código.

Ofuscación Avanzada

Obfuscator

Visitemos Obfuscator. Antes de hacer clic en ofuscar, convertiremos nuestra cadena a base64, como se puede ver en la imagen:

Captura de la herramienta online Obfuscator

Ahora pegamos nuestro código y clicamos en ofuscar:

Ofuscando código con Obfuscator

Obtenemos este código:

var _0x1ec6=['Bg9N','sfrciePHDMfty3jPChqGrgvVyMz1C2nHDgLVBIbnB2r1Bgu='];(function(_0x13249d,_0x1ec6e5){var _0x14f83b=function(_0x3f720f){while(--_0x3f720f){_0x13249d['push'](_0x13249d['shift']());}};_0x14f83b(++_0x1ec6e5);}(_0x1ec6,0xb4));var _0x14f8=function(_0x13249d,_0x1ec6e5){_0x13249d=_0x13249d-0x0;var _0x14f83b=_0x1ec6[_0x13249d];if(_0x14f8['eOTqeL']===undefined){var _0x3f720f=function(_0x32fbfd){var _0x523045='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=',_0x4f8a49=String(_0x32fbfd)['replace'](/=+$/,'');var _0x1171d4='';for(var _0x44920a=0x0,_0x2a30c5,_0x443b2f,_0xcdf142=0x0;_0x443b2f=_0x4f8a49['charAt'](_0xcdf142++);~_0x443b2f&&(_0x2a30c5=_0x44920a%0x4?_0x2a30c5*0x40+_0x443b2f:_0x443b2f,_0x44920a++%0x4)?_0x1171d4+=String['fromCharCode'](0xff&_0x2a30c5>>(-0x2*_0x44920a&0x6)):0x0){_0x443b2f=_0x523045['indexOf'](_0x443b2f);}return _0x1171d4;};_0x14f8['oZlYBE']=function(_0x8f2071){var _0x49af5e=_0x3f720f(_0x8f2071);var _0x52e65f=[];for(var _0x1ed1cf=0x0,_0x79942e=_0x49af5e['length'];_0x1ed1cf<_0x79942e;_0x1ed1cf++){_0x52e65f+='%'+('00'+_0x49af5e['charCodeAt'](_0x1ed1cf)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x52e65f);},_0x14f8['qHtbNC']={},_0x14f8['eOTqeL']=!![];}var _0x20247c=_0x14f8['qHtbNC'][_0x13249d];return _0x20247c===undefined?(_0x14f83b=_0x14f8['oZlYBE'](_0x14f83b),_0x14f8['qHtbNC'][_0x13249d]=_0x14f83b):_0x14f83b=_0x20247c,_0x14f83b;};console[_0x14f8('0x0')](_0x14f8('0x1'));

Obviamente, este código está mucho más ofuscado y no podemos ver restos de nuestro código original. Ahora lo ejecutamos en JSConsole para verificar el resultado.
Prueba las distintas opciones de Obfuscator para generar código aún más ofuscado.

Más ofuscación

Aún existen muchas variantes de herramientas de ofuscación, cada una ofuscando de forma distinta. Fijémonos, por ejemplo, en el siguiente código JavaScript:

[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(!
...SNIP...
[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]](!+[]+!+[]+[+[]])))()

Podemos seguir ejecutando este código y realizará su función original:

Código JavaScript ofuscado

NOTA: El código anterior está resumido, el completo sería mucho más largo, pero funcionaría.

Podemos intentar ofuscar código usando la misma herramienta en JSF y después devolviéndolo. El código tardará un poco en ejecutarse, lo que demuestra cómo la ofuscación afecta al rendimiento.
Hay otras herramientas como JJ Encode o AA Encode. Estas hacen que la ejecución/compilación sea muy lenta, por lo que no se recomienda su uso a no ser que sea por un motivo de peso, como saltarse filtros web o restricciones.

Desofuscación

Así como hay herramientas que ofuscan el código de forma automática, también las hay que lo desofuscan y lo hacen más legible automáticamente.

Embellecer

Para devolver el formato original al código, debemos embellecerlo, que es el proceso opuesto a minificarlo. El método más básico es a través de las herramientas de desarrollador del navegador.
Si usamos Firefox, podemos abrir el debugger del navegador con CTRL+SHIFT+Z, y después clica en nuestro script secret.js. Esto mostrará el script en su formato original, pero si clicamos sobre el botón ‘{ }’ en la parte inferior, embellecerá el script pasándolo a su formato adecuado:

Embelleciendo el código ofuscado

Además, también podemos utilizar herramientas online o plugins de editores de código, como Prettier o Beautifier. Copiamos el script secret.js:

eval(function (p, a, c, k, e, d) { e = function (c) { return c.toString(36) }; if (!''.replace(/^/, String)) { while (c--) { d[c.toString(a)] = k[c] || c.toString(a) } k = [function (e) { return d[e] }]; e = function () { return '\\w+' }; c = 1 }; while (c--) { if (k[c]) { p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]) } } return p }('g 4(){0 5="6{7!}";0 1=8 a();0 2="/9.c";1.d("e",2,f);1.b(3)}', 17, 17, 'var|xhr|url|null|generateSerial|flag|HTB|flag|new|serial|XMLHttpRequest|send|php|open|POST|true|function'.split('|'), 0, {}))
Captura de pantalla de la herramienta Prettier
Captura de pantalla de la herramienta Beautifier

Aún así, el código no es sencillo de leer, ya que estamos trabajando con un código que no ha sido solo minificado, sinó también ofuscado. Necesitaremos otras herramientas para desofuscar el código.

Desofuscar

Una buena herramienta es JSNice. Copiamos en ella el código ofuscado y la ejecutamos clicando sobre el botón Nicify JavaScript.

TRUCO: En las opciones debajo de «Nicify JavaScript», deseleccionamos «Infer types» para reducir el abarrotamiento de código con comentarios.

TRUCO: Asegúrate de no dejar líneas vacías antes del script, ya que puede afectar a la desofuscación y dar resultados inadecuados.

Captura de la herramienta JSNice

Esta herramienta realiza un mejor trabajo al desofuscar el código JavaScript y nos da una salida que podemos comprender:

'use strict';
function generateSerial() {
  ...SNIP...
  var xhr = new XMLHttpRequest;
  var url = "/serial.php";
  xhr.open("POST", url, true);
  xhr.send(null);
};

Otra forma de desempaquetar el código es encontrar el valor de retorno al final y usar console.log para imprimirlo en lugar de ejecutarlo.

Ingeniería inversa

Cuanto más se ofusque y codifique el código, más difícil será encontrar herramientas que automaticen el proceso de desofuscación. Esto es especialmente cierto cuando se usa una herramienta de ofuscación personalizada.
Entonces deberíamos usar ingeniería inversa sobre el código para entender cómo fue ofuscado y su funcionalidad.

Análisis de código

Ahora que hemos desofuscado el código, podemos comenzar a analizarlo:

'use strict';
function generateSerial() {
  ...SNIP...
  var xhr = new XMLHttpRequest;
  var url = "/serial.php";
  xhr.open("POST", url, true);
  xhr.send(null);
};

Solo contiene una función, generateSerial.

Peticiones HTTP

Analicemos cada línea de la función generateSerial.

Variables

La función comienda definiendo la variable xhr, que crea un objeto XMLHttpRequest. Buscamos en Google para qué sirve este objeto y vemos que es una función de JavaScript que gestiona peticiones.
La segunda variable es url, que contiene una URL a /serial.php, que debe estar en el mismo dominio, ya que no se especifica el mismo.

Funciones

Vemos que se usa xhr.open con «POST» y url. Si buscamos esta función en Google, podemos ver que abre una petición HTTP definida ‘GET’ o ‘POST’ a la URL, y después, la línea xhr.send enviaría la petición.

Por tanto, lo que hace generateSerial es enviar una petición POST a /serial.php, sin incluir datos POST ni obtener nada a cambio.
Los desarrolladores la han implementado para cuando necesiten generar un serial. Sin embargo, dado que no hay elementos en el código que generen serials, los desarrolladores no deben haberla usado todavía y la guardan para el futuro.
Con el uso de la desofuscación y el análisis de código, hemos descubierto esta función. Ahora podemos intentar replicar su funcionalidad para ver si se puede gestionar desde el lado del servidor al enviar una petición POST. Si la función está activada y se gestiona desde el lado del servidor, es posible que hayamos descubierto una funcionalidad no publicada, las cuales suelen tener errores y vulnerabilidades.

Peticiones HTTP

Ahora intentaremos enviar una petición POST a /serial.php mediante cURL.

cURL

Mediante el comando cURL obtenemos el mismo código fuente que cuando lo comprobamos en la primera sección de este módulo:

user@htb[/htb]$ curl http://SERVER_IP:PORT/

</html>
<!DOCTYPE html>

<head>
    <title>Secret Serial Generator</title>
    <style>
        *,
        html {
            margin: 0;
            padding: 0;
            border: 0;
...SNIP...
        <h1>Secret Serial Generator</h1>
        <p>This page generates secret serials!</p>
    </div>
</body>

</html>

Petición POST

Para enviar una petición POST, añadiremos el modificador -X POST a nuestro comando:

user@htb[/htb]$ curl -s http://SERVER_IP:PORT/ -X POST

TRUCO: Añadimos el modificador «-s» para reducir la saturación con datos innecesarios en la respuesta.

Sin embargo, las peticiones POST suelen contener datos POST. Para enviar los datos, podemos usar «-d ‘param1=sample'» e incluir nuestros datos para cada parámetro:

user@htb[/htb]$ curl -s http://SERVER_IP:PORT/ -X POST -d "param1=sample"

Utilizaremos esto para replicar lo que está haciendo server.js para entender mejor su propósito.

Descodificando

Ahora tenemos un extraño bloque de texto que parece estar codificado:

user@htb[/htb]$ curl http:/SERVER_IP:PORT/serial.php -X POST -d "param1=sample"

ZG8gdGhlIGV4ZXJjaXNlLCBkb24ndCBjb3B5IGFuZCBwYXN0ZSA7KQo=

Este es otro aspecto de la ofuscación. Algunas técnicas pueden ir más allá ofuscando el código y hacerlo menos legible para los humanos y menos detectable para los sistemas. Es por ello que a menudo encontrarás bloques de texto codificados que son descodificados en la ejecución. Los tres métodos más comunes son:

  • base64
  • hex
  • rot13

Base64

Se suele usar para reducir el uso de caracteres especiales, ya que todos los caracteres se representan con caracteres alfanuméricos, además de los símbolos + y /. Incluso si está en formato binario, el base64 resultante solo usará esos símbolos.

Identificando Base64

Las cadenas en base64 son fácilmente identificables ya que solo contienen caracteres alfanuméricos. El distintivo más claro es su relleno, que consta de caracteres =. La longitud de una cadena base64 ha de ser un múltiple de 4. Si faltan caracteres, se rellenan con «=».

Codificado en Base64

Para codificar un texto a base64 en Linux, podemos hacer un echo de la cadena y añadirle un pipe ( | ) y base64:

user@htb[/htb]$ echo https://www.hackthebox.eu/ | base64

aHR0cHM6Ly93d3cuaGFja3RoZWJveC5ldS8K

Descodificado en Base64

Pasamos la cadena con echo y con un pipe le añadimos el comando base64 -d a continuación:

user@htb[/htb]$ echo aHR0cHM6Ly93d3cuaGFja3RoZWJveC5ldS8K | base64 -d

https://www.hackthebox.eu/

Hex

Cada carácter es codificado en orden hexadecimal según la tabla ASCII.
Puedes encontrar la tabla ASCII completa en Linux con man ascii.

Identificando Hex

Cualquier cadena en hex estará compuesta solo por caracteres hexadecimales (0-9 y a-f), lo que lo hace bastante sencillo de identificar.

Codificado en Hex

Comando xxd -p:

user@htb[/htb]$ echo https://www.hackthebox.eu/ | xxd -p

68747470733a2f2f7777772e6861636b746865626f782e65752f0a

Descodificado en Hex

Comando xxd -p -r:

user@htb[/htb]$ echo 68747470733a2f2f7777772e6861636b746865626f782e65752f0a | xxd -p -r

https://www.hackthebox.eu/

César/Rot13

Otra técnica de codificado bastante común es el cifrado del César, creando un salto de las letras en un número fijo. Existen variaciones de este cifrado del César, como el rot13, que realiza un salto de 13 posiciones.

Identificando César/Rot13

Incluso aunque parezca que este método crea textos que parecen aleatorios, sigue siendo posible detectarlo ya que cada carácter se intercambia por otro carácter fijo.

Codificado en Rot13

No hay un comando específico en Linux que realice esta codificación. Sin embargo, es sencillo crear nuestro propio comando para hacer el cambio:

user@htb[/htb]$ echo https://www.hackthebox.eu/ | tr 'A-Za-z' 'N-ZA-Mn-za-m'

uggcf://jjj.unpxgurobk.rh/

Descodificado en Rot13

Se puede usar el mismo comando que para el codificado:

user@htb[/htb]$ echo uggcf://jjj.unpxgurobk.rh/ | tr 'A-Za-z' 'N-ZA-Mn-za-m'

https://www.hackthebox.eu/

Otra opción es el uso de alguna herramienta online como rot13.

Otros tipos de codificado

Si te enfrentas a tipo de codificado similares, intenta primero determinar el tipo de codificado y después busca herramientas online para descodificarlo.
Algunas herramientas pueden ayudarnos a determinar automáticamente el tipo de codificado, como Cipher Identifier.
Algunas herramientas también usan el cifrado, que consiste en codificar usando una clave, lo cual puede hacer que la ofuscación sea muy complicada de revertir, especialmente si la clave no está almacenada en el mismo script.

Conclusiones

Aquí termina el primer módulo de HackTheBox Academy que publico en La Ruta del Hacker. Si os habéis quedado con las ganas de saber más acerca del hacking con JavaScript o acerca de la codificación y descodificación, os recomiendo un par de libros que están relacionados con estos temas y que creo que os pueden ayudar mucho:

Como siempre, os invito a dejar vuestros comentarios aquí o en redes sociales con vuestras opiniones respecto a este post. Y ya sabéis, si queréis echarme una mano, compartid este artículo con vuestros amigos y allegados. Seguro que ellos también lo agradecerán, ya que es una información que desearán conocer.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.