Skip to content

Commit e6abd74

Browse files
authored
Merge pull request #1707 from HackTricks-wiki/update_ORM_Leaking_More_Than_You_Joined_For_20251223_124524
ORM Leaking More Than You Joined For
2 parents 3748718 + 2da69ea commit e6abd74

File tree

1 file changed

+102
-0
lines changed

1 file changed

+102
-0
lines changed

src/pentesting-web/orm-injection.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,36 @@ From te same post regarding this vector:
9595
- **PostgreSQL**: Doesn't have a default regex timeout and it's less prone to backtracking
9696
- **MariaDB**: Doesn't have a regex timeout
9797

98+
## Beego ORM (Go) & Harbor Filter Oracles
99+
100+
Beego mirrors Django’s `field__operator` DSL, so any handler that lets users control the first argument to `QuerySeter.Filter()` exposes the entire graph of relations:
101+
102+
```go
103+
qs := o.QueryTable("articles")
104+
qs = qs.Filter(filterExpression, filterValue) // attacker controls key + operator
105+
```
106+
107+
Requests such as `/search?filter=created_by__user__password__icontains=pbkdf` can pivot through foreign keys exactly like the Django primitives above. Harbor’s `q` helper parsed user input into Beego filters, so low-privileged users could probe secrets by watching list responses:
108+
109+
- `GET /api/v2.0/users?q=password=~$argon2id$` → reveals whether any hash contains `$argon2id$`.
110+
- `GET /api/v2.0/users?q=salt=~abc` → leaks salt substrings.
111+
112+
Counting returned rows, observing pagination metadata, or comparing response lengths gives an oracle to brute-force entire hashes, salts, and TOTP seeds.
113+
114+
### Bypassing Harbor’s patches with `parseExprs`
115+
116+
Harbor attempted to protect sensitive fields by tagging them with `filter:"false"` and validating only the first segment of the expression:
117+
118+
```go
119+
k := strings.SplitN(key, orm.ExprSep, 2)[0]
120+
if _, ok := meta.Filterable(k); !ok { continue }
121+
qs = qs.Filter(key, value)
122+
```
123+
124+
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.
125+
126+
v2.13.1 limited keys to a single separator, but Harbor’s own fuzzy-match builder appends operators after validation: `q=email__password=~abc``Filter("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.
127+
98128
## Prisma ORM (NodeJS)
99129

100130
The following are [**tricks extracted from this post**](https://www.elttam.com/blog/plorming-your-primsa-orm/).
@@ -298,6 +328,67 @@ It's also possible to leak all the users abusing some loop back many-to-many rel
298328

299329
Where the `{CONTAINS_LIST}` is a list with 1000 strings to make sure the **response is delayed when the correct leak is found.**
300330

331+
### Type confusion on `where` filters (operator injection)
332+
333+
Prisma’s query API accepts either primitive values or operator objects. When handlers assume the request body contains plain strings but pass them directly to `where`, attackers can smuggle operators into authentication flows and bypass token checks.
334+
335+
```ts
336+
const user = await prisma.user.findFirstOrThrow({
337+
where: { resetToken: req.body.resetToken as string }
338+
})
339+
```
340+
341+
Common coercion vectors:
342+
343+
- **JSON body** (default `express.json()`): `{"resetToken":{"not":"E"},"password":"newpass"}` ⇒ matches every user whose token is not `E`.
344+
- **URL-encoded body** with `extended: true`: `resetToken[not]=E&password=newpass` becomes the same object.
345+
- **Query string** in Express <5 or with extended parsers: `/reset?resetToken[contains]=argon2` leaks substring matches.
346+
- **cookie-parser** JSON cookies: `Cookie: resetToken=j:{"startsWith":"0x"}` if cookies are forwarded to Prisma.
347+
348+
Because Prisma happily evaluates `{ resetToken: { not: ... } }`, `{ contains: ... }`, `{ startsWith: ... }`, etc., any equality check on secrets (reset tokens, API keys, magic links) can be widened into a predicate that succeeds without knowing the secret. Combine this with relational filters (`createdBy`) to pick a victim.
349+
350+
Look for flows where:
351+
352+
- Request schemas aren't enforced, so nested objects survive deserialization.
353+
- Extended body/query parsers stay enabled and accept bracket syntax.
354+
- Handlers forward user JSON directly into Prisma instead of mapping onto allow-listed fields/operators.
355+
356+
## Entity Framework & OData Filter Leaks
357+
358+
### Reflection-based text helpers leak secrets
359+
360+
<details>
361+
<summary>Microsoft TextFilter helper abused for leaks</summary>
362+
363+
```csharp
364+
IQueryable<T> TextFilter<T>(IQueryable<T> source, string term) {
365+
var stringProperties = typeof(T).GetProperties().Where(p => p.PropertyType == typeof(string));
366+
if (!stringProperties.Any()) { return source; }
367+
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
368+
var prm = Expression.Parameter(typeof(T));
369+
var body = stringProperties
370+
.Select(prop => Expression.Call(Expression.Property(prm, prop), containsMethod!, Expression.Constant(term)))
371+
.Aggregate(Expression.OrElse);
372+
return source.Where(Expression.Lambda<Func<T, bool>>(body, prm));
373+
}
374+
```
375+
</details>
376+
377+
Helpers that enumerate every string property and wrap them inside `.Contains(term)` effectively expose passwords, API tokens, salts, and TOTP secrets to any user who can call the endpoint. Directus **CVE-2025-64748** is a real-world example where the `directus_users` search endpoint included `token` and `tfa_secret` in its generated `LIKE` predicates, turning result counts into a leak oracle.
378+
379+
### OData comparison oracles
380+
381+
ASP.NET OData controllers often return `IQueryable<T>` and allow `$filter`, even when functions such as `contains` are disabled. As long as the EDM exposes the property, attackers can still compare on it:
382+
383+
```
384+
GET /odata/Articles?$filter=CreatedBy/TfaSecret ge 'M'&$top=1
385+
GET /odata/Articles?$filter=CreatedBy/TfaSecret lt 'M'&$top=1
386+
```
387+
388+
The mere presence or absence of results (or pagination metadata) lets you binary-search each character according to the database collation. Navigation properties (`CreatedBy/Token`, `CreatedBy/User/Password`) enable relational pivots similar to Django/Beego, so any EDM that exposes sensitive fields or skips per-property deny-lists is an easy target.
389+
390+
Libraries and middleware that translate user strings into ORM operators (e.g., Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers) should be treated as high-risk sinks unless they implement strict field/operator allow-lists.
391+
301392
## **Ransack (Ruby)**
302393

303394
These tricks where [**found in this post**](https://positive.security/blog/ransack-data-exfiltration)**.**
@@ -324,10 +415,21 @@ GET /posts?q[user_reset_password_token_start]=1
324415

325416
By brute-forcing and potentially relationships it was possible to leak more data from a database.
326417

418+
## Collation-aware leak strategies
419+
420+
String comparisons inherit the database collation, so leak oracles must be designed around how the backend orders characters:
421+
422+
- Default MariaDB/MySQL/SQLite/MSSQL collations are often case-insensitive, so `LIKE`/`=` cannot distinguish `a` from `A`. Use case-sensitive operators (regex/GLOB/BINARY) when the secret’s casing matters.
423+
- Prisma and Entity Framework mirror the database ordering. Collations such as MSSQL’s `SQL_Latin1_General_CP1_CI_AS` place punctuation before digits and letters, so binary-search probes must follow that ordering rather than raw ASCII byte order.
424+
- SQLite’s `LIKE` is case-insensitive unless a custom collation is registered, so Django/Beego leaks may need `__regex` predicates to recover case-sensitive tokens.
425+
426+
Calibrating payloads to the real collation avoids wasted probes and significantly speeds up automated substring/binary-search attacks.
427+
327428
## References
328429

329430
- [https://www.elttam.com/blog/plormbing-your-django-orm/](https://www.elttam.com/blog/plormbing-your-django-orm/)
330431
- [https://www.elttam.com/blog/plorming-your-primsa-orm/](https://www.elttam.com/blog/plorming-your-primsa-orm/)
432+
- [https://www.elttam.com/blog/leaking-more-than-you-joined-for/](https://www.elttam.com/blog/leaking-more-than-you-joined-for/)
331433
- [https://positive.security/blog/ransack-data-exfiltration](https://positive.security/blog/ransack-data-exfiltration)
332434

333435
{{#include ../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)