在这个时代,Web 和移动应用程序通常是由 RESTful 网络服务提供支持的。 公共和私有 API 在互联网上非常普遍,测试这些 API 绝非易事,但有一些工具可以帮助你。 虽然(通常用与渗透测试)工具不能代替技能,但即使是最熟练的木匠也能用锤子比用鞋子更有效地钉钉子。
Postman 就是这样一个工具,它在开发者中已经流行了很多年。 在我们进入如何设置它的主题之前,我们先来快速介绍一下这个工具是什么以及可以做什么。 Postman 是一个商业桌面应用程序,可用于 Windows、 Mac OS 和 Linux。 它的大部分功能是免费的,也有付费的功能,比如提供协作和文档功能。 与渗透测试人员相比,这些特性对开发人员更有意义。 它用于管理测试各种 API 调用的 HTTP 请求集.合,以及包含变量的环境。 它并不能替代你的代理(如:Burp,ZAP,Mitmproxy 等等) ,但是实际上弥补了浏览器和客户端应用程序层缺失的功能。 对于这款工具主要的替代方案是开源工具 Insomnia 和高级 REST 客户端,商业产品 SoapUI,或建立在 Swagger/Swagger UI 或 curl 的自定义工具。
设置 Postman
在其官方网站上(https://www.getpostman.com,)可以找到 Postman,提供 Windows 和 MacOS 的安装程序,以及 Linux 的 tar 包。 它也可以在 Ubuntu 的 Snap for Ubuntu ( HTTPs://snapcraft.io/postman )和其他社区维护的 repos 中找到,比如 Arch Linux 的 AUR。 设置它的第一步当然是程序安装。
在第一次启动 Postman 时,你会看到一个屏幕,提示你创建一个帐户,注册谷歌,或者用现有的凭证进行登录。 然而, Postman 并不需要一个帐户来执行后续的使用。
登录的帐户用于协作/同步/等; 这些是付费的功能。正如我前面提到的,这几个功能对于开发人员来说很棒,但对你来说,可能并不关心。 事实上,如果你通常需要对你的客户端机械能保密,就像我们在 Secure Ideas 所做的事情,那么你可能明确地不希望将你的项目同步到另一个第三方服务器。
如果你低头看窗口的底部,你会看到一些浅灰色的文字,上面写着跳过登录,直接把我带到了应用程序界面。 点击这个灰色的链接,你将移动到下一个屏幕——一个提示你创建东西的对话框。
有几个部分你不会在这里使用到,所以让我们看看你真正关心的三个功能:
使用 Postman 的基本知识
是时候创建我们的第一个 Postman 集.合并发出 HTTP 请求。
左上角的 New 按钮通常用于创建集.合和请求。 让我们首先创建一个集.合。 这有点像一个单独的应用程序。 你将用于对相关请求进行分组。
集.合还可以作为具有身份验证指令的顶级项,这些身份验证指令将对单个请求进行继承。 现在,只要给它起个名字,然后点击创建按钮就行了。 这里,我起的名字是“测试集.合”。
默认情况下,你已经打开了一个未命名的请求选项卡。 让我们来看看 UI 的这一部分。
我在 HTTP://localhost:4000上设置了一个示例目标,所以我将从填写这个请求并保存到我的某个集.合作为开始。我将发出一个 POST 请求,到 HTTP://localhost:4000/legacy/auth ,没有任何参数(这是一个测试 API。 任何人都可以通过身份验证)。 当我点击保存按钮时,我将命名请求并为它选择一个集.合,如下图所示:
然后单击"保存到测试集.合"(根据你的集.合名称进行调整)按钮来保存我的请求。 现在,单击 Send 按钮将发出请求。 然后我将看到响应填充在窗口的下窗格中,如下图所示:
关于 Cookies 的一个旁注
现在,如果我们使用 Postman 重新发出请求,我们将注意到一个重要的事情: 之前在响应中设置的 cookie 会被自动包含。 它模仿了浏览器通常为你做的事情。 正如你对浏览器的期望一样,在这个 cookie 范围内发出的任何请求,Postman 都将自动包含该 cookie。
如果你想移除某个 cookie 该怎么做呢? 这很简单。 在 Postman 的发送按钮下面,有一个类似链接的按钮,上面写着 Cookies。 点击这个按钮,会打开一个对话框,在那里你可以删除或编辑任何你需要的 cookies。
这就是基于 cookie 的 API。 但是让我们面对现实: 现在我们常见的 API 都会使用无记名令牌(Bearer Token)这比使用 Cookies 更为普遍。
为什么要设置代理?
通过使用 Postman,我们可以将其作为一个优秀的工具,从零开始制作请求并管理这些请求。 通过 Burp 代理 Postman 的流量,我们可以得到这些好处: 我们可以与Burp 的 intruder 功能结合进行模糊测试,我们也可以利用 Burp 的被动扫描器检测突出的安全问题,我们也可以利用 Burp 的扩展插件,这一部分我们将在本系列文章的后续章节中看到。 而且我们可以使用Burp 的 Repeater 篡改请求。 或许你会说,我们也可以在 Postman 里面进行篡改啊。 但是我要说的是使用 Repeater 有两个重要的原因: 1) Postman 的设计是用于发出正确、有效的请求。 在某些情况下,它会尝试纠正格式不正确的语法。 而在测试安全问题时,我们可能不希望它纠正我们篡改的错误。 2)通过使用 Repeater,我们保持了在 Postman 中的请求的干净状态以及在 Repeater 中已篡改的请求这两者之间的分离。
设置 Burp Suite
对 Burp 的实际介绍超出了这篇文章的范围。 如果你正在阅读这篇文章,很可能你已经对它很熟悉了
现在,启动 Burp,检查 Proxy 下的 Options 选项卡。 最上面的部分是代理监听器(Proxy Listeners),你应该会在127.0.0.1和端口8080上看到一个监听器。 它必须一直运行(注意复选框)。 如果它在默认情况下没有运行,那通常意味着端口不可用,你需要将监听器(以及Postman)更改为不同的监听端口。 只要将 Burp 正在监听的端口以及你在 Postman 中设置的代理的地址和端口是同一个,那么你的设置应该没什么问题。
同时检查 Proxy 下的 Intercept 选项卡并验证 Intercept 是否关闭。
在 Postman 中配置 Burp 代理
Postman 是代理感知的,这意味着我们要将它指向我们的中间人代理,也就是 Burp Suite。 我们将通过点击右上角的扳手图标(1)打开设置对话框,然后点击下拉菜单上的设置选项(2)。 这会打开一个比较大的设置对话框,在顶部有不同类别设置的标签。 找到"代理"选项卡并单击进行设置。
打开 Postman 设置面板
在这个标签页上有三件事可以做:
默认的代理接口是127.0.0.1,端口是8080,这里假设你的 Postman 和你的 Burp Suite 是在同一台机器上运行的。 如果你想使用不同的端口,你需要在这里指定它,并确保它被设置为与 Burp 中的代理接口一样。
现在你可以代理流量了,还有一个问题需要考虑。 如今,大多数公共 API 都使用了 SSL/TLS 。 这是一件非常好的事情,但它也意味着当 Burp 作为代理中间人在处理 Postman 的 API 请求和响应时,你会遇到证书错误的问题,除非你的系统已经信任了 Burp 的证书颁发机构。 解决这个问题有两个方法:
验证代理是否正常工作
用 Postman 发出一些请求。 在 Burp 的 Proxy 选项卡上检查你的 HTTP 历史记录。
在 Burp Suite 中的代理历史记录
故障排除
现在我们已经做好了基本的工具链设置。
集.合变量
Postman 中的变量几乎可以用于请求中的任何字段。 语法是在它们的两边使用两层花括号。有几个地方我可以使用变量定义它们。 如果它们是静态的,也许我会将它们设置为集.合变量。 例如,我一直使用 http://localhost:4000作为我的测试主机。 如果我将测试 API 的端口从4000改为4001,我不希望一个个编辑每个请求的 URL。接下来我介绍一下我们该如何将它移动到一个集.合变量中。首先,在菜单侧栏中打开"集.合"列表中编辑该集.合的对话框。 我可以单击… 按钮或者右键单击集.合名称。这两种操作,我们会得到相同的上下文菜单,然后选择编辑(Edit)。
这将打开一个编辑集.合的对话框。 默认视图包括集.合的名称和描述的文本框,但是在这两个字段之间还有一行选项卡。
其中一个标签叫做变量(Variables)。这就是我们想要的,点击这个标签会打开另一个对话框,用于编辑变量。
Postman 集.合变量编辑界面
它有一个表格,其中包含某个变量的变量名称、 初始值列和 当前值列。 这两个值列之间的差异与 Postman 的付费功能进行同步有关。 在这里重要的一个点是,你将输入初始值,然后选项卡进入当前值字段。 这将自动将当前初始值填充到当前值字段中,并且它将如图所示。 现在我有了一个名为 API_host 的集.合变量,其值为 http://localhost:4000。 在完成了变量的编辑之后需要点击更新按钮。
现在是时候修改我的请求,并引用该变量,而不是使用硬编码的主机名和端口。
Postman中的请求,将URL更改为指向一个变量
我只是简单地用占位符替换了每个 URL 中对应的部分: {{API_host}},把鼠标悬停在占位符上可以展开这个变量,会显示变量值和范围。 这里有一些颜色编码也可以帮助我们。 当变量有效时,文本会变成橙色,但是如果我输入一个无效的变量名,文本将变成红色。
我仍然需要对每个请求进行一次更新,让它们使用某个变量。 但是在将来,如果我改变了端口,或者如果我切换到了 HTTPS,或者如果我将我的测试 API 部署到一个完全不同的主机上; 那么我就可以回到集.合变量那里并更新变量的值,我的所有请求都会相应地发生改变。
现在,集.合变量对于相对静态的字段以及不会经常发生改变的字段是很适合的,但是如果我在一个多租户的解决方案中测试多个环境和部署,甚至多个租户呢? 我可能会使用相同的请求集.合,但是使用不同的变量集.合。 那么在这种情下环境变量就可以处理这个问题。
环境变量(Environment Variables)
你可能已经注意到了窗口右上角的界面。 让我们打开看看:
在 Postman 中的环境变量界面
首先,我们需要点击管理环境按钮。 这会打开一个较大但空白的对话框,底部有一个 Add 按钮。 点击这个添加按钮。
你会看到另一个对话框。 这一个看起来几乎和集.合变量对话框一样,除了它有一个名字。
在这里,我把我的命名为 LocalTest。
我还添加了许多其他的变量,其中一个叫做bearer_token,值为 foo。 另一个是 user_id值为1。
一旦完成编辑,我们点击对话框底部附近的添加按钮,然后关闭管理环境对话框。 在我可以在这个环境中使用这个变量之前,还有最后一个重要的、经常被忘记的步骤:我们需要从环境选择器下拉菜单中选择这个环境。
现在这些额外的变量可以像上面的 API_host 变量一样进行访问: {{bearer_token}} 和 {{user_id}}
路由参数
在现代API中使用路由参数是很常见的。这些是作为URL主路径的一部分所提供的值。例如,考虑以下情况http://localhost:4000/user/42/preferences
这样的URL中的数字42实际上是一个参数,很可能是本例中的用户 ID。 当服务器端应用程序路由传入请求时,服务端会提取该值,并使其随时可用于最终处理请求和构造响应的函数。 这是一个路由参数。 这对于编辑参数或是在 Postman 中使用也比较简单。语法是将参数以冒号(:)后跟参数名的形式直接放入 URL 中。 对于 Postman 中的这个示例请求,我将其输入为{{API_host}}/user/:userId/preferences。 然后,在请求的参数(Params)选项卡上,我可以看到它被列出并设置了具体的值。 在下图中,我将其设置为在前面的环境变量中指定的user_id变量。
我也可以把我的变量直接写到URL中,但在我看来,这种方式更干净。
无记名令牌(Bearer Tokens)
想象一下这样一个场景: 你发出某种类型的授权请求,它用一个 bearer token 进行响应,然后你需要在所有其他请求中使用该令牌。 这样做的手动方式可能只是发出验证请求,然后从响应中复制并粘贴令牌到另外一个环境变量。 所有其他的请求都可以使用这个环境变量。
这样也可以正常发出请求,如果你有一个短期的令牌,那么这种做法可能会很痛苦。对于这个问题,有一个更优雅的解决方案。想想下面的响应:
我们已经发出了请求,并且收到了一个包含令牌的 JSON 响应。 现在,我想用自动化的方式使用新的bearer token来更新我的环境变量。 在请求界面上,有几个标签可以做到。 最右边的一个叫做测试。 这主要用于自动检查响应,以确定 API 是否失效,就像单元测试一样。 但是我们可以通过一些 JavaScript 语句来达到我们的目的。
我添加上面的脚本,单击保存,然后再次运行我的请求。 似乎所有的事情都和第一次一模一样。但是如果我使用快速查看(Quick Look)按钮来查看环境变量时……
我们可以看到当前值已经被自动更新。 这是第一步——我现在将值存储在一个我可以轻松引用的地方,但是它不会将Bearer token 这个令牌放入我的请求中。 我有两个方法。 第一个方法是,如果打开请求的 Authorization 选项卡,我们可以从类型下拉列表中设置一个Bearer token,并将其指向我的变量。
这种方法还不错,但是我需要在每个请求上都进行相同的设置。但是每个新的请求的默认授权类型是继承父类身份验证(Inherit auth from parent)。在本例中,父类是个集.合。因此,如果我将这个请求切换回默认的类型,那么我就可以进入编辑集.合(Edit Collection)进行设置(与我进入Collection变量的上下文菜单相同) ,然后进入Collection的Authorization选项卡。
这个功能展示的接口几乎与请求中的 Authorization 接口相同,我可以以相同的方式设置身份验证到信息。 现在的不同之处在于它是在集.合中管理的。 在默认情况下,我创建的每个新的请求都将包含该bearer token,除非我故意更改了该请求的类型。 例如,我的身份验证请求可能不需要bearer token,因此我需要在请求的 Authorization 选项卡上将类型设置为 No Auth。
偶尔,我会遇到一些应用程序,需要从响应中包含的XML或HTML主体中提取一些值。 在这种情况下,内置的xml2Json 函数有助于解析响应内容。
使用 xml2Json 将 HTML 主体整合到 JSON 对象中
还有一个需要注意的功能是 Pre-request Script 选项卡,它使用相同的基本脚本接口。 正如你可能期望的那样,它在请求发送之前执行。 我的一些同事使用这个功能来设置bearer token令牌,这是一个完全有效的方法,只不过不是我平时所使用的方法。 当你只是需要一次性操作的时候,这种方法也会很有帮助,尽管这种情况我一般遇不到。
设置 Jython
其中的一些扩展需要在 Burp Suite中设置Python环境,而Burp Suite在默认情况下是不配置Python环境信息的。如果你已经配置过了,那么你可以继续下面的操作。如果没有配置过,你可以打开 Extender-Options 并设置Jython的独立JAR的位置。如果你需要这个下载jar包,你可以从jython.org上找到。
设置好路径后,导航到 BApp Store 选项卡,就可以开始下载我们将要使用的扩展插件了。我想重点说明的两个插件是JSON Web Token Attacker和Autorize。
现在让我们更详细地看看这两个插件。
JSON Web Token Attacker
目前已经有许多处理身份验证和授权的方法。JWT可能是现代API上最常用的方法。如果在下图中检查Bearer token,你将注意到我们正在使用的JWT的几个明显标志:三个片段,由冒号字符分隔。Base64编码的头部和声明部分,后面跟着加密签名。
这个令牌可以很容易地在Burp suite内置的解码器中解码。只需选择令牌,Base64就可以解码整段文本。上图中的文本解码后,如下:
{"alg":"HS256","typ":"JWT"}.{"userid":1,"tokenid":"fac15939-de30-481d-9d13-6ae89ecd7370","iat":1552444637,"exp":155244823N30.Ü¢R¥çe4r-ø£m-³Ì@
虽然 Burp的 Decoder可以用这种方式以最小的损坏对文本进行解码,但我希望对其进行修改和重新编码。这样做将产生以下结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9LnsidXNlcmlkIjoxLCJ0b2tlbmlkIjoiZmFjMTU5MzktZGUzMC00ODFkLTlkMTMtNmFlODllY2Q3MzcwIiwiaWF0IjoxNTUyNDQ0NjM3LCJleHAiOjE1NTI0NDgyM04zMC7colISpedlNHItBvijbS0TmYWzzIFAAOmykTIWurMtDzVJ
这与原始值相似,但绝对不同。这是因为JWT不是单个base64编码的字符串。标头和声明部分是分开编码的,去掉了任何填充信息(由一个或两个等于号表示)。
在JWT实现中有一些缺陷,尽管大多数缺陷比较少见。 对这些缺陷的利用包括篡改JWT并重新进行编码。虽然这可以手动完成,但JSON Web Token Attacker能更容易完成这个过程。我们只需从请求中复制值,然后切换到该插件添加的Burp中的JOSEPH选项卡。接下来,打开Manual子选项卡,并将值粘贴到输入框中。然后点击加载按钮。
之后会显示一个攻击选择的下拉框界面。 这里有两种攻击方式: 密钥混淆(Key Confusion)和签名排除(Signature Exclusion)。虽然对这些攻击的深入研究超出了本文要说明的范围,但我会给出一个简介的总结,就是这两种攻击中的任何一种都会将头部的alg值更改为不同的算法;要么将其切换为None值(表示不需要签名),要么将其从不对称加密切换到对称加密,这样我们就可以用公钥生成有效的签名。其中任何一种方式的安全性的影响是,加密签名通常是保证令牌完整性的唯一方法。如果攻击者能够破解该签名,则可以修改该令牌,使其具有攻击者想要的任何声明。在这个示例中,这可能意味着攻击者会更改用户ID获取其他账户的会话并使用该帐户。
让我们来看一下“签名排除”攻击方式的操作步骤,并逐步完成基本的手动攻击:
密钥混淆的实际用法与签名排除几乎是一样的,只是你需要提供公钥并对其进行转换。 通常的做法是签名算法选择 RS256,并且公钥可用于验证签名。
另一个你可能已经注意到的事情是,Burp中的代理历史现在有高亮显示的功能和一个新的上下文菜单。
这是一个用于半自动攻击的插件。 我个人不喜欢这样使用这个插件,部分原因是因为我发现它有时会变得混乱并且生成格式错误的请求,还有一部分原因是因为我喜欢严格控制我发送的流量。另外,对于实现JWT失效黑名单的系统的一个罕见的情况是,格式不正确的令牌会将会话列入黑名单。
尽管如此,即使是手动攻击模式也极大地简化了生成修改过的JWT的过程。 这使得这种方法经常成为API测试的有用工具。我发现,产生这些漏洞利用的实现方式似乎并不常见,但是考虑到它们的严重性,应该始终对这些漏洞进行测试。
自动化(Autorize)
在API中可能会出现各种授权问题。除非API是完全公开的,否则你至少拥有经过身份验证的访问权限和未经过身份验证的访问权限。 如果存在某个特定用户拥有的资源或数据的概念,那么验证一个经过身份验证的用户不能访问他们不拥有的任何资源是非常有意义的。有些API还具有多个垂直级别的访问权限,特别是在有组织概念的情况下。一个用户可能比另一个用户有权访问更多的数据或功能。同样,这取决于测试人员来确认是否强制执行了这种权限配置。最后,在多租户系统中(例如,每个客户机组织都有自己独立的空间) ,租户之间的数据或访问泄漏是最大的安全问题之一。
对于所有这些情况,通用的测试策略是将资源和功能映射到具有正确权限的用户。 然后,我们用不同的访问令牌或会话重新发出相同的请求,并比较我们的访问结果。当权限模型具有任何真正的复杂性时,这个验证过程可能会非常耗时。自动化可以显著提高测试效率。让我们从导航到Burp中的Autorize选项卡作为开始,这样我们就可以完成基础设置。
* 界面底部的拦截过滤器选项卡可用于确定包含哪些请求。
现在已经设置好了 Autorize,你可以通过 Postman 发出新的请求,并注意左侧的 Autorize 区域会被填充。
我的示例 API 恰好具有相同的响应,从显示响应长度的三个数字列(分别为74和6)可以看出这一点。 第一个是对未修改请求的响应,第二个是替换了令牌的请求,第三个是未经过身份验证的响应。 剩下的两列用交通信号灯一样的颜色表示强制执行。 图中的黄色部分代表的是一种不确定的响应,而红色和绿色的部分分别用于表示授权是否得到强制执行或执行成功。
现在,可以使用底部的"未经过身份验证的请求探测器"和"强制请求探测器"选项卡来调整认证何时生效。 这能够让你根据你对 API 行为的理解创建匹配条件,并且可以极大地改进结果对响应的解释。 对于较大的 API,或者那些你预期要进行多次测试的 API,这么做当然是值得的。 但是,对于一次性测试,我通常不对此进行调优,而是手动检查响应或长度不一致的情况。
可以使用Autorize UI右侧的选项卡(类似于Burp中的典型请求和响应选项卡)来检查修改过的和未经身份验证的请求和响应。这对于研究请求之间为什么存在差异、API做了什么以及对服务端来说是否是正确的请求至关重要。
总结
虽然本文章的内容不够全面,但是希望这篇文章能够提供足够充分的介绍,让我们开始使用这些插件,这些插件在大多数API测试中都很有用,至少在现代API中是如此。自动化对于测试API和SOAP服务也是非常好用的,而JSON Web Token Attacker 则非常特定于某种已经变得相当流行的并且是常见的身份验证方式,特别是它在OAuth2实现中的使用。