CVE-2022-1388漏洞分析

2022-5-10 11:37:22 Author: wiki.ioin.in(查看原文) 阅读量:267 收藏

前言

前两天在赛博群里看大家讨论这个洞讨论的火热,简简单单的poc,轻轻松松的执行命令,分分钟杀穿BIGIP,看大家发的文章涉及java层的比较少,正巧这个月没写啥文章,蹭一波热度吧。哈哈哈。

环境搭建

进入产品下载页面。https://downloads.f5.com/esd/productlines.jsp

这里我们找一个存在漏洞的版本下载。

下载13.1.3.6_Virtual-Edition版本

https://downloads.f5.com/esd/ecc.sv?sw=BIG-IP&pro=big-ip_v13.x&ver=13.1.3&container=13.1.3.6_Virtual-Editio

下载BIGIP-13.1.3.6-0.0.4.ALL-vmware.ova

https://downloads.f5.com/esd/serveDownload.jsp?path=/big-ip/big-ip_v13.x/13.1.3/english/13.1.3.6_virtual-edition/&sw=BIG-IP&pro=big-ip_v13.x&ver=13.1.3&container=13.1.3.6_Virtual-Edition&file=BIGIP-13.1.3.6-0.0.4.ALL-vmware.ova

接着选择下载链接。

气抖冷,为啥没有中国。这里我从新加坡下载速度还不错。下载链接如下:

https://downloads-sin-f5.s3.ap-southeast-1.amazonaws.com/big-ip/big-ip_v13.x/13.1.3/english/13.1.3.6_virtual-edition/BIGIP-13.1.3.6-0.0.4.ALL-vmware.ova?response-content-disposition=attachment%3B%20filename%3DBIGIP-13.1.3.6-0.0.4.ALL-vmware.ova&X-Amz-Security-Token=FwoGZXIvYXdzEDIaDK71xrWQ3UmgkAdvXiKCAVWaVR6sN0%2B2BXWF%2FYndZRIPyyO0NL1z04ni754BR7SkbrHY%2FYYJn2SwC3hZBET8yi6XWpCt0mEIMWHZt3SUn5hyYcDkE%2FO3fznwuVsqKdGLX6x0rmHO0Rxm3%2FUuJT1wWzHFr%2BDb4nRggX1bg8pPJny4HAtUVuxOVsucdDwc3RI%2BwHcozaLjkwYyKISafETtiqoumXaqokpUe1kgDC63JzFxD68HbTMKc6I2werJf%2Breeto%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220509T083110Z&X-Amz-SignedHeaders=host&X-Amz-Expires=86398&X-Amz-Credential=ASIAWZEHK3GDE5X4S3GD%2F20220509%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=03b2b613a6b07a9df8174fc95d77da92e72acebf48f51818b581835f7f531e6f

下载后将其解压下来,使用VMware导入。

导入成功,启动。

登录需要密码,经过在网上查询。默认的用户名是 root,密码是default.

使用ifconfig命令查看发现是ipv6没有ipv4 查看/etc/sysconfig/network-scripts/ifcfg-eth0内容,发现BOOTROTO内容为static 改为dhcp 如下图

使用config命令更新网络设置,如下图

得到ip。使用xshell连接可以更方便的进行操作。随后我发现了一个问题,那就是访问不了80端口。在网上查阅资料发现走的是https协议,怪不得访问不到 访问https://192.168.50.242/tmui/login.jsp出现登录界面 至此环境准备结束。

漏洞验证

根据网上的poc,尝试发送请求结果直接就执行了命令。

接着具体分析漏洞。

漏洞分析

进行漏洞分析首先需要先定位漏洞点。根据poc发现漏洞是存在于443端口的https的服务上,接着ssh登录上机器,看看443端口绑定的什么服务。

发现是httpd服务。这里看一下httpd服务的配置文件,来找到具体是谁在处理数据。httpd的配置文件叫httpd.conf 这里用find搜索一下有两个结果,再仔细看一下。

文件位于/var/run/config/httpd.conf仔细检查配置文件。

这里注意一点。AuthPAM开启,说明调用了httpd的某个so文件进行预先的认证。

另外就是发向mgmt的请求,都是8100端口的服务处理的。看一下8100端口是什么服务。

位于此处的是一个java服务。以下为classpath:

classpath :/usr/share/java/rest/f5.rest.adc.bigip.jar:/usr/share/java/rest/f5.rest.adc.shared.jar:/usr/share/java/rest/f5.rest.asm.jar:/usr/share/java/rest/f5.rest.icr.jar:/usr/share/java/rest/f5.rest.jar:/usr/share/java/rest/libs/axis-1.1.jar:/usr/share/java/rest/libs/bcpkix-1.59.jar:/usr/share/java/rest/libs/bcprov-1.59.jar:/usr/share/java/rest/libs/commons-discovery.jar:/usr/share/java/rest/libs/commons-exec-1.3.jar:/usr/share/java/rest/libs/commons-io-1.4.jar:/usr/share/java/rest/libs/commons-lang.jar:/usr/share/java/rest/libs/commons-logging.jar:/usr/share/java/rest/libs/concurrent-trees-2.5.0.jar:/usr/share/java/rest/libs/f5.asmconfig.jar:/usr/share/java/rest/libs/f5.rest.mcp.mcpj.jar:/usr/share/java/rest/libs/f5.rest.mcp.schema.jar:/usr/share/java/rest/libs/f5.soap.licensing.jar:/usr/share/java/rest/libs/federation.jar:/usr/share/java/rest/libs/gson-2.6.2.jar:/usr/share/java/rest/libs/icrd-src.jar:/usr/share/java/rest/libs/icrd.jar:/usr/share/java/rest/libs/jaxrpc-1.1.jar:/usr/share/java/rest/libs/joda-time-2.9.4.jar:/usr/share/java/rest/libs/jsch-0.1.53.jar:/usr/share/java/rest/libs/json_simple.jar:/usr/share/java/rest/libs/libthrift.jar:/usr/share/java/rest/libs/log4j.jar:/usr/share/java/rest/libs/lucene-analyzers-common-4.10.4.jar:/usr/share/java/rest/libs/lucene-core-4.10.4.jar:/usr/share/java/rest/libs/lucene-facet-4.10.4.jar:/usr/share/java/rest/libs/odata4j-0.7.0-core.jar:/usr/share/java/rest/libs/quartz-2.2.1.jar:/usr/share/java/rest/libs/slf4j-api.jar:/usr/share/java/rest/libs/slf4j-log4j12.jar:/usr/share/java/rest/libs/wsdl4j-1.1.jar:/usr/share/java/f5-avr-reporter-api.jar:/usr/share/java/commons-codec.jar com.f5.rest.workers.RestWorkerHost

包这么多,如何定位到出现问题的位置呢?这里我去掉了header的Connection中的X-F5-Auth-Token这样的话java就会产生报错打印出堆栈信息。如下图所示.

通过前面的classpath把载入的java包下载下来进行分析。使用反编译工具简单看一下jar包,这里我使用的是jadx。报错中主要涉及的库,位于f5.rest.jar中。

根据经验来看,底层的栈都是错误处理,真正出现问题的地方在中层。例如以下这个看着就离事发地点很近。

at com.f5.rest.workers.storage.StorageWorker.onQuery(StorageWorker.java:235)",

以下为函数体

从currentGenerationMap 取不到storageKey对应的值引发报错。随后往上层栈翻了翻没找到鉴权相关流程。这里切换一下思路。我们找一找X-F5-Auth-Token的处理流程。找到三个和X-F5-Auth-Token相关的代码。

public static final String ACCESS_CONTROL_ALLOW_HEADERS_VALUE = "X-F5-Auth-Token, X-F5-REST-Coordination-Id, X-Auth-Token, X-Forwarded-For, X-F5-Gossip, Authorization, Cookie, Content-Length, Content-Range, Content-Type, User-Agent";
//该变量设置了准许头的值
public static final String X_F5_AUTH_TOKEN_HEADER = "X-F5-Auth-Token";
public static final String X_F5_AUTH_TOKEN_HEADER_WITH_COLON = "X-F5-Auth-Token:"

查找变量X_F5_AUTH_TOKEN_HEADER的引用发现其在buildRequestHeaders函数中使用。整个函数的作用,就是提取请求的头。以字符串把请求返回去。看看另一个变量的引用,存在这么一段代码:

public String getName() {
    return RestOperation.X_F5_AUTH_TOKEN_HEADER_WITH_COLON;
}
public void setData(RestOperation operation, String value) {
    operation.setXF5AuthToken(value);
}
public boolean quickCheck(StringBuilder headerLine) {
    if (!HttpParserHelper.matchesOneChar(headerLine.charAt(2), 'F''f') || HttpParserHelper.matchesHeaderPrefix(headerLine, getName())) {
        return false;
    }
    return true;
}

getName获取参数名字 setData调用operation.setXF5AuthToken 设置值 quickCheck用于简单校验判断是不是这个header,核心代码如下

!HttpParserHelper.matchesOneChar(headerLine.charAt(2), 'F''f') || !HttpParserHelper.matchesHeaderPrefix(headerLine, getName())) 

这么设计可能是为了节约时间?收起疑问继续分析。operation.setXF5AuthToken函数如下。

public RestOperation setXF5AuthToken(String token) {
    setupAuthorizationData();
    if (token == null) {
        this.authorizationData.xF5AuthTokenState = null;
    } else {
        this.authorizationData.xF5AuthTokenState = new AuthTokenItemState();
        this.authorizationData.xF5AuthTokenState.token = token;
    }
    return this;
}

该函数首先使用函数创建对象,具体实现代码如下

private void setupAuthorizationData() {
    if (this.authorizationData == null) {
        this.authorizationData = new AuthorizationData();
    }
}

接着判断传入的参数token是否为空 为空把this.authorizationData.xF5AuthTokenState 设置为null。不为空则把值赋值给this.authorizationData.xF5AuthTokenState.token 。往下看发现了getXF5AuthToken和getXF5AuthTokenState方法。

public String getXF5AuthToken() {
    if (this.authorizationData == null || this.authorizationData.xF5AuthTokenState == null) {
        return null;
    }
    return this.authorizationData.xF5AuthTokenState.token;
}
public AuthTokenItemState getXF5AuthTokenState() {
    if (this.authorizationData == null) {
        return null;
    }
    return this.authorizationData.xF5AuthTokenState;
}

在鉴权过程中必然会使用这两个代码,因此查看这两个函数的引用即可。猜测鉴权过程中先调用getXF5AuthTokenState 确认是否有token 而后使用get获取token。(猜错了哈哈哈) 查看两个函数的引用,getXF5AuthToken的引用如下

重点看第三四个,是EvaluatePermissions的两个方法,英文中 evaluate 的含义是评估Permission 的含义是许可。进入其中查看其代码果然整个EvaluatePermissions是鉴权部分。整个EvaluatePermissions含有三个方法。evaluatePermission,completeEvaluatePermission,failPermissionValidation 其中failPermissionValidation代表鉴权失败,做一些失败后的数据设置

public static void failPermissionValidation(RestOperation request, String error) {
    request.setWwwAuthenticate(RestOperation.X_AUTH_TOKEN_HEADER);
    String deviceAuthCookie = request.getCookie(DeviceAuthTokenHelper.BIGIP_AUTH_COOKIE);
    String authToken = request.getXF5AuthToken();
    if (deviceAuthCookie != null && authToken == null) {
        request.setWwwAuthenticate(RestOperation.BASIC_REALM_REST_API);
    }
    request.setBody((String) null);
    request.setIsRestErrorResponseRequired(true);
    request.setStatusCode(RestOperation.STATUS_UNAUTHORIZED);
    request.fail(new SecurityException(error));
}

其中最核心的代码是completeEvaluatePermission函数,而漏洞也出在此处。evaluatePermission函数会根据情况不同,来为completeEvaluatePermission提供不同的参数。

这里可以看到根据authToken的值为completeEvaluatePermission赋予不同的参数。当authToken为null时,completeEvaluatePermission的token参数为空,这时候问题就来了,我们分析completeEvaluatePermission函数。以下是completeEvaluatePermission的部分代码

public static void completeEvaluatePermission(RestOperation request, AuthTokenItemState token, RolesWorker rolesWorker, CompletionHandler<Void> finalCompletion) {
    final String path;
    if (token != null) {
        if (token.expirationMicros.longValue() < RestHelper.getNowMicrosUtc()) {
            failPermissionValidation(request, "X-F5-Auth-Token has expired.");
            finalCompletion.failed((Exception) nullnull);
            return;
        }
        request.setXF5AuthTokenState(token);
    }
    request.setBasicAuthFromIdentity();
    if (!request.getUri().getPath().equals(EXTERNAL_LOGIN_WORKER) || !request.getMethod().equals(RestOperation.RestMethod.POST)) {
        final RestReference userRef = request.getAuthUserReference();
        if (RestReference.isNullOrEmpty(userRef)) {
            failPermissionValidation(request, "Authorization failed: no user authentication header or token detected. Uri:" + request.getUri() + " Referrer:" + request.getReferer() + " Sender:" + request.getRemoteSender());
            finalCompletion.failed((Exception) nullnull);
        } else if (AuthzHelper.isDefaultAdminRef(userRef)) {
            finalCompletion.completed(null);
        } else {
            if (UrlHelper.hasODataInPath(request.getUri().getPath())) {
                path = UrlHelper.removeOdataSuffixFromPath(UrlHelper.normalizeUriPath(request.getUri().getPath()));
            } else {
                path = UrlHelper.normalizeUriPath(request.getUri().getPath());
            }
            final RestOperation.RestMethod verb = request.getMethod();
            if (path.startsWith(EXTERNAL_GROUP_RESOLVER_PATH) && request.getParameter(RestHelper.ODATA_EXPAND_FIELD) != null) {
                String filterField = request.getParameter(RestHelper.ODATA_FILTER_FIELD);
                if (USERS_GROUP_FILTER_STRING.equals(filterField) || USERGROUPS_GROUP_FILTER_STRING.equals(filterField)) {
                    finalCompletion.completed(null);
                    return;
                }
            }
            if (token != null) {
                if (path.equals(UrlHelper.buildUriPath(EXTERNAL_AUTH_TOKEN_WORKER_PATH, token.token))) {
                    finalCompletion.completed(null);
                    return;
                }
            }

第一步,判断token是否为空,当token不为空时,略过此流程,往下继续走。

request.setBasicAuthFromIdentity()函数方法如下

public void setBasicAuthFromIdentity() {
    if (this.authorizationData != null) {
        this.authorizationData.basicAuthValue = AuthzHelper.encodeBasicAuth(getAuthUser(), (String) null);
    }
}

作用大致为,设置basicAuthValue的值为base64编码后的this.identityData.userName。继续往下走。

出现这样一段代码。

if (!request.getUri().getPath().equals(EXTERNAL_LOGIN_WORKER) || !request.getMethod().equals(RestOperation.RestMethod.POST)) {

大致作用为,对访问路径,请求包的方法进行校验,当我们请求的路径不是登录路径或者不是post方法时,进入后面的流程。接着执行

final RestReference userRef = request.getAuthUserReference();
if (RestReference.isNullOrEmpty(userRef)) {
        failPermissionValidation(request, "Authorization failed: no user authentication header or token detected. Uri:" + request.getUri() + " Referrer:" + request.getReferer() + " Sender:" + request.getRemoteSender());
        finalCompletion.failed((Exception) nullnull);
    } else if (AuthzHelper.isDefaultAdminRef(userRef)) {
        finalCompletion.completed(null);

获取this.identityData.userReference的值给userRef,随后判断userRef的值是否为空,接着判断userRef是否是admin的userRef值。当为admin的userRef值时。进入finalCompletion.completed函数。从而导致了绕过鉴权。回过头来思考一个问题,this.identityData.userReference是从何获取的呢?通过查看该数据的引用。找到了函数setIdentityData。代码如下。

public RestOperation setIdentityData(String userName, RestReference userReference, RestReference[] groupReferences) {
    if (userName == null && !RestReference.isNullOrEmpty(userReference)) {
        String segment = UrlHelper.getLastPathSegment(userReference.link);
        if (userReference.link.equals(UrlHelper.buildPublicUri(UrlHelper.buildUriPath(WellKnownPorts.AUTHZ_USERS_WORKER_URI_PATH, segment)))) {
            userName = segment;
        }
    }
    if (userName != null && RestReference.isNullOrEmpty(userReference)) {
        userReference = new RestReference(UrlHelper.buildPublicUri(UrlHelper.buildUriPath(WellKnownPorts.AUTHZ_USERS_WORKER_URI_PATH, userName)));
    }
    this.identityData = new IdentityData();
    this.identityData.userName = userName;
    this.identityData.userReference = userReference;
    this.identityData.groupReferences = groupReferences;
    return this;
}

这里分两种情况userReference和userName均为空以及userReference和userName均不为空。这两个变量是函数的参数,往上找上一层的函数。查看其引用发现com.f5.rest.common.RestOperationIdentifier有多次引用,看一下代码。其中有一个setIdentityFromBasicAuth方法。

private static boolean setIdentityFromBasicAuth(RestOperation request) {
    String authHeader = request.getBasicAuthorization();
    if (authHeader == null) {
        return false;
    }
    request.setIdentityData(AuthzHelper.decodeBasicAuth(authHeader).userName, (RestReference) null, (RestReference[]) null);
    return true;
}

看起来像是从header取得数据来进行初始化authHeader变量。实际上返回的是this.authorizationData.basicAuthValue的值,接着找设置该值的函数。也就是setBasicAuthorizationHeader函数,代码如下

public RestOperation setBasicAuthorizationHeader(String value) {
    byte[] data;
    setupAuthorizationData();
    if (value != null && ((data = DatatypeConverter.parseBase64Binary(value)) == null || data.length == 0)) {
        LOGGER.warningFmt("Basic Authorization header set to value that is invalid base64. Value: %s", value);
        value = null;
    }
    this.authorizationData.basicAuthValue = value;
    return this;
}

接着查看该函数在何处引用,传入的value值是什么。看到了熟悉的一串代码,类似从header的X-F5-Auth-Token提取数据初始化的过程。

BASIC_AUTH {
    public String getName() {
        return RestOperation.BASIC_AUTHORIZATION_HEADER_LOWERCASE;
    }
    public void setData(RestOperation operation, String value) {
        operation.setBasicAuthorizationHeader(value);
    }
    public boolean quickCheck(StringBuilder headerLine) {
        return HttpParserHelper.matchesOneChar(headerLine.charAt(0), 'A''a') && HttpParserHelper.matchesOneChar(headerLine.charAt(1), 'U''u') && HttpParserHelper.matchesHeaderPrefix(headerLine, getName());
    }
},

查看getname返回的字段RestOperation.BASIC_AUTHORIZATION_HEADER_LOWERCASE定义的变量,代码如下:

public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BASIC_AUTHORIZATION_HEADER = "Authorization: Basic ";
public static final int BASIC_AUTHORIZATION_HEADER_LENGTH = BASIC_AUTHORIZATION_HEADER.length();
public static final String BASIC_AUTHORIZATION_HEADER_LOWERCASE = BASIC_AUTHORIZATION_HEADER.toLowerCase();

也就是说该字段是由header的Authorization: Basic设置的。观察poc也存在此字段

Authorization: Basic YWRtaW46

这里我们尝试更改poc中的Authorization数据为dXNlcjo=,也就是user:的base64编码

果然不能执行命令了。YWRtaW46的base64解码正好是admin: 至此整个绕过的思路就清晰了,首先是当X-F5-Auth-Token为空时走入另一条验证流程,而这个流程依赖于我们给header提供的Authorization:字段。因为Authorization字段可控,并且没有复杂的加密处理,从而导致可以轻易绕过鉴权。接着就是如何设置X-F5-Auth-Token为空了。这里涉及到一个hop-by-hop headers abuse的漏洞,可以参考https://nathandavison.com/blog/abusing-http-hop-by-hop-request-headers。

简单来说就是。遇到Keep-Alive、Transfer-Encoding、TE、Connection、Trailer、Upgrade这些标头时,兼容的代理应该处理或操作这些标头所指示的任何内容,而不是将它们转发到下一个跃点。如我们的请求中带有header头:Connection: close, X-Foo, X-Bar,原始请求在转发到代理时,逐跳处理则会将X-Foo和 X-Bar从原始请求中删除。这样我们既通过了对X-F5-Auth-Token 标头的校验,同时又能使其在到达java处理流程时为空。而在实际测试中,我却发现这个和hop-by-hop参考文章里的又不一样。即使我不使用Keep-Alive、Transfer-Encoding、TE、Connection、Trailer、Upgrade这些标头。我一样可以执行命令如下图:

又或者

经过和忍酱、pcat、Zeddy、落沐萧萧等师傅的讨论,我大概理解了,应该是这样的,httpd服务当存在Connection:的时候,不论提供的参数是什么值都会产生逐跳,缺省参数会默认按keep-alive处理。

漏洞修复

具体修复参考官方提供的方法,https://support.f5.com/csp/article/K23605346。

一种是通过下载官方修补后的版本,也就是Fixes introduced的版本。

在https://downloads.f5.com/esd/productlines.jsp里选择最新的版本即可。

如果不想更新版本官方还提供了其他的缓解措施。

  • 通过自身 IP 地址阻止 iControl REST 访问(https://support.f5.com/csp/article/K23605346#proc1)
  • 通过管理界面阻止 iControl REST 访问(https://support.f5.com/csp/article/K23605346#proc2)
  • 修改 BIG-IP httpd 配置(https://support.f5.com/csp/article/K23605346#proc3)

具体措施可以在官方页面查看。https://support.f5.com/csp/article/K23605346#proc1。

结语

此时是5.10日凌晨2.15终于写完这篇文章,后面状态不是特别好,并且个人对web的知识以及httpd的特性也不是特别了解,如果写的有什么问题欢迎大佬们帮我纠正。

参考文献

https://nathandavison.com/blog/abusing-http-hop-by-hop-request-headers

https://nosec.org/home/detail/4722.html

https://mp.weixin.qq.com/s/6gVZVRSDRmeGcNYjTldw1Q


文章来源: https://wiki.ioin.in/url/DA7R
如有侵权请联系:admin#unsafe.sh