跳转至

API测试

应用程序编程接口使得软件系统与应用能够相互通信并共享数据。因此,对 API 的测试非常重要,因为 API 中的漏洞可能会破坏网站的核心功能,如保密性、完整性和可用性。

所有动态网站都是由 API 构成的,因此像 SQL 注入这样的传统网络漏洞也可以被归类为 API 测试的范畴。在本课程中,我们将教你如何测试那些未被网站前端完全利用的 API,重点将是 RESTful 接口和 JSON 接口。此外,我们还会教你如何检测可能影响内部 API 的服务器端参数污染漏洞。

API识别

要开始进行 API 测试,首先需要尽可能多地了解有关该 API 的信息,以便识别其潜在的安全风险。

首先,你需要确定 API 端点。这些点是 API 接收关于其服务器上特定资源的请求的位置。例如,考虑以下 GET 请求:

Text Only
GET /api/books HTTP/1.1
Host: example.com

该请求的 API 端点为 /api/books 。这将触发与 API 的交互,以获取图书馆中的书籍列表。另外,也可能有一个其他的 API 端点,例如 /api/books/mystery ,该端点则可以获取推理小说列表。

一旦确定了端点地址,接下来就需要决定如何与它们进行交互。这样就能构建出有效的 HTTP 请求来测试 API 功能。例如,你需要获取有关以下信息的资料:

  • 该 API 处理的输入数据包括必填参数和可选参数。
  • 该 API 支持的请求类型,包括可用的 HTTP 方法和媒体格式。
  • 速率限制和认证机制。

API文档

API 通常会被详细文档化,以便开发者了解如何使用和与之集成。

查找 API 文档的方法

即使相关 API 的文档并未公开提供,您仍然可以通过浏览那些使用了该 API 的应用程序来获取相关信息。

为此,你可以使用 Burp Scanner 来遍历 API。或者,你也可以手动使用 Burp 的浏览器来浏览应用程序。寻找那些可能引用 API 文档的端点元素。例如:

  • /api
  • /swagger/index.html
  • /openapi.json

如果你确定了某个资源的端点地址,请务必检查基础路径。例如,如果你确定了资源端点 /api/swagger/v1/users/123 ,那么你应该检查以下路径:

  • /api/swagger/v1
  • /api/swagger
  • /api

可以通过字典进行目录扫描

验证API端点

你也可以通过浏览使用该 API 的应用程序来获取大量信息。即使你已经掌握了 API 文档的内容,这么做也是值得的,因为有时候文档中的信息可能是不准确或过时的。

你可以使用 Burp Scanner 来遍历应用程序的各个部分,然后手动使用 Burp 工具中的浏览器功能来调查那些值得关注的攻击面。

在浏览应用程序时,注意 URL 结构中是否存在暗示 API 端点的模式,例如 /api/ 这样的符号。同时留意 JavaScript 文件,因为这些文件中可能包含那些尚未通过网页浏览器直接调用的 API 端点的引用。Burp Scanner 在扫描时会自动提取一些端点信息,但如果你想进行更全面的提取,可以使用 JS Link Finder BApp 工具。你还可以手动检查 Burp 中的 JavaScript 文件。

与API端点进行交互

一旦确定了 API 端点,就可以使用 Burp Repeater 和 Burp Intruder 来与它们进行交互。这样就能观察 API 的行为,并发现更多的攻击机会。例如,你可以研究当 HTTP 方法和媒体类型发生变化时,API 会有怎样的反应。

确定支持的 HTTP 方法

HTTP 方法指定了对资源进行的操作。例如:

  • GET – 用于从资源中检索数据。
  • PATCH - 对资源进行部分修改。
  • OPTIONS – 用于获取关于可以在某个资源上使用的请求方法类型的信息。

一个 API 端点可能支持多种 HTTP 方法。因此,在测试 API 端点时,需要测试所有可能的方法。这样可以帮助你发现端点的其他功能,从而扩大攻击的可能性。

例如,端点 /api/tasks 可能支持以下方法:

  • GET /api/tasks – 用于获取任务列表。
  • POST /api/tasks - 创建一个新的任务。
  • DELETE /api/tasks/1 - 删除一个任务。

识别支持的内容类型

API 端点通常要求数据遵循特定的格式。因此,根据请求中数据的类型,它们的行为可能会有所不同。改变数据的类型或许能帮助你解决相关问题。

  • 触发能够揭示有用信息的错误。
  • 绕过那些有缺陷的防御措施。
  • 利用处理逻辑中的差异来发挥优势。例如,某个 API 在处理 JSON 数据时可能非常安全,但在处理 XML 数据时则容易受到注入攻击。

要更改内容类型,请修改 Content-Type 头文件,然后相应地重新格式化请求体数据。你可以使用Content type converter BBApp 来自动在 XML 和 JSON 格式之间转换请求中提交的数据。

查找隐藏的端点节点

一旦你确定了某些初始的 API 端点,就可以使用 Intruder 来发现那些隐藏的端点。例如,假设你已经找到了一个用于更新用户信息的 API 端点:

Text Only
PUT /api/user/update

为了识别隐藏的端点,可以使用 Burp Intruder 工具来查找具有相同结构的其他资源。例如,可以在路径的 /update 位置添加一些有效载荷,然后列出其他常见的函数,比如 delete 和 add 。

在寻找隐藏的端点时,可以使用基于常见 API 命名规范和行业术语的词汇表。同时,确保包含那些与应用程序相关的术语,这些术语需要根据初步的评估来确定。

寻找隐藏的参数

在进行 API 调试时,你可能会发现一些未被文档记录的参数。你可以尝试利用这些参数来改变应用程序的行为。Burp 工具提供了许多工具,可以帮助你识别这些隐藏的参数。

  • Burp Intruder 使你能够自动发现隐藏的参数。你可以使用常见的参数名称列表来替换现有的参数,或者添加新的参数。请确保包含与应用程序相关的参数名称,这取决于你对系统的初步分析。
  • Param miner BApp 允许你每次请求自动猜测最多 65,536 个 param 名称。Param miner 会根据从 scope 中获取的信息,自动猜测与应用程序相关的名称。

识别隐藏参数

由于批量分配会生成来自对象字段的参数,因此可以通过手动检查 API 返回的对象来识别这些隐藏的参数。

例如,考虑一个 PATCH /api/users/ 请求,该请求允许用户更新他们的用户名和电子邮件地址,并且包含以下 JSON 数据:

JSON
{
    "username": "wiener",
    "email": "wiener@example.com",
}

同时进行的 GET /api/users/123 请求返回了以下 JSON 数据:

JSON
{
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com",
    "isAdmin": "false"
}

这表明,那些隐藏的 id 和 isAdmin 参数已经与内部用户对象绑定在一起了,同时,更新的用户名和电子邮件参数也已经被考虑进去了。

测试漏洞

为了测试是否可以修改枚举类型的 isAdmin 参数值,请将其添加到 PATCH 请求中:

JSON
{
    "username": "wiener",
    "email": "wiener@example.com",
    "isAdmin": false,
}

此外,还应发送了一个包含无效参数值的 PATCH 请求:

JSON
{
    "username": "wiener",
    "email": "wiener@example.com",
    "isAdmin": "foo",
}

如果应用程序的行为有所不同,那么这可能意味着无效的值影响了查询逻辑,而有效的值则没有这种影响。这表明该参数可以被用户成功更新。

然后,你可以发送一个 PATCH 请求,将 isAdmin 参数的值设置为 true ,以此来尝试利用该漏洞:

JSON
{
    "username": "wiener",
    "email": "wiener@example.com",
    "isAdmin": true,
}

如果请求中的 isAdmin 值没有经过充分的验证和清理操作,就直接绑定到了用户对象上,那么用户 wiener 可能会错误地获得管理员权限。为了确定是否发生了这种情况,可以访问应用程序的 wiener 界面,看看是否能够使用管理员功能。

Graphql接口漏洞

GraphQL 的漏洞通常源于实现和设计上的缺陷。例如,可能会存在允许内省功能的激活情况,这会让攻击者能够查询 API 以获取有关其模式的信息。

GraphQL 攻击通常表现为一些恶意的请求,这些请求可以让攻击者获取数据或执行未经授权的操作。这类攻击可能会产生严重的后果,尤其是当攻击者通过操纵查询或执行 CSRF 攻击来获取管理员权限时。易受攻击的 GraphQL API 还可能导致信息泄露问题。

Tip

什么是Graphql:https://portswigger.net/web-security/graphql/what-is-graphql

查找 GraphQL 端点信息

在测试 GraphQL API 之前,首先需要找到它的端点。由于 GraphQL API 的所有请求都使用同一个端点,因此这一信息非常有价值。

通用查询

如果您将 query{__typename} 发送到任何 GraphQL 端点,那么该端点的响应中将会包含 {"data": {"__typename": "query"}} 字符串。这种机制被称为“通用查询”,是一种有效的工具,可以用来判断某个 URL 是否对应一个 GraphQL 服务。

image-20261011215224089

这个查询可以成功执行,因为每个 GraphQL 端点都有一个名为 __typename 的预留字段,该字段会以字符串形式返回被查询对象的类型。

常见的终端名称

Text Only
altair
explorer
graphiql
graphiql.css
graphiql/finland
graphiql.js
graphiql.min.css
graphiql.min.js
graphiql.php
graphql
graphql/console
graphql-explorer
graphql.php
graphql/schema.json
graphql/schema.xml
graphql/schema.yaml
playground
subscriptions
api/graphql
graph
v1/altair
v1/explorer
v1/graphiql
v1/graphiql.css
v1/graphiql/finland
v1/graphiql.js
v1/graphiql.min.css
v1/graphiql.min.js
v1/graphiql.php
v1/graphql
v1/graphql/console
v1/graphql-explorer
v1/graphql.php
v1/graphql/schema.json
v1/graphql/schema.xml
v1/graphql/schema.yaml
v1/playground
v1/subscriptions
v1/api/graphql
v1/graph
v2/altair
v2/explorer
v2/graphiql
v2/graphiql.css
v2/graphiql/finland
v2/graphiql.js
v2/graphiql.min.css
v2/graphiql.min.js
v2/graphiql.php
v2/graphql
v2/graphql/console
v2/graphql-explorer
v2/graphql.php
v2/graphql/schema.json
v2/graphql/schema.xml
v2/graphql/schema.yaml
v2/playground
v2/subscriptions
v2/api/graphql
v2/graph
v3/altair
v3/explorer
v3/graphiql
v3/graphiql.css
v3/graphiql/finland
v3/graphiql.js
v3/graphiql.min.css
v3/graphiql.min.js
v3/graphiql.php
v3/graphql
v3/graphql/console
v3/graphql-explorer
v3/graphql.php
v3/graphql/schema.json
v3/graphql/schema.xml
v3/graphql/schema.yaml
v3/playground
v3/subscriptions
v3/api/graphql
v3/graph
v4/altair
v4/explorer
v4/graphiql
v4/graphiql.css
v4/graphiql/finland
v4/graphiql.js
v4/graphiql.min.css
v4/graphiql.min.js
v4/graphiql.php
v4/graphql
v4/graphql/console
v4/graphql-explorer
v4/graphql.php
v4/graphql/schema.json
v4/graphql/schema.xml
v4/graphql/schema.yaml
v4/playground
v4/subscriptions
v4/api/graphql
v4/graph

Note

GraphQL 服务通常会对任何非 GraphQL 的请求返回“查询不存在”或类似的错误。在测试 GraphQL 端点时,请务必注意这一点。

请求方式

在尝试找到 GraphQL 端点的过程中,下一步就是使用不同的请求方法来进行测试。

对于生产环境中的 GraphQL 端点来说,最好只接受内容类型为 application/json 的 POST 请求。这样有助于防止 CSRF 攻击。不过,有些端点可能接受其他类型的请求,比如 GET 请求,或者使用内容类型为 x-www-form-urlencoded 的 POST 请求。

如果您通过向常见端点发送 POST 请求仍然无法找到 GraphQL 端点,请尝试使用其他 HTTP 方法来重新发送通用查询请求。

Note

Burp的自动扫描是可以发现GraphQL端点的:

image-20261011220705777

Exploiting unsanitized arguments

此时,你可以开始寻找潜在的漏洞了。测试查询参数是一个很好的起点。

如果 API 使用参数直接访问对象,那么可能会存在访问控制漏洞。用户只需提供一个与某些信息对应的参数,就有可能访问到本不应获得的信息。这种情况通常被称为不安全的直接对象引用漏洞。

例如,下面的查询请求的是一家在线商店的产品列表:

Text Only
#Example product query

    query {
        products {
            id
            name
            listed
        }
    }

返回的产品列表仅包含所列出的产品。

Text Only
#Example product response

    {
        "data": {
            "products": [
                {
                    "id": 1,
                    "name": "Product 1",
                    "listed": true
                },
                {
                    "id": 2,
                    "name": "Product 2",
                    "listed": true
                },
                {
                    "id": 4,
                    "name": "Product 4",
                    "listed": true
                }
            ]
        }
    }

根据这些信息,我们可以得出以下结论:

  • 这些产品都被分配了一个连续的 ID 号。
  • 产品编号 3 不在列表中,可能是因为该产品已经从列表中移除。

通过查询缺失产品的 ID,我们可以获取该产品的详细信息,即使该产品并未在商店中列出,且原始查询中也没有记录到该产品的退货信息。

Text Only
#Query to get missing product

    query {
        product(id: 3) {
            id
            name
            listed
        }
    }
Text Only
#Missing product response

    {
        "data": {
            "product": {
            "id": 3,
            "name": "Product 3",
            "listed": no
            }
        }
    }

查找模式信息

测试该 API 的下一步是整理有关底层模式的相关信息。

实现这一功能的最佳方式就是使用内省查询。内省是 GraphQL 内置的功能,它允许你向服务器查询有关模式的信息。

内省有助于你了解如何与 GraphQL API 进行交互。此外,内省还能揭示一些可能敏感的数据,比如描述字段中的信息。

使用内省方法

要使用内省来发现模式信息,请查询 __schema 字段。这个字段在所有查询的根类型中都是可用的。

与常规查询一样,在运行内省查询时,也可以指定希望返回的响应中的字段和结构。例如,你可能只希望响应中包含可用的突变名称。

Note

Burp可以自动生成内省查询测试语句:

image-20261011220910977

验证是否启用了内省

在生产环境中禁用内省功能是一种良好的实践,但这一建议并不总是被遵循。

你可以使用以下简单的查询来查询可用的 introspection 信息。如果启用了 introspection 功能,那么响应将会返回所有可用查询的名称。

Text Only
query{__schema{queryType{name}}}

image-20261011221414789

执行一个完整的 introspection 查询

下一步是对该端点执行一次全面的 introspection 查询,这样就能获得关于底层架构的尽可能多的信息。

下面的示例查询会返回所有查询、修改操作、订阅、类型以及片段的完整详细信息。

Text Only
#Full introspection query

    query IntrospectionQuery {
        __schema {
            queryType {
                name
            }
            mutationType {
                name
            }
            subscriptionType {
                name
            }
            types {
             ...FullType
            }
            directives {
                name
                description
                args {
                    ...InputValue
            }
            onOperation  #Often needs to be deleted to run query
            onFragment   #Often needs to be deleted to run query
            onField      #Often needs to be deleted to run query
            }
        }
    }

    fragment FullType on __Type {
        kind
        name
        description
        fields(includeDeprecated: true) {
            name
            description
            args {
                ...InputValue
            }
            type {
                ...TypeRef
            }
            isDeprecated
            deprecationReason
        }
        inputFields {
            ...InputValue
        }
        interfaces {
            ...TypeRef
        }
        enumValues(includeDeprecated: true) {
            name
            description
            isDeprecated
            deprecationReason
        }
        possibleTypes {
            ...TypeRef
        }
    }

    fragment InputValue on __InputValue {
        name
        description
        type {
            ...TypeRef
        }
        defaultValue
    }

    fragment TypeRef on __Type {
        kind
        name
        ofType {
            kind
            name
            ofType {
                kind
                name
                ofType {
                    kind
                    name
                }
            }
        }
    }

image-20261011221527816

Note

如果启用了内省功能,但上述查询仍然无法执行,那么请尝试从查询结构中移除 onOperation 、 onFragment 和 onField 这些指令。许多终端并不接受这些指令,因此在查询中移除它们通常能更好地实现内省功能。

可视化内省结果

可以使用:https://nathanrandal.com/graphql-visualizer/或https://apis.guru/graphql-voyager/,对内省查询的结果进行可视化展示

image-20261011221844542

image-20261011222147530

更多建议

即使内省功能完全被禁用,有时仍然可以通过一些技巧来获取有关 API 结构的信息。

Apollo GraphQL 平台的一个特点是,服务器可以在错误消息中提示用户修正查询中的错误。这种功能适用于那些查询虽然有些错误,但仍然可以识别的情况(例如, There is no entry for 'productInfo'. Did you mean 'productInformation' instead? )。

你或许可以从这些信息中获得一些有用的见解,因为回复实际上揭示了一些重要的模式信息。

Clairvoyance是一个很好的工具,它能够通过提示自动恢复整个或部分的 GraphQL 模式数据,即使在内视功能被禁用的情况下也是如此。这大大减少了从提示响应中提取信息所需的时间。

在 Apollo Server v4 及更高版本中,你可以使用 hideSchemaDetailsFromClientErrors 选项来禁用推荐功能。

绕过 GraphQL 的 introspection 防御机制

如果您无法让测试中的 API 执行内省查询,可以尝试在 __schema 关键词之后插入一个特殊字符。

当开发者禁用内观功能时,他们可以使用正则表达式来排除查询中的 __schema 关键词。建议尝试使用空格、新行和逗号等字符,因为这些字符会被 GraphQL 忽略,但不会被有缺陷的正则表达式所识别。

因此,如果开发者只是排除了 __schema{ ,那么下面的检查查询仍然会被包括在内。

Text Only
#Introspection query with newline

    {
        "query": "query{__schema
        {queryType{name}}}"
    }

如果这种方法仍然无效,可以尝试使用其他请求方法来执行探测操作。因为通常只有在使用 POST 方法时才能禁用内嵌式请求。可以尝试发送 GET 请求,或者发送一个内容类型为 x-www-form-urlencoded 的 POST 请求。

下面的例子展示了通过 GET 请求发送的包含 URL 编码参数的内省探针请求。

Text Only
# Introspection probe as GET request

GET /graphql?query=query%7B__schema%0A%7BqueryType%7Bname%7D%7D%7D

通过使用别名来绕过速率限制

通常情况下,GraphQL 对象不能包含多个同名属性。不过,通过创建别名可以绕过这一限制,从而明确指定 API 应返回的属性名称。利用别名,你可以在一次请求中返回同一类型对象的多个实例。

更多参考:https://portswigger.net/web-security/graphql/what-is-graphql#aliases

虽然别名的目的是限制需要调用的 API 调用的数量,但它们也可以被用于 brute-force 攻击 GraphQL 端点。

许多终端节点都配备了某种速率限制器,以防止暴力攻击的发生。一些速率限制器是根据接收到的 HTTP 请求数量来进行限制的,而不是根据终端节点执行的操作数量。由于别名功能允许在单个 HTTP 消息中发送多个查询,因此它们可以绕过这种限制。

下面的简化示例展示了一系列带有别名查询的用例,这些查询用于验证商店的折扣码是否有效。这种操作可以绕过速率限制,因为只是一个单独的 HTTP 请求而已。不过,这种方式也可以用来同时检查大量的折扣码。

Text Only
#Request with aliased queries

    query isValidDiscount($code: Int) {
        isValidDiscount(code:$code){
            valid
        }
        isValidDiscount2:isValidDiscount(code:$code){
            valid
        }
        isValidDiscount3:isValidDiscount(code:$code){
            valid
        }
    }

其他攻击面

DOS攻击

由于 GraphQL 允许客户端随意构造查询结构,攻击者可以构造极其消耗服务器资源的查询,导致 CPU 飙升或内存溢出。

别名滥用

GraphQL 允许使用“别名”在同一层级请求同一个字段多次。攻击者可以在一个 HTTP 请求中发送成千上万个别名,迫使后端执行大量重复的数据库查询。

Text Only
{
  user1: user(id: 1) { name }
  user2: user(id: 1) { name }
  user3: user(id: 1) { name }
  # ... 攻击者可以使用脚本生成 10,000 个别名 ...
  user10000: user(id: 1) { name }
}
深度嵌套查询

如果数据模型存在关联(例如 User 关联 Post,Post 关联 Author),攻击者可以构造极深的嵌套查询。

Text Only
{
  users {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                # ... 嵌套 100 层以上 ...
              }
            }
          }
        }
      }
    }
  }
}

后端在解析和解析这种深层结构时,会消耗大量 CPU 和内存,甚至导致栈溢出

常见的注入攻击

GraphQL 本身只是一个解析层,它最终会将数据传递给后端的 Resolver(解析函数)。如果 Resolver 直接将用户输入拼接到 SQL、NoSQL 或系统命令中,就会产生注入漏洞。GraphQL 只是成为了注入攻击的“新载体”。

比如SQL注入:

Text Only
query {
  user(id: "1' OR '1'='1' --") {
    name
  }
}

如果后端直接将GraphQL传递的id参数值拼接到SQL语句中进行查询,那么就存在SQL注入漏洞

越权与逻辑漏洞

GraphQL 本身不处理任何授权逻辑。它只负责“你要什么,我给什么”。如果后端的 Resolver 没有严格校验当前用户的权限,就会导致越权。

Text Only
# 攻击者尝试获取其他用户(如管理员 ID=2)的敏感信息
query {
  user(id: 2) {
    id
    name
    # 假设后端有 email 字段,但前端正常界面不显示
    email 
  }
}

水平越权(访问他人数据)或垂直越权(访问管理员数据)。由于 GraphQL 可以按需获取字段,攻击者更容易试探出后端存在哪些未公开的敏感字段。

错误信息泄露

在开发环境中,GraphQL 会返回详细的错误堆栈信息。如果在生产环境中未关闭,攻击者可以通过故意构造错误的查询来探测后端的实现细节。

Text Only
query {
  user(id: "abc") { # 假设 id 必须是 int
    name
  }
}

image-20261011223536544

Note

由于GraphQL只是一种接口形式,所以其他常见的Web漏洞仍然可能存在,更多参考:Graphql接口安全问题浅析-先知社区

如何防御API漏洞

在设计 API 时,务必从一开始就考虑安全性问题。具体来说,请确保做到以下几点:

  • 如果您不希望自己的 API 具有公开可访问性,请务必确保文档的安全性。
  • 请确保您的文档保持最新状态,这样合法的测试人员就能全面了解该 API 的潜在风险点。
  • 应用一个允许的HTTP 方法的白名单。
  • 确认每个请求或响应都符合预期的内容类型要求。
  • 使用通用的错误信息,以避免泄露可能对攻击者有用的信息。
  • 对 API 的所有版本都采取保护措施,而不仅仅是针对当前的生产版本。