我们的 AI 引擎 Neutron 在加州大学伯克利分校的 CyberGym 基准测试中取得了 96.75% 的成绩。 了解更多

工程

工程

借助 Proxy 对象插桩增强 PostMessage XSS 检测

本文介绍一种利用 JavaScript Proxy 对象检测 PostMessage 跨站脚本(XSS)漏洞的新方法,用以增强传统的动态模糊测试技术。

引言

跨站脚本(XSS)漏洞始终是一种持续存在的威胁。它利用 Web 应用的动态特性来执行恶意脚本。在可被 XSS 攻击利用的众多渠道中,HTML5 postMessage API 由于被广泛用于实现跨源通信而格外突出。

然而,这种便利也打开了漏洞的“潘多拉魔盒”,在未经严格校验时尤其如此。传统检测方法虽然在一定程度上有效,但往往难以准确定位并缓解此类漏洞,在复杂的、动态生成的 Web 环境中更是如此。

本文介绍一种增强 PostMessage XSS 漏洞检测的新方法。该方法利用 JavaScript Proxy 对象进行插桩,并在动态模糊测试之前加入一个画像(profiling)阶段。

通过将 Proxy 对象插桩融入对 Web 应用的初步画像中,我们为更有针对性、更高效的模糊测试过程奠定了基础。这种方法简化了对细微 XSS 向量的检测。

下文将深入探讨该方法的技术细节及其实现。通过这一探讨,我们希望为开发人员、安全专业人员和研究人员提供一套强大的工具,帮助他们加固 Web 应用,抵御始终存在的 XSS 攻击威胁。

背景

postMessage API 是 HTML5 不可或缺的一部分,它通过实现跨源消息传递彻底改变了 Web 通信。它旨在促进来自不同源的文档之间的交互,在现代 Web 中扮演着关键角色,从第三方小组件到复杂的单页应用都依赖于它。然而,它的灵活性和强大功能也使其成为被利用的目标,主要是通过跨站脚本(XSS)攻击。

XSS 攻击是指向其他用户浏览的网页中注入恶意脚本,利用用户对特定网站的信任。传统的 XSS 漏洞源于未经妥善净化的用户输入被直接包含在网页内容中。postMessage API 通过跨源发送消息为此类攻击引入了新的向量,这些消息可能包含能在接收页面上下文中执行的恶意内容。

常见漏洞与陷阱

源校验

与 postMessage 相关的主要漏洞之一是缺乏源校验。接收到消息时,如果没有检查消息的源,或者该检查实现不正确,就可能接受来自恶意来源的消息。以下示例展示了一个存在漏洞的实现:

window.addEventListener('message', (event) => {

  // Dangerous: No check for `event.origin`

  eval(event.data);

});

在这段代码中,消息事件监听器不加区分地执行任何接收到的消息中包含的代码,而不验证其源。这种做法为执行任意 JavaScript 代码敞开了大门,使其成为 XSS 攻击的首要目标。

带有源校验的代码示例。

window.addEventListener('message', (event) => {
  // Securely checking the origin of the message
  if (event.origin === 'https://trusted-origin.com') {
    // Assuming the content is safe, further validation can also be implemented here
    eval(event.data);
  } else {
    console.error('Untrusted origin:', event.origin);
  }
});

跨站脚本

当应用对通过 postMessage API 接收的数据处理不当,导致执行不可信且可能恶意的代码时,就会发生 PostMessage XSS。该漏洞通常源于两种常见的疏忽:

  • 缺乏源校验:未校验传入消息的源,可能使攻击者能够从不可信来源发送恶意消息。
  • 消息内容净化不当:将接收到的消息内容视为可信输入而不进行充分净化,可能导致执行任意 JavaScript 代码。

设想一个 Web 应用会监听消息,并根据通过 postMessage 接收的数据动态更新内容:

window.addEventListener('message', (event) => {
  // Assume the content of the message is a URL to be navigated to
  if (event.data.url) {
    window.location.href = event.data.url; // Potential for exploitation
  }
});

在这种场景下,攻击者可以构造一条包含 JavaScript URL(javascript:)的消息,从而导致任意代码执行:

parent.postMessage({url: "javascript:alert('XSS')"}, "*");

JavaScript 中的 Proxy 对象

JavaScript Proxy 对象是 ECMAScript 2015(ES6)中引入的一项强大特性,它可以为另一个对象创建代理。这使您能够拦截并重新定义该对象的基本操作,例如属性查找、赋值、枚举和函数调用。这一能力在多种高级场景中尤其有用,例如对象虚拟化、日志记录、性能剖析和数据校验。

Proxy 对象插桩基础

创建 Proxy 对象需要两个参数:它所封装的目标对象,以及为各种操作定义陷阱(trap)的处理器对象。拦截逻辑就定义在处理器对象中。以下是一个基本示例:

let target = {};
let handler = {
  get: function(obj, prop) {
    console.log(`Accessing property ${prop}`);
    return prop in obj ? obj[prop] : 37; // Default value
  }
};

let proxy = new Proxy(target, handler);
console.log(proxy.a); // Output: Accessing property a
                      // 37 (since 'a' is not a property of target)

在此示例中,get 陷阱会记录对属性的访问,并在目标对象上不存在该属性时返回默认值。

Proxy 对象的局限性

虽然 JavaScript Proxy 为安全插桩和其他高级操作提供了强大的能力,但它也存在一定的局限性。理解这些局限性对于开发人员在应用中有效使用 Proxy 至关重要。

  1. 无法拦截对原生类型的操作:JavaScript Proxy 无法直接拦截对字符串、数字或布尔值等原生类型执行的操作。由于这些类型不是对象,对它们的操作无法被 Proxy 拦截。例如,您无法使用 Proxy 直接拦截字符串比较或转换。这一局限会影响对涉及原始值的操作进行监控和校验的能力。
let proxy = new Proxy('example string', handler); // This will throw an error
  1. 无法监控某些内置对象操作:Proxy 无法拦截内置对象上的某些操作,例如直接修改数组的长度或在函数上设置属性。虽然您可以拦截方法调用和属性访问,但对不会触发 setter/getter 访问的内部属性的修改超出了 Proxy 的能力范围。
let numbers = new Proxy([1, 2, 3], handler);

numbers.length = 2; // This operation cannot be intercepted directly
  1. 对某些内置函数不透明:当参数是 Proxy 对象时,某些 JavaScript 内置函数和方法的行为可能有所不同。例如,即使代理封装的是一个数组,Array.isArray(proxyObject) 也会返回 false。这种不透明的行为可能会让依赖类型检查或原生行为的代码产生意想不到的结果。

利用 Proxy 对象插桩检测 PostMessage XSS

消息画像

在模糊测试之前加入画像阶段,可以提升识别 JavaScript 应用中漏洞的有效性和效率,尤其是针对 postMessage 处理器和存储访问模式。

画像步骤旨在深入了解应用的行为,重点捕获 postMessage 处理器和消息,以及与 Web 存储(如 localStorage 和 sessionStorage)的交互。这些信息有两个主要用途:

  • 为模糊测试重新定位目标:通过收集有关应用交互和数据流的详细信息,可以定制模糊测试输入,更有效地覆盖更多代码路径,从而发现原本可能隐藏的漏洞。
  • 提升性能:画像可以识别哪些输入是相关的、被应用实际使用的,使后续的模糊测试阶段能够聚焦于这些区域。这种有针对性的方法避免在无关输入上浪费时间,从而最大限度地提升模糊测试过程的效率。

为了捕获 postMessage 处理器,我们重写 addEventListener 方法,记录所有被注册的事件监听器。这对于识别可能成为模糊测试目标的消息处理器尤其有用。

// Object to store event listeners
const registeredEventListeners = [];


// Save the original addEventListener method
const originalAddEventListener = EventTarget.prototype.addEventListener;


// Override the addEventListener method
EventTarget.prototype.addEventListener = function(type, listener, options) {
   // Store the event details
   registeredEventListeners.push({ element: this, type, listener, options });


   // Call the original addEventListener method
   originalAddEventListener.call(this, type, listener, options);
};

这段代码将每个已注册的事件监听器存储在一个数组中,便于之后审查和分析这些事件监听器,尤其是与 message 事件相关的监听器。

为了监控和记录 postMessage 活动,我们添加了一个自定义处理器来记录被发送的消息,出于效率和简便考虑使用控制台输出。

function handleMessage(event) {
   console.error("magic_post_message", JSON.stringify({data: event.data, origin: event.origin}));
}

window.addEventListener('message', handleMessage, false);

该处理器会捕获传入的消息并记录其内容和源,从而为了解应用内如何使用 postMessage 提供有价值的洞察,并突出在模糊测试中值得进一步调查的潜在区域。

画像还会通过使用 Proxy 记录访问,追踪应用如何与对象交互,包括嵌套属性访问:

/**
* This function creates a proxy around the provided obj that tracks access to its properties. If a property of the object
* (or a nested object) is accessed, the accessHandler function is called with the object, the property name, and the path
* to the property.
*/
function createAccessTrackingProxy(obj, accessHandler) {
   // A recursive function to create a proxy for an object and its nested objects
   const createProxy = (target, path) => {
       return new Proxy(target, {
           get(target, property, receiver) {
               // Trigger the access handler function
               accessHandler(obj, property, path);


               // Check if the property accessed is an object and not null for recursion
               if (target[property] !== null && typeof target[property] === 'object') {
                   return createProxy(target[property], path.concat(property));
               }


               // Return the actual property value
               const value = Reflect.get(target, property, receiver);
               if (isObject(value)) {
                   return createProxy(value, path.concat(property));
               } else {
                   if (coinFlip()) {
                       return createProxy({}, path.concat(property));
                   } else {
                       return value;
                   }
               }


           }
       });
   };


   return createProxy(obj, []);
}


/**
* Logs path of object accesses
*/
function accessHandler(obj, property, path) {
   try {
       console.log('magic_post_message', JSON.stringify({
           data: obj,
           origin: null,
           path: [...path, property],
       }));
   } catch (e) {
       // Pass.
   }
}

这种方法会在对象(或嵌套对象)外围创建一个 Proxy,并记录对其属性的每一次访问。每当访问某个属性时,都会调用 accessHandler 函数,记录被访问属性的路径。这种细致的追踪有助于理解应用如何与其数据交互,从而进一步为模糊测试过程提供依据。

在此处使用“抛硬币”是一种为画像阶段引入随机性的简单方式:通过动态决定返回实际值还是指向新对象的代理,有可能发现隐藏的路径和行为。

PostMessage 动态模糊测试

模糊测试步骤会利用已发现的访问路径,丰富在画像阶段收集到的消息语料库。这一扩充过程是发现更复杂漏洞的关键,它确保模糊测试能够覆盖 JavaScript 应用中更广泛的代码路径。扩充之后,消息集会被精简,以去除冗余或无关的输入,从而优化模糊测试过程的效率。

Ostorlab 采用了一个能够理解嵌套编码方案的插入点生成器。这一能力对于测试处理复杂对象的应用至关重要,例如 SAML 消息,它可能涉及包含 base64、XML、HTTP 查询和 JSON 编码的嵌套结构。

下面的代码展示了模糊测试工具如何遍历收集到的 postMessage 事件,根据收集到的访问路径动态构造模糊测试消息,并生成插入点来探测应用:

if request.profile is not None:
    post_message_events = request.profile.post_messages or []
    for post_message_event in post_message_events:
        post_message = post_message_event["data"]

        # Traverse the message structure based on collected paths
        post_message_pointer = post_message
        for elm in post_message_event.get("path", []):
            if elm not in post_message_pointer:
                post_message_pointer[elm] = {}
            post_message_pointer = post_message_pointer[elm]

        # Generate and yield fuzzed messages
        for generated_insertions in self.generate(post_message):
            insertions = [WhatInsertionPoints.POST_MESSAGE, post_message]
            insertions.extend(generated_insertions)
            yield insertions

        # Repeat the process for JSON-encoded messages
        for generated_insertions in self.generate(json.dumps(post_message)):
            insertions = [WhatInsertionPoints.POST_MESSAGE, json.dumps(post_message)]
            insertions.extend(generated_insertions)
            yield insertions

当由模糊测试消息触发的 JavaScript 方法回调执行了恶意代码时,即检测到跨站脚本(XSS)漏洞。Ostorlab 通过使用 console.trace 在每次触发 XSS 漏洞时收集堆栈跟踪,从而增强检测过程。这种方法能够精确识别导致漏洞的执行路径,便于进行更深入的分析和更有效的修复。

结论

事实证明,将画像、Proxy 对象插桩与传统动态模糊测试相结合,能够有效检测此前未被发现的 PostMessage XSS 漏洞。

然而,字符串比较等挑战表明仍需进一步改进。我们的下一步是引入静态分析来检测字段比较,以确保实现更好的覆盖率。

标签:

xss, instrumentation