原文:《Exploiting Windows 10 in a Local Network with WPAD/PAC and JScript》
译者: c1tas,de4dcr0w @ 360CERT
译者公众号:https://mp.weixin.qq.com/s/bEWXDfZE5dWtt6EyddVWJw

0x00 aPAColypse now: Exploiting Windows 10 in a Local Network with WPAD/PAC and JScript by Ivan Fratric, Thomas Dullien, James Forshaw and Steven Vittitoe

by Ivan Fratric, Thomas Dullien, James Forshaw and Steven Vittitoe

0x01 Intro

许多广泛部署的技术在经过 20/20 hindsight(判断方式) 之后发现都是一些奇怪的或不必要 的冒险想法

在 IT 里的工程决定往往在不完善的信息和时间压力下完成, 一些古怪的 IT 设置可能被解 释为" 在这时候这看起来是个好的方案", 在这篇文章的作者中, 一些人发现 WPAD("Web Proxy Auto Discovery Protocol") 更多被称为 ("Proxy Auto-Config"), 就是一个奇怪的点

在互联网的早期,在 1996 之前,Netscape 的工程师们决定 JavaScript 是一种很好的语言 来编写配置文件。其结果是 PAC-配置文件格式,其工作方式如下:浏览器连接到预先配置的 服务器,下载 PAC 文件,并执行特定的 JavaScript 函数以确定适当的代理配置。为什么不呢? 它肯定比 XML 更具表达性和更少的细节,似乎是向许多客户提供配置的合理方式。

PAC 本身加上一个叫做 WPAD-一个协议,使浏览器不必有一个预配置的服务器用来连接 协议。相反,WPAD 允许计算机查询本地网络来确定从哪个服务器下载的 PAC 文件。 不知怎么的,这项技术最终成为了一个 1999 到期的 IETF 的草案,现在,在 2017,每台 Windows 机器都会问本地网络:“嘿,我在哪里可以找到一个 JavaScript 文件来执行?“。这可 以通过多种机制来实现:DNS、WINS,但也许最有趣的是 DHCP。

近年来,浏览器的开发已经从最初的 DOM 转向直接针对 JavaScript 引擎,所以仅仅提到 我们可以在没有浏览器的情况下在网络上执行 JavaScript 是一种激励。初步调查显示 JS 引擎 负责执行这些配置文件是 jscript.dll-遗留的 JS 引擎,也带动了 IE7 和 IE8(和仍然可在 IE11 在 IE7 / 8 兼容模式如果使用适当的脚本属性)。这有好有坏——一方面,它意味着不是每一个 Chakra bug 都是自动的本地网络远程攻击,但另一方面,它意味着一些相当旧的代码将负责执 行我们的 JavaScript。

早先, 安全研究人员就警告过 WPAD 的危险性, 但这貌似我们知道的第一例, 证明利用 WPAD 进攻击是可以危害到用户机的 。

windows 不是唯一在使用 WPAD 的软体, 其他的操作系统和软件同样有在使用.Google Chrome 就同样具有 WPAD 解释器, 但是 Chrome 会把 PAC 文件中的 JS 代码放在沙箱中运 行, 但其他的操作系统就没有这么做, 这就是 windows 为何是这种攻击最有兴趣的目标。

0x02 Web Proxy Auto-Discovery

1.Intro

如上所述,WPAD 将查询 DHCP 和 DNS(按此顺序)以获取要连接的 URL-如果没有来 自 DNS 的响应,则显然 LLMNR 和 Netbios 也可以使用。WPAD-over-DNS 的一些特性使得 中间人攻击能够出人意料地发挥作用。

2.Attack scenario: Local network via DHCP

在最常见的场景中,机器将使用 option code 252 查询本地 DHCP 服务器。DHCP 服务器 返回一个字符串如http://server.domain/proxyconfig.pac,指定一个 URL 的配置文件应该被加 载。然后,客户端继续获取该文件,并将内容作为 JavaScript 执行。 在本地网络中,攻击者可以简单地模仿 DHCP 服务器 - 可以通过 ARP 欺骗或通过合法的 DHCP 进行竞争。然后,攻击者可以提供一个恶意 JavaScript 文件所在的 URL。

3.Attack scenario: Remote over the internet via privileged position and DNS

除了当地的网络攻击的情况下,事实上,查找 WPAD 这一请求也可能通过 DNS 查询发生 的这一情况, 也就产生了更多攻击场景。许多用户配置他们的计算机执行 DNS 查找某个公共 DNS,全局可见的 DNS 服务器(如 8.8.8.8、8.8.4.4,地址 208.67.222.222 和 208.67.220.220)。 在这样的情况下,机器会发出 DNS 查询(如 wpad.local),位于本地网络服务器之外。攻击者 在网络上的特权位置(例如网关或任何其他上行主机)可以监视 DNS 查询并欺骗应答,指示 客户端下载并执行恶意 JavaScript 文件。 像这样的设置似乎很常见-根据这一维基百科条目,DNS 根服务器所看到的流量中的一大 部分是本地请求。

4.Attack scenario: Remote over the internet via malicious wpad_tld

WPAD 的特殊之处在于递归地遍历本地机器名称以查找要查询的域。如果一台机器被称为 “laptop01.us.division.company.com”,则按照以下方式查询以下域名:

  • wpad.us.division.company.com

  • wpad.division.company.com

  • wpad.company.com

  • wpad.com

这(根据这个维基百科条目)过去导致人们注册 wpad.co.uk 并将流量重定向到在线拍卖 网站。进一步引用该条目: 通过 WPAD 文件,攻击者可以将用户的浏览器指向自己的代理,拦截并修改所有的 WWW 流量。尽管在 2005 年对 Windows WPAD 处理进行了简单的修复,但它只解决了.com 域的问题。在 Kiwicon 的一个演示显示,世界其他地区仍然严重受到这个安全漏洞的威胁, 在 新西兰注册了一个样本域名,用于测试目的,接收来自全国各地的代理请求,速度为几秒钟。 几个 wpad.tld 域名(包括 COM,NET,ORG 和 US)现在指向客户端回送环回地址,以帮助 防止此漏洞,尽管某些名称仍然被注册(wpad.co.uk)。

因此,管理员应该确保用户可以信任组织中的所有 DHCP 服务器,并且保证组织的所有 可能的 WPAD 域都受到控制。此外,如果没有为组织配置 wpad 域,用户将转到域分层结构 中具有下一个 wpad 站点的任何外部位置,并将其用于其配置。这允许只要注册了特定国家的 wpad 子域名,通过设置自己作为所有流量或感兴趣的站点的代理,就可以对该国的大部分互 联网流量进行中间人攻击。

另一方面,IETF 草案明确要求客户只允许“规范”(例如非顶级域名)。我们还没有调查是 否有人在什么程度上实施这个攻击,或者二级域名(如.co.uk)是否是流量重定向的历史案例 的罪魁祸首。 无论哪种方式, 如果一个管理员注册 wpad.$TLD 给定组织的 TLD, 前提是该 TLD 没有被 客户端实施明确列入黑名单, 那么在 JavaScript 引擎的漏洞就可以被通过互联网远程利用. 鉴于 1999 年的 IETF 草案提到了 1994 年的 TLD 列表(RFC1591),客户不太可能已经更新以反映 新 TLD 的扩散。

我们尝试为各种 TLD 注册 wpad.co.$TLD 尚未成功。

0x03 Bugs

我们花了一些时间在寻找jscript.dll, 采用手工分析和 fuzzing,JScript最初的挑战是许许多 多的"feature" 能在 javascript engines 触发 bugs 的 tips 无法在其工作, 应为它太陈旧而不支持 它们. 举个例子

  • 没有多种的 array tpyes(int array, float array etc.). 因此混淆一个数组类型是不可能的

  • 没有更多的优化例如 ("fast paths"), 在更新的更快的 Javascript engines 中这些 fast path 的优化常常是 bug 的来源

  • 不能对一个通常的 JavaScript 对象定义 getter/setter. 只在 dom 对象中可以调用的定义 的属性, 但在 WPAD 进程中没有 DOM. 并且 DOM 对象中大量的 JScript 函数将 fail 并伴随一个消息"JScript object expected"

  • 只要在一个对象被创建后, 不允许修改他的属性 (i.e. 这里没有proto属性)

然而,JScript 会遭受更多"old school" 的漏洞, 比如 use-after-free. JScript 的垃圾收集器 (a post).Jscript 使用了非世代 (no-generational) 的标记和清理垃圾收集器. 从本质上讲, 无论 何时垃圾收集被触发, 它都会标记所有的 JScript 对象. 然后从一组"root" 对象 (有时候被称 为"scavengers"(清道夫)) 开始扫描他们, 并从所有的对象中清除标记. 所有仍被标记的对象都将 被删除. 一个经常出现的问题是, 默认情况下, 堆栈上的本地变量不会添加到根对象列表中, 这 意味着程序员需要记住将他们添加到垃圾收集器的根列表, 特别是这些变量引用对象, 在函数的 生命周期中可以被删除的时候.

另一种可能的漏洞类型包括 buffer overflows(缓冲区溢出),uninitialized variables 等等 Fuzzing, 我们使用了基于语法的Domato fuzzing 引擎, 并且写了一套新的 JScript 语法. 我 们把发现的有趣的内置的属性和函数添加到这个语法中, 通过观察不同 JScript 对象的EnsureB uildin方法. 被添加的 JScript 语法在 Doamto repository(here)

在 fuzzing 和手工测试中我们发现了 7 个安全漏洞, 整理如下表

在发布这篇文章的时候, 所有的 bugs 已经被修复了 这个表格通过漏洞触发类别, 和所需要的兼容模式来区分.JScript 在 WPAD 中相当于在 IE 兼容模式运行脚本, 这意味我们虽然发现了 7 个漏洞, 但实际只有 5 个引发 WPAD, 然而其 他漏洞任然可以被用在 IE(包括 IE11) 投入 IE8 兼容模式的恶意网页。

0x04 Exploit

1、 Understanding JScript VARs and Strings

在这篇文章中的接下来的部分,我们将讨论JScript VARs和Strings, 在讨论漏洞工作方式之前,先讲述这些内容是十分有用的

JScript VAR是一个24-byte(64位)结构,这表示一个JavaScript变量,本质上是和VARIANT的数据结构相同的(MSDN described).

在多数情况下(足以跟踪漏洞)内存布局如下所示

例如,我们可以用一个VAR表示一个双精度数字,它在前2个字节中是5(表示double类型),后跟一个实际的双值,在偏移量为8。最后8个字节将没有使用,但如果从这个VAR.复制另一个VAR的值,它们将被复制

一个JScript的String 是VAR的一种类型在标志位为8,并且指针偏移量是8.这个指针指向一个BSTR结构体(described here).在64位的情况下BSTR的结构如下

一个String VAR 指针直接指向字符数组,这意味着,获取一个字符串的长度,指针需要-4从那个位置读取长度.注意,BSTRs被OleAut32.dll的处理,被分配在独特的堆(i.e. 意思就是其他的Jscript的对象被分配在其他的堆)

释放BSTR也不同于大多数对象,在释放的时候,不再是直接释放一个BSTR,当SysFreeSting被调用的时候,它首先会想一个字符串放在被OleAut32.dll控制的cache中.这个机制在Javascript Heap 风水

2、Stage 1:Infoleak

infoleak 的目的在于获取一个完全由我们控制的string在内存的地址,我们在这个点上没获得任何可执行模块的地址,但接下来会.然而,我们的目标是击破hig-entropy heap randomization(高熵堆随机化)并且让第二阶段的exploit可靠,而不需要是用堆喷

因此我们主要在RegExp.lastParen中使用这个漏洞.为了理解这个bug,我们先来仔细看看jscript!RegExpFncObj的内存布局,以及他对应的 JScript RegExp 对象.在偏移OxAC处, RegExpFncObj包含20个整数的缓冲区.实际上这是10对整数:每一对的第一个元素的输入字符串的start index(开始索引),第二个元素是end index(结束索引).只要RegExp.test,RegExp.exec或带有RegExp参数的String.search在匹配正则语法-组(group)的时候(RegExp中的圆括号),匹配的开始和结束索引就会存储在这里.显然的缓冲区中只有10对空间,所以只有前10个匹配项会被存在这个缓冲区中

但是,如果RegExp.lastParen被调用,并且有超过10个的捕获组的时候,RegExpFncObj::LastParen会很高兴的使用捕获组的数量作为缓冲区的索引,从而导致越界读取

这是一个PoC:

var r= new RegExp ( Array (100) .join ('() ') );
''.search (r);
alert ( RegExp.lastParen );

这两项指标(我们称之为start_index, end_index)读取缓冲区边界之外,并且可以达到任意大的区域.假设这第一个越界访问不会造成崩溃,如果这些索引中的值大于输入字符串的长度,那将发生第二次越界读取,这将允许我们读取输入字符串边界之外的内容.像这样的字符串越界读取,将字符串内容返回到字符串变量中的可以被检测到的调用处.

这第二次越界读将会是我们所使用的,但首先我们需要去明白如何获得start_index, end_index的控制权.幸运的是,在RegExpFncObj的布局中,在index缓冲区后的有一段数据我们可以控制:RegExp.input 的值.通过设置RegExp.input为一个整数值,并且使用一个由41组空括号组成的RegExp(正则表达式),当RegExp.lastParen被调用的时候,start_index将会变成0,然后end_indx的值将会是我们写到RegExp.input的值

如果我们将一个输入字符串和一个释放字符串相邻,然后通过越界读取输入字符串的边界外内容,我们就可以获得堆的metadata,比如指向其他空闲堆的指针(堆块的红黑树的左,右和父节点参阅window10堆内部信息获得更多的信息),Image1 展示了相应对象在infoleak时的情况。

Image 1: Heap infoleak layout

我们将使用20000 bytes-long的字符串作输入,为了不被分配在低碎片堆(Low Fragmentation Heap)(LFH只能被用来16k字节或更小的分配).因为LFH的堆metadata是不同的,在win10的Segment堆中不包含有用的指针.此外,LFH还引入了随机性,这会影响到我们把输入字符串被放在释放字符串旁边的构想

通过从返回的字符串中读取堆的metadata,我们可以获得一个释放字符串的地址,那么我们分配一个与释放字符串相同大小的字符串,它可能被分配在这个地址,我们实现了我们的目标,那就是我们知道了一个由我们控制的字符串的地址

这整个infoleak的过程大概是:

  1. 分配 1000 10000-character的String(提示: 10000 characters == 20000 bytes)

  2. 释放每第二个(Free every second one)

  3. 触发infoleak bug,使用剩余的字符串之一作为输入字符串并越界读取20080个bytes(字节)

  4. 分析泄露的字符串并获得指向其中一个释放字符串的指针

  5. 使用特定内容分配与释放的字符串(10000个 characters)长度相同的500个字符串

特定内容的字符串在这个步骤还不重要,但在下一阶段将变得十分重要,因此会在下一个阶段描述它.还注意,通过检查堆metadata,我们可以很容易的确定进程正在使用的堆(segment 堆和NT堆)

Image2和Image3展示了使用Heap History Viewer 在infoleak时生成的堆可视化的图像

绿色的条纹表示分配块(被字符串占用),灰色的条纹标识分配后,但是被释放的后再分配的块,(指向的是我们释放后,又触发infoleak后被重新分配到(the stings we free and then reallocate after triggering the infoleak bug)),白色的条纹是代表从未被分配的数据(保护页).你可以看到随着时间的推移字符串是如何被分配的,然后一半被释放的(灰色的),一段时间后又被分配(条纹变成绿色)

我们可以看到,每3个这样大小的分配后就会有一个保护页.我们的exploit是从来没有想过去接触这些保护页的(这发生在读取字符串尾少量的数据(it reads too little data past the end of the string for that to occur))但有1/3的几率,infoleak输入的字符串后面讲不会有空字符串,所以预期的堆metadata将会丢失.但是我们可以很容易的检测到这种情况,并且使用另一个输入字符串触发infoleak错误,或者终止exploit(注意:到目前为止我们没有触发任何内存损坏)

Image 2: Heap Diagram: Showing the evolution of the heap over time

Image 3: Step-by-step illustration of leaking a pointer to a string.

3、Stage2: overflow

在exploit的第二个阶段,我们使用this heap overflow bug 在Array.sort中.如果驶入的数组中元素数量大于Array.length/2,JsArrayStringHeapSort(如果未指定比较函数则被Array.sort调用)就会分配一个与当前数组元素数量相同大小的临时的缓冲区(注意:可比array.length小).然后尝试从0到Array.length的每一个数组引索检索相应的元素,如果该元素存在,则将其添加到缓冲区,并转化为字符串.如果数组在JSArrayStringHeapSort的生命周期中没有改变,这将工作正常.但是,JsArrayStringHeapSort将数组元素转化为Strings将触发toString()回调.如果在其中一个toString()回调元素被添加到之前未定义的数组中时,将发生溢出.

为了更好的理解这个bug以及其可利用性,我们来仔细看看我们将会溢出的缓冲区结构.已经提到,数组的大小和当前正在输入数组中的元素数量相同(确切的说,这将是元素数量+1)数组的每个元素都是48字节(64位)结构如下

在JsArrayStringHeapSort期间,检索索引为 <array.length> 的数组的每个元素,并且如果元素被定义,这会发生以下情况

  1. 这个数组元素将被读到偏移16的VAR中

  2. 原始的VAR将被转换为一个String VAR.指向这个String VAR的指针被写到偏移0

  3. 在偏移8,这个元素的index将被写入

  4. 取决于原始VAR的类型,0或1将被写入偏移40

看到临时缓存区的buffer,我们不能直接控制大部分.如果一个数组成员是一个字符串,那么在它偏移量为0和24时,我们将会有一个指针,当他被解引用的时候,我们将在偏移8处包含另一个指向我们控制数据的指针.然而,这是一个间接的在大多数情况下对我们有用的大层次

但是,如果数组的成员是双精度,则在偏移量24(对应原始VAR中的偏移量8)中,该数值将被写入,并直接在我们的控制之下.

如果我们使用与阶段1中获得指针相同的双重表示形式创建一个数字,那么可以使我们的溢出在缓存区结束后的某个地方使用指向我们直接控制的内存的指针来覆盖指针

现在问题变成了,我们可以使用这个方法改写这个漏洞,如果我们仔细研究一下对象如何在JScript中工作,那么答案可能就是其中一个

每个对象(具体的说,一个NameList的JScript对象)将有一个指向hash表的指针.这个hash表只是一个指针数组.当一个对象的成员元素被访问的时候,元素名称的hash被计算.然后取消对应于hash表最低位的偏移处指针.这个指针指向一个对象元素的链表,并且这个链表被遍历,直到我们到达一个与请求元素具有相同名字的元素.这将在Image4中显示

Image 4: JScript Object element internals

请注意,当元素名称少于4个字节时,它存储在与VAR(元素值)相同的结构中,否则将会有个指向元素名称的指针.名称长度<=4对于我们来说已经足够,所以我们不需要深入了解这个细节

一个对象的hash表是覆盖的很好的候选者,因为

  1. 我们能通过访问相应的成员对象来控制它的那些元素被解引用.我们用我们不控制的数据覆盖的元素将永远不会被访问

  2. 通过控制相应对象有多少个成员,我们对hash表的控制有限.例如,hash表以1024个字节开始,但如果我们向对象添加多于512个元素,hash表将被重新分配为8192个字节

  3. 通过用指向我们控制的数据的指针,覆盖hash表指针,我们可以在我们控制的数据中创建假的JScript变量,并通过访问相应对象成员来访问它们

要更可靠的覆盖,我们需要执行以下操作:

  1. 分配和释放大小为8192的大量内存块.这就将代开LFH以分配8192大小的堆.这将确保我们溢出的缓冲区,以及我们正在溢出的hash表被分配在LFH上.这一点很重要,因为这意味着附近不会有其他大小的分配来破坏漏洞攻击(因为LFH bucket只能包含特定大小的分配),这反过来将确保我们将高度可靠地完全覆盖我们想要的东西

  2. 创建2000个对象,每个对象包含512个成员.在这种状态下,每个对象都有一个1024字节的hash表,但是向其中一个对象添加一个元素将使其增长到8192个字节

  3. 添加第513个元素到前1000个对象中,这导致1000个8192-byte的hash表被分配

  4. 使用长度为300包含170个元素的数组触发Array.sort.这将分配一个(170 + 1)*48 = 8208bytes.由于LFH的规则,这个对象将被分配在与8192字节hash表相同的LFH bucket中.

  5. 立即在(第一个数组元素的toString()方法中)向第二个1000个对象添加第513个元素.这使得我们非常确定,现在排序缓冲区是相邻hash表之一.在相同的toString()方法中,还会向数组中添加更多的元素,这将使它超出边界.

Image5 显示了排序缓存区地址周围的堆可视化(红线).你可以看到排序缓存区被大小相近的分配所包围,这些分配都与hash表对应.你也可以观测到LFH随机性,因为随后分配的不一定在随后的地址上,然而这对我们的利用没有任何影响

Image 5: Heap visualization around the overflow buffer

正如前面提到的,我们以这样一种方式制作了我们的溢出:一个不幸的JScript对象的hash表指针将会被指向我们控制的数据指针覆盖.现在我们把这些数据放在什么地方:我们制作了一个包含5个(假)JavaScript变量的方法:

  1. 变量1 只包含数字1337.

  2. 变量2 是特殊类型的0x400c.这个类型基本上告诉Javascript,实际的VAR是有偏移量为8的指针指向的,在读取或写入这个变量之前,这个指针应该被解引用.在我们的例子中,这个指针指向变量1之前的16个字节,这基本上意味着变量2的最后8字节的qword和变量1的第一个8字节的qword重叠

  3. 变量3, 变量4和变量5是简单的整数.它们的特殊之处在于它们分别在最后8字节包含数字5,8,0x400c.

Image6显示了溢出之后被破坏的对象状态

Image 6: State of objects after the overflow. Red areas indicate where the overflow occurred. Each box in the bottom row (except those marked as ‘...’) corresponds to 8 bytes. Data contained in ‘...’ boxes is omitted for clarity

我们可以访问变量1, 只需要访问正确引索处的已破坏对象(我们之前称为Index1),对于变量2-5也是如此.实际上,我们可以通过访问所有对象的Index1来检测哪个对象被破坏,并查看哪个对象有现在的特征值1337

重叠变量1,变量2的作用是可以将变量1的类型(第一个word)改为5(double),8(字符串),或0x400c(指针).我们通过读取变量2,3或4然后将读取的值写入变量2来实现.例如语句:

corruptedobject [ index 2] = corruptedobject [ index 4];

具有将变量1的类型更改为字符串(8)的效果,而变量1的所有其他字段将保持不变

这种布局给了我们几个非常强大的开发原语:

  • 如果我们将一个包含指针的变量写入变量1,我们可以通过将变量1的类型改为double(5)并将其读取出来公开该指针的值

  • 我们可以通过在改地址上伪造一个字符串来在任意地址上公开(读取)内存.我们可以通过首先将与我们想要读取的地址对应double的值写入变量1,然后将变量1的类型更改为Stirng(8)来完成此操作

  • 首先将对应于地址的数值写入变量1,然后将变量1的类型更改为0x400c(指针),最后将一些数据写入变量1,从而写入任意地址.

有了这些利用源语,通常获得代码执行将非常简单,但由于我们正在利用win10,我们首先要绕过Control Flow Guard(CFG).

4、 Stage 3: bypass CFG

我们可以使用一些已知的绕过方式,但是如果攻击者有读写的权限就可以针对jscript.dll进行方便绕过。我们想要达到以下的效果:

  1. 返回地址没有受CFG保护

  2. 一些Jscript对象已经指向原始的栈

特别地,每一个NameTbl 对象(在Jscript中,所有的JavaScript对象都从NameTbl继承),在偏移24的位置保存一个指向CSession对象的指针。CSession对象偏移80的位置保存着指向靠近原始栈顶的指针。

所以,如果可以任意读,通过任何Jscript对象的一连串指针,有可能获得指向原始栈的指针。如果可以任意写,就有可能绕过CFG来覆盖返回地址。

5、Stage 4: Getting code execution as Local Service

准备好所有攻击的条件,我们现在可以实现代码执行。具体步骤如下:

  1. 从任一JScript对象的虚表中读取jscript.dll的地址

  2. 通过读取jscript.dll的导入表获取kernel32.dll的地址

  3. 通过读取kernel32.dll的导入表获取kernelbase.dll的地址

  4. 从kernel32.dll中搜索我们需要的rop gadget

  5. 从kernel32.dll的导出表中获取WinExec函数的地址

  6. 根据上文的描述泄露栈地址

  7. 准备好ROP链并写入栈中,从最接近泄露的栈地址的一个返回地址开始执行rop

我们需要的rop链大致如下:

[ address of RET ] // needed to align the stack to 16 bytes
[ address of POP RCX ; RET ] // loads the first parameter into rcx
[ address of command to execute ]
[ address of POP RDX ; RET ] // loads the second parameter into rdx
1
[ address of WinExec ]

通过执行rop链我们可以调用WinExec函数调用指定的命令。例如:如果我们运行‘cmd’命令,将产生一个命令提示符,和WPAD服务运行用户相同的权限。

不幸的是,在子进程起的本地服务无法连接网络,但是我们可以将提权代码从内存落地到可写可执行的本地磁盘中。

6、Stage 5: Privilege escalation

当本地服务用户是一个service用户时,并没有管理员权限。这意味着exploit在进行系统的访问和修改操作时有很大的局限性,特别是攻击过后或者系统重启后的持久化。但是Windows上总是有可能找到未修补的提权漏洞,所以我们不需要找一个新的提权漏洞。我们可以滥用一个built-in功能实现从本地服务权限到SYSTEM权限。下图是WPAD中service用户被赋予的权限:

Image 7: Service Access Token’s Privileges showing Impersonate Privilege

我们只能得到以上三个权限,但是图中高亮的SeImpersonatePrivilege很重要。该权限允许此服务获得本地系统其他用户的权限。Impersonate权限表明该服务接受本地系统其他用户的请求,可能要代表这些用户执行一些操作。然而只要我们获得这些用户的访问令牌,就可以获得这些用户的全部权限,包括SYSTEM用户,这使得我们在本地系统上拥有了管理员权限。

滥用Impersonate权限是Windows安全模式(你可以通过查找Token Kidnapping获取更多细节)下一个已知的问题。微软已经尝试让它更难获得其他用户的访问令牌,但是不可能避免所有可能的情况。例如,James发现了一个Windows DCOM实现的一个漏洞。该漏洞允许任意用户获得SYSTEM用户访问令牌。微软修复直接提权的漏洞,但是没有或者很难修复token kidnapping问题。我们可以滥用这个功能来获取SYSTEM权限,然后完全控制该系统,比如安装一个提权服务。

这里有一个通过DCOM实现的token kidnapping(RottenPotato),但是这个实现是被设计用于Metasploit框架中 getsystem 命令的,我们无法使用。所以我们用C++实现了更简易的版本,通过 CreateProcessWithToken API直接产生带有SYSTEM 令牌的随机进程。我们将它编译成11KB大小的可执行程序,比RottenPotato小的多,方便落地到本地,执行rop链。

7、Tying it all together

当WPAD服务请求PAC文件时,我们将运行恶意文件来攻击WPAD服务,运行WinExec来落地和运行提权程序。该程序会在SYSTEM权限下执行命令(我们例子中是‘cmd’的硬编码)。

该exploit在我们的实验环境中运行得十分稳定,但是100%的稳定性并不需要,因为如果该exploit使WPAD服务崩溃了,一旦用户发起另一个WPAD服务请求,就会产生新的进程,所以攻击者可以一直尝试。如果用户没有关闭Window 错误报告,将不会有图形化的页面显示WPAD服务崩溃了。如果开启,将会捕捉到这个崩溃,然后报告给微软。

事实上,我们的exploit一旦运行它的payload就会使WPAD服务崩溃,所以如果我们在服务被攻击后继续保持恶意的PAC文件,将会再次攻击服务。你可以在图片7中看到该效果,在攻击服务后的几分钟里,在受害者机器上产生了大量的HTTP请求。

Image 8: Did we leave the exploit running for too long?

我们将很快在 issue tracker 上公布我们的exploit源码

0x05 Conclusion

执行不信任的JavaScript 代码是危险的,在一个没有沙箱的进程里执行更是如此。即使是在像jscript.dll这样相对安全的JavaScript引擎中也是这样。我们披露了jscript.dll 7个安全问题,成功证明了可以从本地网络(或者之外)达到稳定地代码执行。即使是在Windows 10 64-bit with Fall Creators Update版本的全补丁(撰写本文之前)也可以实现。

尽管现在bug已经被修复了,是否意味我们就可以完成任务,安枕无忧了?事实不然,尽管我们花了相当多的时间,精力和电力成本来发现jscript.dll漏洞,但是我们并没有表明我们找到了所有漏洞。事实上,发现了7个漏洞,就很有很可能发现第8个。所以一些事情不改变,早晚有一天我们很可能会在野外看到这些利用。(这还是乐观地假设攻击者没有这种能力。)

那么,微软能做什么来使未来像这类攻击变的艰难:

  1. 默认关闭WPAD,事实上,虽然其他的操作系统也支持WPAD,但是Windows是唯一默认开启的。

  2. 沙箱化在WPAD服务中的Jscript解释器。当解释器需要执行一个定义好输入输出的JavaScript函数时,应该直接沙箱化它。考虑到输入输出模型的简易性,如果微软推出一个类似于seccomp-strict,有限制性的沙箱:一些进程不需要高于“接收一些数据”、“执行一些计算”、“返回一些数据”的权限。

如果你想要自己解决这些问题,当前防止这类攻击(除了未知的漏洞)的唯一方法似乎就是完全关闭WinHttpAutoProxySvc服务。由于有其他服务依赖于WPAD,所以在Services 界面里有时不能关闭WinHttpAutoProxySvc服务(“Startup type”可能变灰失效),但是可以通过修改相应的注册表项关闭:将“HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinHttpAutoProxySvc”中的“Start”值从3(手动)改成4(关闭)。

当搜索“disabling WPAD”时,在网上发现一些常见的建议在我们的实验中并不能防范攻击:

  1. 在控制面板关闭“Automatically detect settings”

  2. 设置“WpadOverride”注册表值

  3. hosts文件中写入“255.255.255.255 wpad”(这将会关闭DNS变量,但是不能关闭DHCP变量)


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/541/