使用Semmle QL进行漏洞挖掘:DOM XSS攻击分析

本文详细介绍了如何利用Semmle QL技术挖掘DOM-based XSS漏洞,包括定义源与接收器、处理ReactJS场景及优化查询以减少误报。通过实际案例展示在Outlook Web应用中发现关键安全漏洞的过程。

使用Semmle QL进行漏洞挖掘:DOM XSS攻击分析

在之前的两篇博客文章(第一部分第二部分)中,我们讨论了如何在C和C++代码库中使用Semmle QL来发现整数溢出、路径遍历和导致内存损坏的漏洞。在本文中,我们将探索如何将Semmle QL应用于Web安全,通过寻找最常见的客户端漏洞之一:基于DOM的跨站脚本(XSS)攻击。

您可能已经知道,XSS攻击可以是服务器端或客户端的。大多数Microsoft服务构建在ASP.NET之上,而客户端则大量基于TypeScript和JavaScript。本文将介绍确定目标上的源(输入的起点)和接收器(输入可能到达的位置,可能被利用为漏洞)的过程,以及如何消除误报/不可利用的情况,以减少代码审计的时间投入。我们的目标是Outlook Web(outlook.office.com),它是Office 365的一部分。

注意:在阅读本文之前,建议您阅读本系列的前几部分,以了解一些基本概念,特别是数据流分析和污点跟踪。

定义源

源是应用程序接收用户提供数据的地方。从攻击者的角度来看,有趣的源是那些他们可以在受害者端轻松控制的地方。例如,攻击者不太可能通过仅仅诱骗受害者访问恶意页面来控制HTTP请求中的User-Agent字段,这使得它成为一个不有趣的源。

在初步审查中,我们观察到源可能来自多个地方,例如HTML元素属性、HTTP请求头、API响应、跨源消息通道或URL本身(location.search、location.href、location.hash、location.pathname)等。

缩小源的范围非常困难、昂贵,并且有可能错误地丢失假阴性案例。相反,我们可以通过重写Dataflow库的isSanitizer谓词来评估并随后消除不可利用的路径。

此外,由于我们的数据可能流经一些最终会污染其他节点的地方(第二部分提到),这里将使用污点跟踪来跟踪流。例如,对应于字符串“Hello World”的数据流节点应该污染res变量。

1
var res = "Hello world!".substr(1, 4);

让我们通过以下模型在TaintTracking配置中定义源,使其简单且广泛开放:

1
2
3
4
5
class Cfg extends TaintTracking::Configuration {
  Cfg() { this = "Track data flows to XSS sink" }
  override predicate isSource(DataFlow::Node source) { any() }
  ...
}

定义接收器

由于Web世界的复杂性,XSS接收器非常丰富。通常,我们需要在开始编写查询之前手动审查并获取一些关于目标的知识。一段时间后,我们应该准备好列出一些有希望的接收器来执行我们的分析。在本文中,我们专注于三个最大且众所周知的接收器:Location、Document和ReactJS。

Location接收器

Location接收器是用户浏览器通过各种方式导航到其他地方的地方(见下图)。由于常见的注入javascript: URI方案向量,这些可能容易受到XSS攻击,这会使浏览器执行JavaScript代码。

赋值 方法调用
location = “javascript:alert(document.domain)”
window.location = “javascript:alert(document.domain)”
document.location = “javascript:alert(document.domain)”
location.href = “javascript:alert(document.domain)”
open (“javascript:alert(document.domain)”)
window.open(“javascript:alert(document.domain)”)
location.assign(“javascript:alert(document.domain)”)
location.replace(“javascript:alert(document.domain)”)

注意:与HTML元素和其他相关的接收器不在本文的讨论范围内。

让我们开始吧。

首先,我们寻找一个赋值,其左侧是全局对象location,右侧值是我们感兴趣的接收器节点。location对象引用可以谓词化为DataFlow::globalVarRef(string name),以访问名为name的全局对象。

以下查询揭示了window.location=…和location=…,因为它们都作为全局对象暴露。

1
2
3
4
5
6
7
8
class LocationXSS_Sink extends DataFlow::Node {
  LocationXSS_Sink() {
    exists(Assignment m | m.getLhs() = DataFlow::globalVarRef("location").asExpr() |
      this.asExpr() = m.getRhs()
    )
    ...
  }
}

下一步是找到剩下的两个赋值:document.location=…和location.href=…

显然,这两个都是将值写入对象属性的表达式。DataFlow::SourceNode提供了一个名为getAPropertyWrite(string prop_name)的谓词来帮助我们跟踪写入属性prop_name的所有节点。谓词getAPropertyWrite返回一个DataFlow::PropWrite,因此我们需要定义一个来抓取它,然后让接收器节点成为这个赋值的右侧。

通过添加另一组接收器节点,如以下QL所示,我们能够识别所有在赋值语句中形成的location接收器。

1
2
3
4
5
6
7
exists(DataFlow::PropWrite pw |
      DataFlow::globalVarRef("document").getAPropertyWrite("location") = pw  //document.location = ...
      or
      DataFlow::globalVarRef("location").getAPropertyWrite("href") = pw  //location.href = ...
    |
      this = pw.getRhs()
)

下一步是识别形成函数调用的location接收器。我们获取对名为open的全局对象的数据流节点引用。具体来说,以下QL查询列出了目标为open(…)和window.open(…)的调用。

1
2
import javascript
select DataFlow::globalVarRef("open").getACall()

除了getAPropertyWrite,DataFlow库还提供了名为getAMethodCall的谓词,用于查找SourceNode(GlobalVarRefNode的超类型)上的所有方法调用。以下是一个查询,用于定位对location.assign或location.replace的任何调用。

1
2
3
4
5
6
7
import javascript

from DataFlow::MethodCallNode call
where
  call = DataFlow::globalVarRef("location").getAMethodCall("assign") or
  call = DataFlow::globalVarRef("location").getAMethodCall("replace")
select call

将所有这些放在一起,识别此类接收器的最终查询如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class LocationXSS_Sink extends DataFlow::Node {
  LocationXSS_Sink() {
    exists(DataFlow::CallNode call |
      call = DataFlow::globalVarRef("open").getACall()  // window.open(...) and open(...)
      or
      call = DataFlow::globalVarRef("location").getAMethodCall("assign")
      or
      call = DataFlow::globalVarRef("location").getAMethodCall("replace")
    |
      this = call.getArgument(0)
    )
    or
    exists(Assignment m | m.getLhs() = DataFlow::globalVarRef("location").asExpr() |
      this.asExpr() = m.getRhs()  // this uncovers `location=...` and `window.location=...`
    )
    or
    exists(DataFlow::PropWrite pw |
      DataFlow::globalVarRef("document").getAPropertyWrite("location") = pw  //document.location = ...
      or
      DataFlow::globalVarRef("location").getAPropertyWrite("href") = pw  //location.href = ...
    |
      this = pw.getRhs()
    )
  }
}

Document接收器

与之前类似,让我们将此类接收器分为两种形式,如下所示:

赋值 方法调用
element.innerHTML = “
element.outerHTML = “
document.write("<img src=a onerror=",“alert(1)>”)
document.writeln("<img src=a onerror=",“alert(1)>”)
node.insertAdjacentHTML(“afterend”,"")
jquery_method.html("")

注意:除了.html()之外,还有许多jQuery方法接受HTML字符串。然而,在此代码库中,我没有观察到这些方法的大量使用,并且它们看起来不容易受到攻击。

首先,我们寻找任何将值写入对象的innerHTML或outerHTML属性的数据流节点。这可以通过一个简单的查询完成:

1
2
3
4
import javascript
from DataFlow::PropWrite pw
where pw.getPropertyName().regexpMatch("(innerHTML|outerHTML)")
select pw

在“方法调用”部分,采用与之前接收器类似的方法,我们感兴趣的是全局DOM对象document的write和writeln方法的调用的第一个参数。另一方面,insertAdjacentHTML和html调用的第二个和第一个参数分别是我们正在寻找的接收器节点。

最后,我们根据上述条件定义一个接收器,使用查询:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Document_Sinks extends DataFlow::Node {
  Document_Sinks() {
    exists(DataFlow::MethodCallNode call, int argPos |
      call = DataFlow::globalVarRef("document").getAMethodCall("write")
      or
      call = DataFlow::globalVarRef("document").getAMethodCall("writeln")
      or
      call.getCalleeName() = "insertAdjacentHTML" and argPos = 1
      or
      call.asExpr().(JQueryMethodCall).getCalleeName() = "html" and argPos = 0
    |
      this = call.getArgument(argPos)
    )
  }
}

注意:我们使用内联转换.(JqueryMethodCall),基于javascript库构建,以整齐地削减任何不对应于jQuery方法的调用节点。因为JQueryMethodCall是一个表达式,在进行转换之前,我们需要暴露数据流节点call的表达式。另外,因为write和writeln可以接受多个参数作为HTML字符串,我们让argPos未指定用于document方法,以使getArgument捕获所有它们。

ReactJS XSS接收器

在OWA代码库中,开发人员还采用了ReactJS,这是一种快速且方便的构建用户界面的方式。因此,对于这种情况,还有另一个接收器需要处理。

dangerouslySetInnerHTML是一个属性,让开发人员在渲染(称为JSX)时将HTML字符串直接推送到React元素。它在代码库中看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export default class HtmlContent extends React.Component<HtmlContentProps, {}> {
...
    render() {
        /* tslint:disable:react-no-dangerous-html */
        return (
            <div
                ref={ref => (this.htmlContentRef = ref)}
                dangerouslySetInnerHTML={{ __html: this.props.html }}
            />
        );
        /* tslint:enable:react-no-dangerous-html */
    }
}

如代码所示,开发人员使用tslint来确保代码质量,并可能避免一些众所周知的问题。这个React组件负责将HTML表达式写入文档,在内容经过仔细清理之后,因为它可能包含不受信任的数据。

现在,让我们回到QL。幸运的是,内置库有一个模块semmle.javascript.JSX,为我们提供了处理JSX代码的类和谓词。例如,以下查询指示了HtmlContent被使用的地方:

1
2
3
4
5
import javascript

from JSXElement jsx
where jsx.getName() = "HtmlContent"
select jsx

此外,JSXAttribute类帮助我们在JSX代码中识别属性/特性,这里的

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