Pesquisadores do Centro Helmholtz de Segurança da Informação (CISPA) e do Instituto Real de Tecnologia (Suécia) analisaram a aplicabilidade da técnica de poluição do protótipo de objetos JavaScript para criar ataques à plataforma Node.js e a aplicativos populares baseados nela, levando à execução de código.
O método de poluição de protótipos utiliza uma característica da linguagem JavaScript que permite adicionar novas propriedades ao protótipo raiz de qualquer objeto. Em aplicativos, podem existir blocos de código (gadget) cujo funcionamento é afetado pela propriedade injetada, por exemplo, no código pode haver uma construção como 'const cmd = options.cmd || "/bin/sh"', cuja lógica de operação será alterada se o atacante conseguir injetar a propriedade "cmd" no protótipo raiz.
Para que o ataque seja bem-sucedido, é necessário que os dados externos recebidos pela aplicação possam ser usados para criar uma nova propriedade no protótipo raiz do objeto, e que durante a execução exista um gadget que dependa da propriedade alterada. A modificação do protótipo é realizada através do tratamento em Node.js das propriedades especiais "__proto__" e "constructor". A propriedade "__proto__" retorna o protótipo da classe do objeto, enquanto a propriedade "constructor" retorna a função usada para criar o objeto.
Se no código da aplicação houver uma atribuição "obj[a][b] = value" e os valores forem provenientes de dados externos, o atacante pode definir "a" como "__proto__" e obter a instalação de sua propriedade com o nome "b" e valor "value" no protótipo raiz do objeto (obj.__proto__.b = value;), e a propriedade definida no protótipo será visível em todos os objetos. Da mesma forma, se o código contém expressões do tipo "obj[a][b][c] = value", definindo "a" como "constructor" e "b" como "prototype", em todos os objetos existentes pode-se definir uma nova propriedade com o nome "c" e valor "value".
Exemplo de alteração de protótipo: const o1 = {}; const o2 = new Object(); o1.__proto__.x = 42; // criamos a propriedade "x" no protótipo raiz console.log(o2.x); // acessamos a propriedade "x" de outro objeto // o resultado será 42, pois através do objeto o1 o protótipo raiz foi alterado, sendo usado também pelo objeto o2
Exemplo de código vulnerável: function entryPoint (arg1, arg2, arg3){ const obj = {}; const p = obj[arg1]; p[arg2] = arg3; return p; }
Se os argumentos da função entryPoint forem gerados a partir de dados de entrada, o invasor pode passar para arg1 o valor "__proto__" e criar uma propriedade com qualquer nome no protótipo raiz. Se passar para arg2 o valor "toString" e para arg3 — 1, pode-se definir a propriedade "toString" (Object.prototype.toString=1) e causar a falha do aplicativo durante a chamada da função toString().
Como exemplo de situações que podem levar à execução do código do invasor, estão a criação de propriedades "main", "shell", "exports", "contextExtensions" e "env". Por exemplo, o invasor pode criar uma propriedade "main" no protótipo raiz do objeto, gravando nela o caminho para seu script (Object.prototype.main = "./../../pwned.js") e essa propriedade será chamada durante a execução no código da instrução require("my-package"), a menos que o pacote incluído defina explicitamente a propriedade "main" no package.json (se a propriedade não estiver definida, ela será obtida a partir do protótipo raiz). Da mesma forma, podem ser substituídas as propriedades "shell", "exports" e "env": let rootProto = Object.prototype; rootProto["exports"] = {".":"./changelog.js"}; rootProto["1"] = "/path/to/npm/scripts/"; // chamada de ativação require("./target.js"); Object.prototype.main = "/path/to/npm/scripts/changelog.js"; Object.prototype.shell = "node"; Object.prototype.env = {}; Object.prototype.env.NODE_OPTIONS = "--inspect-brk=0.0.0.0:1337"; // chamada de ativação require("bytes");
Pesquisadores analisaram 10 mil pacotes NPM com o maior número de dependências e descobriram que 1958 deles não possuem a propriedade main no package.json, 4420 usam caminhos relativos na expressão require, e 355 usam diretamente a API para substituir comandos.
Como exemplo funcional, pode-se mencionar um exploit para atacar o backend do Parse Server, que redefine a propriedade evalFunctions. Para facilitar a identificação de vulnerabilidades dessa natureza, foi desenvolvido um conjunto de ferramentas que combina métodos de análise estática e dinâmica. Durante os testes do Node.js, foram encontrados 11 gadgets que podem ser usados para realizar ataques que levam à execução do código do invasor. Além do Parse Server, duas vulnerabilidades exploráveis também foram identificadas no NPM CLI.
Fonte: opennet.ru
