通过反向代理走私 HTTP 标头

admin 2023年12月26日14:23:02评论29 views字数 8931阅读29分46秒阅读模式

之前发过一篇HTTP协议解析问题造成的一些绕过风险利用HTTP解析器不一致进行ACL绕过,最近又发现了一些新的东东,在某些情况下,可以通过反向代理走私 HTTP 标头,即使之前已明确未设置。 在某些情况下,由于 HTTP 标头标准化和解析器差异,这是可能的。 由于 HTTP 标头通常用作将身份验证数据传递到后端的方式(例如在双向 TLS 场景中),因此这可能会导致严重漏洞。

在下面的文章中,我将描述一些理论和实践场景以及如何滥用它们。 其中一些方法用于绕过关键内部应用程序的身份验证。如果您不想阅读详细说明,可以 在本文末尾 查看总结。

          

通过反向代理进行客户端证书身份验证

在对内部平台进行审计时,我更深入地了解了我们内部经常使用的身份验证方法。此身份验证是通过智能卡完成的,并为我们提供了一种简单的方法,通过用户的电子邮件地址和 X509 客户端证书来对用户进行身份验证。由于许多员工都可以使用它,因此与旧用户名加密码相比,它是一种快速且更好的身份验证方法。

这个过程一开始就很容易实现。我们的内部指南以及互联网上的许多流行资源都使用如下内容:

1) 后端前面的反向代理执行双向 TLS (mTLS) 流并确保有效的客户端证书。2) 从证书中提取一些 X509 字段,例如电子邮件地址或全名。3) 这些字段作为附加标头添加,并将请求转发到后端。4) 后端对用户进行身份验证(通过传递的字段)。

您会看到这里可能是一个大问题:只有某些标头才能将攻击者与身份验证绕过(或某些权限升级)分开。

为了防止这种情况,通常建议 取消设置原始请求中传递的标头

这是一个示例配置:

<VirtualHost *:443>    # activate HTTPS on the reverse proxy    SSLEngine On    SSLCertificateFile    /etc/apache2/ssl/mycert.crt    SSLCertificateKeyFile /etc/apache2/ssl/mycert.key
<Location /auth/cert> # activate the client certificate authentication SSLCACertificateFile /etc/apache2/ssl/client-accepted-ca-chain.crt SSLVerifyClient require SSLVerifyDepth 3
# enrich request with client certificate data RequestHeader set SSL_CLIENT_S_DN "%{SSL_CLIENT_S_DN}s"
ProxyPass http://localhost:8080/ ProxyPassReverse http://localhost:8080/ </Location></VirtualHost>

所以从理论上讲,这看起来相当安全。但正如您可能已经猜到的那样,情况并非总是如此。让我们首先探讨标准化如何导致意外行为。

被标准化,攻击者最好的朋友

根据反向代理、后端软件甚至所使用框架的组合,攻击者传递的 HTTP 标头将被标准化,并可能干扰已设置的“过滤器”。

对于以下每个场景,我将使用以下 Apache 配置。这将取消设置/删除标头 CLIENT_VERIFIED ,然后将请求传递给在 :1337 运行的应用程序。

RequestHeader unset CLIENT_VERIFIED
<Location /> ProxyPass http://localhost:1337/ ProxyPassReverse http://localhost:1337/</Location>

在深入研究这个问题之前,我有点惊讶我的尝试几乎成功了。正如在很多帖子和文档中可以读到的那样,Apache 和 Nginx 会 默默地删除所有带有下划线的标头 。但是我发现了一些问题:当请求通过 Apache 上的 ProxyPass 传递时,不会完成 此操作。许多框架似乎忽视了这一点, 错误地记录了这种行为    

 apache 文档 指出 ,当通过环境变量传递 HTTP 标头时会发生这种情况:

一个特殊情况是 HTTP 标头,它通过环境变量传递给 CGI 脚本等(见下文)。它们被转换为大写,并且只有破折号被下划线替换;如果标头包含任何其他(无效)字符,则整个标头将被静默删除

Apache和django(与gunicorn一起部署)

在后端,我使用以下代码设置了一个简单的 django 应用程序,它返回所有标头:

Plaintext                  
def index(request):                  
    headers = [f"{k}:{v}" for k,v in request.META.items()]                  
    return HttpResponse('n'.join(headers))

所以有了上面的配置,下面的请求当然不会传递 CLIENT_VERIFIED 到后端,因为Apache之前会取消它:

Plaintext                  
GET / HTTP/1.1                  
Host: localhost                  
CLIENT_VERIFIED: [email protected]

现在我们先退后一步。django/python/gunicorn 如何处理 HTTP 请求?Python 通常通过WSGI 来完成此操作 ,WSGI 是一个描述 Web 服务器如何与 Web 应用程序通信的规范。它源自CGI时代。一旦涉及(或曾经)涉及 CGI,事情就会变得有点奇怪。由于在 CGI 时代如何将标头作为环境变量传递,因此标头名称中存在连字符(和下划线)问题。这导致决定标准化这些标头值:

当 HTTP 标头放入 WSGI 环境中时,它们会通过转换为大写、将所有破折号转换为下划线以及在前面添加 HTTP_ 来标准化。

因此,作为“foo-bar”传递到 django 应用程序的标头被转换/规范化为 HTTP_FOO_BAR . 如果您现在添加 1 和 1,您应该会看到这将如何影响我们的场景:    

通过反向代理走私 HTTP 标头

          

因此,Apache 配置中的( unsetset 空字符串)无效。

这种混淆已经在 几年前 django 的安全公告中记录下来。上述修复(“为了防止此类攻击,Nginx 和 Apache 2.4+ 默认情况下会从传入请求中删除所有包含下划线的标头。”)在反向代理环境中不会发生。但是,即使这样做,我们也不会在此处的请求中使用下划线,因为它们无论如何都会转换为下划线。

重要提示:如果 apache 取消设置连字符标头名称(例如 CLIENT-VERIFIED ),这也有效。然后你可以只传递带有下划线的标头(例如 CLIENT_VERIFIED ),django 会很乐意将其转换为 HTTP_CLIENT_VERIFIED .

Apache和flask(与gunicorn一起部署)

Flask(使用 werkzeug )有自己的处理标题(和连字符)的方法。使用以下代码(在Flask应用程序中):

Plaintext                  
@app.route('/')                  
def index():                  
    headers = [f"{k}:{v}" for k,v in request.headers]                  
    header_str = 'n'.join(headers)                  
    is_authenticated = request.headers.get('CLIENT_VERIFIED', False)                  
    return f"{header_str}nn{is_authenticated}"

如果 CLIENT_VERIFIED 传递了一个值,此代码将打印出所有标头并回显(基于 Apache 配置,这应该是不可能的)。    

试一试吧:

请求:

Plaintext                  
GET /login_cst HTTP/1.1                  
Host: 127.0.0.1                  
CLIENT_VERIFIED: foobar

返回:

Plaintext                  
HTTP/1.1 200 OK                  
Date: Wed, 22 Apr 2020 18:16:07 GMT                  
Server: gunicorn/20.0.4                  
Content-Type: text/html; charset=utf-8                  
Content-Length: 160                  
Vary: Accept-Encoding                  
                 
Host:localhost:1337                  
X-Forwarded-For:10.0.2.2                  
X-Forwarded-Host:127.0.0.1                  
X-Forwarded-Server:10.0.2.15                  
Connection:Keep-Alive                  
                 
False

正如您在这里所看到的,Flask (werkzeug) 在这里进行了自己的规范化,将每个单词大写并转换为连字符。

现在让我们尝试滥用这种标准化并绕过“身份验证”:

通过反向代理走私 HTTP 标头    

          

你可以看到这里 is_authenticated 返回了一个值。这是可能的,因为 werkzeug 覆盖了 __getitem__ 方法,用下划线替换连字符。这样,我们的走私标头现在就可以在 访问了 request.headers.get('CLIENT_VERIFIED') 。巨大的成功!

此时我必须承认,我自己编写的一个应用程序很容易受到这种攻击。这里的主要问题是(a)我在中间件中接受了身份验证标头,因此不在特定路径下,并且(b)标头规范化导致绕过 unset .

重要提示:此方法也可以反之亦然:如果 Apache 未设置像FOO-BAR 之类的标头  ,我们可以只发送 FOO_BAR 将被标准化的标头,然后仍然可以在 Flask 中使用 进行访问 request.headers.get('FOO-BAR')

Apache和 PHP

PHP 的工作方式与 django 在这里相同,规范化 client-verified 标头并使其在 $_SERVER['HTTP_CLIENT_VERIFIED'] . 后端无法区分它最初是作为 CLIENT_VERIFIED (来自潜在的反向代理)还是直接从客户端发送的。

GET /phpinfo.php HTTP/1.1Host: 127.0.0.1client-verified: [email protected]
[...]<tr><td class="e">$_SERVER['HTTP_CLIENT_VERIFIED']</td><td class="v">[email protected]</td></tr>[...]


当反向代理设置标头(例如 SSL_Test )并且客户端选择标准化后相同的标头名称时,还会出现一些有趣的行为:SSL-Test 。标准化后,该标头将为 SSL_TEST .

当使用 Apache 和 Flask/django 执行此操作时,标头会连接起来 - 首先是客户端标头:

<VirtualHost *:443>    # [...]    <Location />        RequestHeader set SSL_Test "[email protected]"
ProxyPass http://localhost:1337/ ProxyPassReverse http://localhost:1337/ </Location></VirtualHost>

结果(在本例中来自 django):HTTP_SSL_TEST:foobar,[email protected]

如您所见,客户端值已预先添加。X-Forwarded-For 虽然我不得不承认在这种情况下它的前景不太好,但是当向、等 添加值时,这可能会变得很方便, X-Forwarded-Host 这些值是由 Apache 默认设置的:

通过反向代理走私 HTTP 标头

          

现在让我们采用以下 nginx 配置,结果看起来更有希望:

Plaintext                  
server {                  
    listen 80 default_server;                  
                 
    location /foo {                  
        proxy_set_header SSL_Test "[email protected]"                  
        proxy_pass http://localhost:1337                  
    }                  
}

结果响应(Flask):

Ssl-Test: [email protected],foobar (或对于 django SSL_TEST: [email protected],foobar )。

无法通过此绕过直接匹配,但您可能会成功通过一些过滤器。一个例子是使用过滤器来匹配后缀 @corp.com

Plaintext                  
@app.route("/login")                  
def login_certificate():                 
    email = request.headers.get('SSL_CLIENT_S_DN_Email', False)                  
                 
    if not email.endswith('@corp.com'):                  
        return abort(403)                  
                 
    return "authenticated"
       

 @corp.com 现在作为标头值 发送将绕过上述方法,因为它将被附加并因此 endswith 返回成功:

Plaintext                  
SSL_CLIENT_S_DN_EMAIL: [email protected],@corp.com

真实环境下身份验证绕过

一些内部平台上的情况略有不同。身份验证配置有很大的不同(与我在本文开头描述的配置不同)。它看起来像这样:

<VirtualHost *:443>    # virtualhost config    # [...]
<Location /auth/cert/smartcard.xhtml> # do client certification stuff SSLVerifyClient require SSLVerifyDepth 3 SSLOptions +StrictRequire +StdEnvVars
# set headers for backend RequestHeader set SSL_CLIENT_S_DN_Email "%{SSL_CLIENT_S_DN_Email}s" RequestHeader set SSL_CLIENT_S_DN_CN "%{SSL_CLIENT_S_DN_CN}s" RequestHeader set SSL_CLIENT_VERIFY "%{SSL_CLIENT_VERIFY}s" </Location>
ProxyPass http://localhost:8443/ ProxyPassReverse http://localhost:8443/</VirtualHost>

如您所见,仅在处理客户端身份验证时才设置身份验证标头(否则表示:路径匹配)。后端应用程序(在 Tomcat 上运行的 Java 应用程序)仅接受路径下的特定标头 /auth/cert/smartcard.xhtml 。身份验证流程示例:    

用户请求 /auth/cert/smartcard.xhtml

Apache 将确保客户端发送有效的证书

Apache 将使用从证书中提取的身份验证信息来丰富原始请求并将其转发。

后端接收丰富的请求 /auth/cert/smartcard.xhtml

由于攻击者无法直接到达 /auth/cert/smartcard.xhtml ,因此不可能传递指定的 SSL_* 标头并完成身份验证。因此旁路是不可能的。或者是吗?

这里的关键假设是前端 Web 服务器总是以与 后端相同的方式解释路径 。如果没有,您可以跳过 Location 反向代理检查的块,并将身份验证标头直接传递到后端。

我记得几年前 Orange Tsai 的 一些关于解析器逻辑的 Blackhat 演讲谈到了这一点以及 Tomcat(在后端运行)的一些奇怪行为。像这样的路径 /auth/cert;foo=bar/smartcard.xhtml 将被解析为 /auth/cert/smartcard.xhtml ,因为 foo=bar 将被解释为参数。

这是 的路径解释 /auth/cert;foo=bar/smartcard.xhtml

                  

解释路径

Apache

/auth/cert;foo=bar/smartcard.xhtml

nginx

/auth/cert;foo=bar/smartcard.xhtml

IIS

/auth/cert;foo=bar/smartcard.xhtml

Tomcat

/auth/cert/smartcard.xhtml

Jetty

/auth/cert/smartcard.xhtml

WildFly

/auth/cert/smartcard.xhtml

WebLogic

/auth/cert/smartcard.xhtml

结合 (a) Apache 不会删除下划线标头与 (b) 路径规范化行为这一事实,我们可以成功绕过 Apache 证书检查并直接将身份验证标头发送到后端。这将允许绕过身份验证和帐户接管。    

Plaintext                  
$ curl --path-as-is 'https://redacted.telekom.de/auth/cert;foo=bar/smartcard.xhtml'                  
    -H 'SSL_CLIENT_S_DN_Email: [email protected]'                  
    -H "SSL_CLIENT_S_DN_CN: User Name"                  
    -H "SSL_CLIENT_VERIFY: SUCCESS"

不要被愚弄,认为只有带有 Tomcat 的 Apache 才容易受到攻击。我确信还有其他组件组合,其中前端和后端将以不同的方式解释路径。在这种情况下,完全绕过身份验证所需的只是细微的差别。

总结

在某些情况下,HTTP 标头名称可以通过下划线/破折号组合进行欺骗

像 django 或 Flask 这样的 WSGI 框架假定反向代理的工作就是去掉下划线标头

Apache 不会 去掉通过 ProxyPass 和其他一些模块传递的请求的带下划线的标头

Nginx 确实会 去除通过以下方式传递的请求的带下划线的标头 proxy_pass (除非 underscores_in_headers 打开)

如果传递带有连字符的 HTTP 标头名称,基于 WSGI 的框架和 PHP 会将标头标准化,不允许用户区分它最初是如何传递的

以安全敏感的方式匹配此条件的 HTTP 标头可能会被滥用以绕过身份验证

在某些情况下,路径解析差异也会导致身份验证绕过

我没有检查所有可能的组件组合,但对于我查看过的组件,我可以对 Apache 进行简短的概述。Nginx 不会传递下划线,但连字符/下划线转换保持不变:

                  

允许使用下划线

转换 _-

转换 -_

Apache → Django

                  

Apache  → Flask

1

Apache  → PHP

                  

                  

Apache  → tomcat(2)        

                  

                  

Apache +PHP模块

                  

                  

1 连字符不会转换,但所有连字符标头仍可通过下划线访问。

2 路径解析差异危险

Nginx(可能还有其他反向代理)

您可能已经注意到,本文主要关注反向代理场景中的 Apache。这是因为 nginx 默认会去掉所有下划线标头。

当 headers 设置两次时(由于标准化),nginx 会出现一些有趣的行为。虽然传递下划线标头不适用于 nginx,但带有连字符的标头仍可能在后端进行转换。

如果反向代理不去除下划线标头,则可以应用与 Apache 使用的相同技术。我测试的唯一的其他反向代理是 Caddy ,它也不会去除下划线标头。

严重性和影响

如上所述,这并不意味着每个 Apache-mTLS-as-a-reverse-proxy 场景都有问题。这篇文章的要点是强调处理特定标头和配置时的意外行为。有些组合和配置会出现问题,有些则不会。对于 Apache+Tomcat 示例,这可能会导致严重漏洞。当然,情况也并非总是如此。

在本文开头描述的场景中,Apache 与基于 WSGI 的组件一起使用,并且 ProxyPass mTLS 在根级别上使用。这降低了身份验证绕过对帐户接管的影响,因为攻击者无法绕过初始 mTLS。

当身份验证标头仅在特定路径上被接受时,潜在的攻击者需要使用路径差异来完全走私标头。

虽然本文试图给出理论上的 mTLS 身份验证场景的示例,但还有一些其他有趣的标头可能会被走私:、 、 X-Forwarded-ForX-Forwarded-HostX-Real-Ip 如果示例的后端应用程序添加基于 IP 地址的身份验证,攻击者可能会使用上面的技巧之一是将自己的标头值传递到后端。    

由组件组合引起的漏洞及其影响很大程度上取决于所使用的组件及其配置。

建议

有很多组件和场景可能会产生破坏性影响。然而,我们采取了以下措施来防止我们能想到的所有情况:

  • 始终 在根级别取消设置/清除身份验证标头(而不仅仅是在 Location 与身份验证相关的块中)。

  • 不要 在安全敏感的 HTTP 标头名称中使用下划线或连字符,除非您专门检查是否以及如何完成规范化,并且在反向代理级别删除所有变体。

  • 考虑在身份验证块(如 Apache 中)中使用 机密  <Location>  ,并在后端进行检查。这样,即使攻击者能够像 Apache 和 Tomcat 一样滥用解析器差异,攻击者也不知道秘密,并且后端能够注意到伪造。    

原文始发于微信公众号(暴暴的皮卡丘):通过反向代理走私 HTTP 标头

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月26日14:23:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   通过反向代理走私 HTTP 标头https://cn-sec.com/archives/2228091.html

发表评论

匿名网友 填写信息