原型污染漏洞实战指南:从发现到利用

本文深入探讨JavaScript原型污染漏洞的发现与利用方法,涵盖服务端和客户端的检测技术,通过实际案例演示漏洞利用过程,并介绍自动化工具DOM Invader的使用技巧。

快速上手原型污染漏洞

自我们发现原型污染漏洞以来,关于其本质和利用方式一直存在困惑。本文将讨论在真实环境中识别原型污染漏洞的最简单方法,这些漏洞可能引发各种利用方式!

原型污染可能存在于服务端或客户端。服务端原型污染可用于修改应用程序控制流,利用过程既有趣又有回报。当客户端存在原型污染漏洞时,可被用来实施跨站脚本(XSS)攻击。在这两种情况下,利用原型污染都高度依赖于具体环境。幸运的是,识别这类漏洞通常并不困难。

我们将通过几个示例来展示识别这种漏洞的简易程度,然后深入探讨这些漏洞产生的原因,并研究其他识别和利用方法。

发现服务端原型污染

识别此漏洞的最简单方法之一是向Burp Suite的Repeater发送API方法,并将JSON格式的请求体包装在__proto__对象中。

下面的示例展示了一个针对虚构披萨餐厅网站的有效请求。用户提供他们最喜欢的披萨类型和电话号码以注册奖励计划。

1
2
3
4
{
  "pizzaType": "pepperoni",
  "phoneNumber": "555-1234"
}

在后端,API解析请求体并确保所有必需参数都存在。如果我们发送以下请求,将收到400错误请求响应。

1
2
3
{
  "invalidKey": "value"
}

我们可以利用这种输入验证。然后将有效的请求体包装在__proto__对象中,如下所示,应用程序返回200 OK响应。

1
2
3
4
5
6
{
  "__proto__": {
    "pizzaType": "pepperoni",
    "phoneNumber": "555-1234"
  }
}

这看起来可能很奇怪,但这是我们发现此处存在漏洞的第一个指标。此时,请求被接受有两个潜在原因:

  1. API在请求体中搜索有效请求键
  2. 正在执行不安全的合并操作(稍后详述)

我们可以发送以下请求来排除第一个选项。如果应用程序正在搜索有效键,那么我们应该能够将__proto__对象更改为任何其他内容,应用程序将响应200 OK。例如,我们将__proto__更改为false_positive

1
2
3
4
5
6
{
  "false_positive": {
    "pizzaType": "pepperoni",
    "phoneNumber": "555-1234"
  }
}

由于应用程序拒绝了此请求,我们可以假设__proto__对象在应用程序中具有特殊含义。可以尝试变体如_proto___proto,响应都应返回400错误请求。

如果仅在对象名称为__proto__时收到200 OK响应,那么恭喜,你刚刚发现了服务端原型污染漏洞!稍后我们将深入探讨其工作原理以及如何利用。

测试客户端原型污染

PortSwigger已将自动化原型污染识别和利用功能添加到其浏览器工具DOM Invader中。该工具可以识别接收器和工具,甚至创建概念验证利用!

接收器是代码中可以修改原型对象的位置,例如应用程序未安全处理的URL参数。工具是可以利用污染对象进行利用的位置。DOM Invader使查找接收器和工具变得容易,只需确保拥有最新版本的Burp Suite并按照以下步骤操作:

  1. 在Burp中打开DOM Invader(Proxy > Intercept > Open Browser)
  2. 转到浏览器扩展,启用Burp Suite扩展
  3. 在扩展中打开DOM Invader和原型污染
  4. 重新加载页面并打开检查器,然后导航到新添加的"DOM Invader"选项卡
  5. 如果工具识别出接收器,则重新打开扩展并启用工具扫描
  6. 重新加载并导航回检查器的"DOM Invader"选项卡。顶部应看到进度条。如果为先前找到的接收器识别出任何工具,则应看到生成概念验证利用的选项

DOM Invader扩展在搜索客户端代码方面非常强大和有效,这些代码通常被压缩且在真实环境中难以阅读。如果你对手动方法感兴趣,强烈建议查看PortSwigger Academy的原型污染课程。

原型污染的工作原理

那么,为什么__proto__对象很特殊?如果你熟悉面向对象编程,“继承"这个词应该很耳熟。当创建新对象时,它们会从其类以及任何父类获得属性。在JavaScript中,存在"原型"对象的概念,这本质上是所有对象继承的根父对象。

在JavaScript中,原型对象是可写的,甚至在运行时也是如此。如果向原型添加任何属性,那么每个新创建的对象都将具有该属性。这允许我们修改开发人员从未打算(或期望)我们控制的变量!

考虑一个服务端合并函数,它获取一个对象的属性并在另一个对象中更新它们。你可能希望保存目标对象中存储的一些属性,仅更新源对象中描述的值。此外,包含在其他对象中的对象需要以相同方式复制。下面的代码片段可能是此问题的解决方案。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      if (!target[key]) {
        target[key] = {};
      }
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

这里的漏洞可能不明显。如果攻击者包含一个__proto__键,其值设置为对象,递归不仅会写入当前对象的原型,还会写入全局对象的原型。这意味着所有新创建的对象都将继承攻击者定义的属性。其后果仅受攻击者想象力的限制。

服务端原型污染利用

由于无法涵盖所有可能的情况,我们仅看几个如何利用此漏洞的示例。

建立环境

在利用原型污染时,环境至关重要。利用服务端任何内容的困难在于通常无法看到另一侧的情况。首先,尽可能进行发现。使用Gobuster和SecLists中的发现词表来查找服务器上的隐藏文件和位置。如果幸运,可能会找到源代码仓库。如果项目本身是开源的,你就可以开始深入挖掘了。

由于全局原型将在线程的整个生命周期中被污染,你可能能够利用应用程序使用的开源库。尝试通过模糊测试强制服务器返回错误消息。我最喜欢的工具选择是Burp Suite Intruder和wfuzz中提供的词表。攻击完成后,可以按长度和状态代码对响应进行排序,甚至可以搜索错误消息关键词。

收到的错误消息可能会返回部分(如果不是详细的话)堆栈跟踪,这可以帮助你映射后端源代码。此外,如果能够识别应用程序使用的任何库,你可能能够利用源提供的环境。

示例

作为示例,假设应用程序在收到POST请求时执行以下步骤:

  1. 服务器检查请求头中提供的会话令牌并接受或拒绝请求。用户ID存储在线程内存中供以后使用
  2. URL与更新用户配置文件的函数匹配
  3. 函数(不安全地)将请求数据合并到对象中以保存用户提供的数据
  4. 服务器验证所有必需属性是否包含
  5. 应用程序根据数据库中存储的信息创建用户对象
  6. 对用户对象执行权限检查,其中仅对管理用户设置属性
  7. 函数通过写回用户数据库并返回200 OK来完成其例程

在这种情况下,攻击者可以简单地向__proto__对象添加管理属性,从而提升请求的权限。请记住,在利用原型污染后创建的每个对象都会受到影响。

结论

随着JavaScript API的普及、语言本身的奇特特性以及漏洞的非明显性质,我认为我们尚未完全发现其危险性和利用潜力。

最后,我想向你推荐一些资源,这些资源将帮助你进一步探索这个主题。Olivier Arteau就这个主题写了一篇精彩的研究论文,其中包括从真实JavaScript库中发现和利用的示例。

Changhui Xu关于这个主题的另一篇精彩文章,非常清晰地描述了问题并提供了更多示例。

这两个先前来源都被官方CWE引用:不当控制的对象原型属性修改(‘原型污染’)。

PortSwigger有一个关于服务端原型污染的新课程(截至本文撰写时),可在其学院网站上获得。

最后,我为上面部分描述的示例创建了一个GitHub仓库。该示例大约100行代码,在一个文件中,没有依赖项,可以在Node.js或浏览器控制台中运行。可以将其用作参考或实验场。

快乐黑客, Isaac

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计