安全模式wpa(在内网环境使用WPAD)
安全模式wpa(在内网环境使用WPAD)在最常见的情况下,一台机器将使用选项码252查询本地DHCP服务器.DHCP服务器回复一个字符串-- 比如“http://server.domain/proxyconfig.pac”,它指定一个配置文件的URL。 然后客户端继续获取这个文件,并将内容作为Javascript执行。如上所述,WPAD将查询DHCP和DNS(按此顺序)以获取要连接的URL -- 如果没有可用的DNS响应,显然LLMNR和Netbios也可以使用。WPAD-over-DNS的一些特性使得攻击向量能够出人意料地发挥作用。近年来,浏览器的攻击已经从主要面向DOM的方向发展到直接面向Javascript引擎,所以只要我们可以在没有浏览器的情况下通过网络来执行Javascript执行就是激动人心的。最初调查显示,JS引擎负责执行这些配置文件是JSCRIPT.DLL - JS引擎也支持IE7和IE8(如果使用适当的脚
引子从20/20事后来看,许多广泛部署的技术似乎是一种奇怪或不必要的冒极大风险的想法。IT工程决策往往是由信息不完善和时间压力造成的,而IT系统中的一些古怪问题最好用“当时看来是个好主意”来解释。在这篇文章的作者个人观点中,WPAD(“WebProxy 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的攻击,结果导致使用WPAD用户机器的完全破坏。
Windows并不是实现WPAD的唯一系统,其他操作系统和应用程序也是如此。例如Google Chrome也有一个WPAD实现,但在Chrome中,评估来自PAC文件的JavaScript代码发生在一个沙箱内。而其他支持WPAD的操作系统默认不启用它。这就是为什么Windows是目前这种攻击最有趣的目标。
Web Proxy Auto-Discovery如上所述,WPAD将查询DHCP和DNS(按此顺序)以获取要连接的URL -- 如果没有可用的DNS响应,显然LLMNR和Netbios也可以使用。WPAD-over-DNS的一些特性使得攻击向量能够出人意料地发挥作用。
攻击场景:通过DHCP的本地网络在最常见的情况下,一台机器将使用选项码252查询本地DHCP服务器.DHCP服务器回复一个字符串-- 比如“http://server.domain/proxyconfig.pac”,它指定一个配置文件的URL。 然后客户端继续获取这个文件,并将内容作为Javascript执行。
在这篇博文发布之前,所有以上的漏洞都已经呗微软所修复。
该表中的漏洞通过触发它们所需的类和兼容模式来触发。WPAD中的JScript相当于在IE7兼容模式下运行脚本,这意味着尽管我们发现了7个漏洞,但WPAD中只能触发5个。但是,其他漏洞仍然可以在Internet ExplorerIE8兼容模式下被恶意网页使用(包括IE11)。
利用理解Jscript的变量和字符串
在这篇博文的其余部分中,我们将讨论JScript VAR和Strings,在深入探讨漏洞的工作方式之前,先描述这些内容是非常有用的。
JScript VAR是一个24字节(在64位版本上)的结构,表示一个JavaScript变量,并且与MSDN文章中描述的VARIANT数据结构基本相同。在大多数情况下(足以跟踪漏洞)其内存布局如下所示:
例如,我们可以通过将5写入前2个字节(表示双重类型)的VAR表示双精度数,后在偏移8跟一个实际的双精度值.最后8个字节将是未使用的,但它们如果从这个VAR复制另一个VAR的值,将被复制。
JScript字符串是一种类型为8,偏移量8处为指针的VAR。指针指向这里描述的BSTR结构。在64位构建BSTR布局看起来像这样:
一个字符串VAR直接指向字符数组,这意味着,要获得一个字符串的长度,指针需要减少4,并从那里读取长度。请注意,BSTR由OleAut32.dll处理,并分配在一个单独的堆上(即与其他JScript对象不同的堆)。
BSTR的释放也不同于大多数对象,并不是直接释放BSTR,而是在调用SysFreeString时,它首先将一个字符串放入由OleAut32.dll控制的缓存中。 这个机制在JavaScript的堆风水(原文HeapFeng Shui)中有详细的介绍。
阶段1:信息泄露infoleak的目的是获得我们完全控制的字符串的内存地址。此时我们不会泄露任何可执行的模块地址,这将在稍后进行。相反,其目标是挫败高熵堆的随机化,使第二阶段的利用可靠,而不必使用堆喷射。
对于infoleak,我们要在RegExp.lastParen中使用这个bug。为了理解这个bug,我们先来仔细看看jscript!RegExpFncObj的内存布局,它对应于JScript RegExp对象。在偏移量0xAC处,RegExpFncObj包含20个整数的缓冲区。实际上这些是10对整数:第一个元素是输入字符串的起始索引,第二个元素是结束索引。只要RegExp.test,RegExp.exec 或带有RegExp参数的String.search遇到捕获组(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的布局,在索引缓冲区结束后就是我们控制的数据:RegExp.input值。通过将RegExp.input设置为整数值并使用由41组空括号组成的RegExp,当调用RegExp.lastParen时,start_index将为0,end_index将为我们写入RegExp.input的任何值。
如果我们把一个输入字符串与一个被释放的字符串相邻,那么通过在输入字符串的边界之后读取,我们可以获得堆的元数据,例如指向其他空闲堆段的指针(在红黑色中的Left,Right和Parent节点堆的树块,请参阅Windows10 Segment堆内部了解更多信息)。图1显示了infoleak时的相关对象。
我们使用20000字节长的字符串作为输入,以便它们不会分配到低碎片堆(LFH只能用于16K字节或更小的分配),因为LFH堆的元数据是不同的,并且不包括Windows 10 Segment堆中有用的指针。此外,LFH引入了随机性,这会干扰我们将输入字符串放在释放字符串旁边的操作。
通过从返回字符串读取堆中的元数据,我们可以获得一个释放字符串的地址。那么,如果我们分配一个与被释放字符串大小相同的字符串,它就可以被放置在这个地址。我们实现了我们的目标,那就是我们知道一个内容由我们控制的字符串的内存地址。
整个infoleak过程如下所示:
分配1000个10000字符的字符串(注意:10000个字符== 20000字节)。
释放2,4,6字符串……
触发信息泄漏错误。使用剩余的字符串之一作为输入字符串并读取20080字节。
分析泄漏的字符串并获取指向一个释放字符串的指针。
分配与释放的字符串(10000个字符)长度相同的特定内容的500个字符串。
特制字符串内容在这个阶段并不重要,但在下一个阶段是重要的,所以在这里不会描述。另外请注意,通过检查堆元数据,我们可以轻松地确定进程正在使用的堆(Segment Heap vs NT Heap)。
图2和图3显示了在infoleak时使用Heap History Viewer创建的堆可视化图像。绿色条纹表示分配的块(被字符串占据),灰色条纹表示分配的块被释放,然后再次被分配(字符串释放并在触发infoleak bug后被重新分配)白色条纹表示从未分配的数据(保护页)。你可以看到随着时间的推移字符串是如何分配的,一半被释放(灰色的),一段时间后又被分配(条纹变成绿色)。
我们可以看到每隔3次分配这个大小后就会有一个保护页。我们的漏洞从来没有实际触及任何这些保护页(它读取的字符串末尾的数据太少,以至于不会发生这种情况),但在这种情况下,在输入字符串之后不会有空闲字符串infoleak,所以预期的堆元数据将会丢失。但是,我们可以很容易地检测到这种情况,并使用另一个输入字符串触发infoleak bug,或者悄悄地中止exploit(注意:到目前为止我们没有触发任何内存损坏)。
Image 2: Heap Diagram: Showing the evolution ofthe heap over time
Image 3: Step-by-step illustration ofleaking a pointer to a string.
阶段2:溢出在攻击的第二阶段,我们将使用Array.sort中的堆溢出bug。如果输入数组中的元素数量大于Array.length / 2,则JsArrayStringHeapSort(如果未指定比较函数,则由Array.sort调用)将分配一个相同大小的临时缓冲区(注意:可以小于array.lenght)。然后尝试从0到Array.length索引检索相应的元素,如果该元素存在,则将其添加到缓冲区并转换为字符串。如果数组在JsArrayStringHeapSort的生命周期中没有改变,这将工作正常。但是,JsArrayStringHeapSort将数组元素转换为可以触发toString()回调的字符串。如果在其中一个toString()回调元素被添加到未定义的数组中,将发生溢出。
为了更好地理解这个bug及其可利用性,我们来仔细看看溢出的缓冲区的结构。已经提到过,数组的大小与当前在输入数组中的元素的数量相同(确切地说,它将是元素的数量 1)。数组中的每个元素的大小都是48字节(64位版本),结构如下:
在JsArrayStringHeapSort期间,index
1.数组元素被读入偏移量为16的VAR
2.原始的VAR被转换成一个字符串VAR。指向字符串VAR的指针写在偏移量0处。
3.在偏移8处,写入数组中当前元素的索引
4.根据原始的VAR类型,在偏移量40写0或1
观察临时缓冲区的结构,我们不能直接控制很多细节。如果一个数组成员是一个字符串,那么在偏移量为0和24时,我们将会有一个指针,当它被取消引用时,偏移量为8的指针包含另一个指向由我们控制的数据的指针。然而,这是一个间接的在大多数情况下对我们有用的大方向。
然而,如果数组的成员是一个双精度数,则在偏移24(对应于偏移量8原始VAR),该数的值将直接被写入,并且是我们的控制之下。如果我们创建具有相同代表double的如在阶段1中获得的指针的数,那么我们可以溢出覆盖指向缓冲区之外的指针为指向我们直接控制内存中。
现在问题变成了,我们该按照这样的方式覆盖什么指针来利用。如果我们仔细研究一下对象如何在JScript中工作那么就会发现一个可能的答案。
每个对象(更具体地说,一个NameList的JScript对象)将有一个指向哈希表的指针。这个哈希表是一个指针数组。当一个对象的成员元素被访问时,先计算元素名称的hash。然后取消对应于哈希最低位偏移处的指针。这个指针指向一个对象元素的链表,然后这个链表被遍历,直到我们到达一个与请求元素具有相同名字的元素。这在图4中显示。
Image 4: JScript 内部对象元素
请注意,当元素的名称少于4个字节时,它将存储在与VAR(元素值)相同的结构中。否则,将会有一个指向元素名称的指针。名称长度<= 4对于我们来说已经足够了,所以我们不需要深入了解这个细节。
一个对象散列表是一个很好的覆盖候选,因为:
我们可以通过访问相应的对象成员来控制哪些元素被取笑引用。我们用不受控制的数据覆盖的元素将永远不会被访问。
通过控制相应对象的成员个数,我们对散列表大小有着有限控制。例如,散列表以1024字节开始,但如果我们向对象添加超过512个元素,散列表将被重新分配为8192个字节。
通过用指向控制数据的指针覆盖散列表指针,我们可以在控制数据中创建假的JScript变量,并通过访问相应的对象成员来访问它们。
要可靠地进行覆盖,我们执行以下操作:
分配和释放大量大小为8192的内存块.这将打开Low Fragmentation Heap使得分配大小为8192。这将确保我们溢出的缓冲区,以及我们溢出的散列表将被分配LFH。这一点很重要,因为这意味着附近不会有其他大小的分配来破坏漏洞攻击(因为LFH只能包含特定大小的分配)。这反过来确保我们将高度可靠地覆盖我们想要的东西。
创建2000个对象,每个对象包含512个成员。在这种状态下,每个对象都有一个1024字节的散列表。但是,向其中一个对象添加一个元素将导致其散列表增长到8192个字节。
将513元素添加到前1000个对象中,导致1000个8192字节哈希表的分配。使用长度为300和170个元素的数组触发Array.sort。这将分配一个大小为(170 1)* 48 = 8208字节的缓冲区。由于LFH的粒度,这个对象将被分配在与8192字节哈希表相同的LFH中。
立即(在第一个数组元素的toString()方法中)向第二个1000对象添加第513个元素。这使得我们非常确定现在排序缓冲区是相邻的哈希表之一。在相同的toString()方法中也向数组中添加更多的元素,这会导致它超出边界。
图5显示了排序缓冲区地址时的堆可视化(红线)。您可以看到排序缓冲区被大小相近的分配所包围,这些分配都与对象哈希表相对应。你也可以观察到LFH的随机性,因为随后的分配不一定在随后的地址上,然而这对我们的利用没有任何影响。
Image5: Heap visualization around the overflow buffer
正如前面提到的,我们以这样一种方式造成了溢出:一个不幸的JScript对象的hashtable指针会被覆盖为指向到我们控制的数据的指针。现在终于轮到我们的数据来起作用:我们以这样一种方式创建一个包含5个(假的)JavaScript变量:
变量1只包含数字1337。
变量2是特殊类型的0x400C。 这个类型基本上告诉JavaScript,实际的VAR是由偏移量为8的指针指向的,在读取或写入这个变量之前,这个指针应该被解除引用。在我们的例子中,这个指针指向变量1之前的16个字节。这基本上意味着变量2的最后8字节的qword和变量1的第一个8字节的qword重叠。
变量3,变量4和变量5是简单的整数。它们的特殊之处在于它们分别在其最后8个字节中包含数字5 8和0x400C。
图6中显示了溢出之后被破坏的对象的状态。
图6:溢出后的对象状态红色区域表示溢出发生的地方。 底行中的每个框(除了那些标记为“...”的)对应于8个字节。 为清楚起见,“...”框中的数据被省略
我们只需访问正确索引处(我们称之为index1)的已损坏对象就可以访问变量1,对于变量2-5也是如此。实际上,我们可以通过访问所有对象的index1来检测哪个对象被破坏,并查看哪个对象现在具有值1337。
重叠变量1和变量2的作用是可以将变量1的类型(第一个WORD)更改为5(double),8(string)或0x400C(pointer)。我们通过读取变量2 3或4然后将读取的值写入变量2来实现。例如,语句
corruptedobject [index2] = corruptedobject[index4];
具有将变量1的类型更改为String(8)的效果,而变量1的所有其他字段将保持不变。
这种布局给了我们几个非常强大的利用权限:
如果我们将一个包含一个指针的变量写入变量1,我们可以通过将变量1的类型改为double(5)并读出它来泄露这个指针的值
我们可以通过在该地址伪造一个字符串来在任意地址上泄露(读取)内存。我们可以通过首先将与我们想要读取的地址对应的double值写入变量1,然后将变量1的类型更改为String(8)来完成此操作。
首先将对应地址的数值写入变量1,然后将变量1的类型更改为0x400C(指针),最后将一些数据写入变量1,从而写入任意地址。
有了这些利用权限,通常获得代码执行将非常简单,但是由于我们正在利用Windows 10,我们首先需要绕过Control Flow Guard(CFG)。
阶段3 绕过CFG这里可能有已经公开的方法,但事实证明,jscript.dll有一些非常方便的绕过方法(一旦攻击者有一个读/写权限)。我们将利用以下事实:
返回地址不受CFG保护
一些Jscript对象有指向本地堆栈的指针
具体来说,每个NameTbl对象(在Jscript中,所有的JavaScript对象都继承自NameTbl),在偏移量为24的位置持有一个指向CSession对象的指针。CSession对象在偏移量为80的地方保存一个指向本地堆栈顶部附近的指针。
因此,通过任意读取,跟随来自任何JScript对象的指针链,可以检索指向本地堆栈的指针。然后,通过任意写入,可以覆盖返回地址,绕过CFG。
阶段4 在本地服务实现任意代码执行利用所有的漏洞元素,我们现在可以开始编写poc。 我们正在这样做:
1.从任何JScript对象的vtable读取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链如下所示:
[addressof RET] //needed to align the stack to16 bytes
[address of POPRCX; RET] //loads the first parameter into rcx
[address ofcommand to execute]
[address of POPRDX; RET] //loads the second parameter into rdx
1
[address ofWinExec]
通过执行这个ROP链,我们用指定的命令调用WinExec。例如,如果我们运行命令“cmd”,我们将看到一个命令提示符被生成,以本地服务运行(运行WPAD服务的相同用户)。
不幸的是,从作为本地服务运行的子进程中,我们不能与网络通信,但是我们可以做的是将提权载荷从内存中放入到磁盘中。本地服务可以从那里写入并执行它。
阶段5:提权虽然本地服务帐户是服务帐户,但它不具有管理权限。这意味着攻击者在系统上可以访问和修改的东西是非常有限的,尤其是在利用之后或系统重启之后维持访问。虽然在Windows中总有可能存在未固定的特权升级,但我们不需要找到新的漏洞来升级我们的特权。相反,我们可以滥用内置功能从本地服务升级到SYSTEM帐户。我们来看看已授予WPAD服务帐户的权限:
Image7: Service Access Token’s Privileges showing Impersonate Privilege
我们只有三个权限,但突出显示的权限SeImpersonatePrivilege非常重要。此权限允许服务模拟本地系统上的其他用户。服务具有模拟特权的原因是它接受来自本地系统上所有用户的请求,可能需要代表他们执行操作。但是,只要我们能够获取我们想要模拟的帐户的访问令牌,我们就可以获得令牌帐户的完全访问权限,包括SYSTEM,这会使我们获得本地系统上的管理员权限。
滥用模拟是Windows安全模型的一个已知问题(您可以通过搜索令牌绑架找到更多详细信息)。微软已经试图让获得特权用户访问令牌变得更加困难,但几乎不可能关闭所有可能门路。例如,James在Windows的DCOM实现中发现了一个漏洞,允许任何用户访问SYSTEM访问令牌。虽然微软修复了直接特权提升漏洞,但是他们没有,也许不能修复令牌绑架问题。我们可以利用这个功能来捕获SYSTEM令牌,冒充令牌,然后完全破坏系统,比如安装特权服务。
有一个通过DCOM(RottenPotato)令牌绑架的现有实现,但是实现被设计为与Metasploit框架的getsystem命令一起使用,但我们并没有使用msf。因此,我们在C 中实现了我们自己的更简单的版本,它使用CreateProcessWithToken API直接生成一个带有SYSTEM标记的任意进程。我们能够将它编译成11KiB的可执行文件,比RottenPotato小得多,这使得它更容易放入磁盘并从ROP载荷运行。
将它们联系在一起
当WPAD服务查询PAC文件时,我们提供利用WPAD服务的漏洞文件并运行WinExec来释放和执行权限提升的二进制文件。这个二进制然后执行一个系统命令(在我们的情况下,硬编码'CMD')。
这个漏洞在我们的实验中非常可靠地工作,但是有趣的是,不需要100%可靠的漏洞 - 如果漏洞使WPAD服务崩溃,当客户端发出另一个来自WPAD的请求时,将会产生一个新的实例服务,所以攻击者可以再试一次。虽然窗口错误报告可能会发生崩溃并将其报告给Microsoft,但用户没有禁用它,UI中将没有任何迹象表明WPAD服务已经崩溃。
事实上,我们的利用没有很好的回收措施,一旦运行载荷就会使WPAD服务崩溃,所以如果我们在服务被利用后继续提供PAC文件,它将会被再次利用。你可以在图7中看到这个效果,这个效果是在漏洞利用服务器运行了几分钟并且在受害者机器上发出大量的HTTP请求之后得到的。
稍后我们会将漏洞利用源码发布在issuehacker中
总结执行不受信任的JavaScript代码是危险的,并且在非沙箱环境中执行它更是如此。即使它是由相对强健的JavaScript引擎(如jscript.dll)也是如此。我们确定了7个安全漏洞,并成功地演示了在安装了Fall Creator Update的Windows 10 64位完全打补丁(撰写本文)时从本地网络(以及其他地方)执行任意代码。
由于错误现在已经修复,这是否意味着我们已经完成并可以休息?不太可能。尽管我们花费了相当多的时间,精力和计算能力来查找jscript.dll错误,但是我们没有声称我们发现了所有这些错误。事实上,有第7个bug,就可能会有第8个。所以如果有什么东西没有改变的话,我们很有可能会看到这样的一个链条,就像有一天在野外一样(当然,乐观地认为攻击者已经没有这个能力了)。
那么,微软可以做些什么来使今后的攻击变得更加困难:
默认情况下禁用WPAD。实际上,在其他操作系统支持WPAD的情况下,Windows是唯一一个默认启用的。
在WPAD服务内部提供JScript解释器。由于解释器需要执行一个定义好输入的JavaScript函数并返回输出字符串,因此沙盒应该非常简单。考虑到输入输出模型的简单性,如果微软在 seccomp-strict 中引入了一个类似限制性的沙箱,那将是非常好的:有些进程真的不需要比“接收一点数据” “执行一些计算“,”返回一点数据“更多的特权。
如果您想自行采取措施,唯一能够使用新的,防止此类攻击以及目前未知漏洞的方法似乎是完全禁用WinHttpAutoProxySvc服务。有时候,由于其他服务依赖于WPAD,所以在“服务”UI中无法完成(“启动类型”控件将变灰),但可以通过相应的注册表项来完成。在“HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinHttpAutoProxySvc”下,将“Start的值从3(手动)更改为4(禁用)。
以下是在搜索“禁用WPAD”时通常在网上找到的一些建议,这些建议在我们的实验中无法防止攻击:
在控制面板中关闭“自动检测设置”
设置“WpadOverride”注册表项
将“255.255.255.255 wpad”放在主机文件中(这将停止DNS变体,但可能不是DHCP变体)
本文由看雪翻译小组 wangrin 编译 转载请注明来自看雪社区