能力中心
本站所有文章均为原创,如需转载请注明出处
F5 BIG-IP是F5公司的一款应用交付平台,今日F5官方发布安全公告,称有七个有关BIG-IP和BIG-IQ的安全漏洞。其中CVE-2021-22986是一个未认证的远程命令执行漏洞,由于对HTTP请求的认证不完全,可以允许攻击者通过设置特殊的HTTP header来绕过权限认证并访问BIG-IP的REST API从而执行命令。
BIG-IP (全部模块) v16.0.0-16.0.1
BIG-IP (全部模块) v15.1.0-15.1.2
BIG-IP (全部模块) v14.1.0-14.1.3.1
BIG-IP (全部模块) v13.1.0-13.1.3.5
BIG-IP (全部模块) v12.1.0-12.1.5.2
BIG-IQ v7.1.0-7.1.0.2
BIG-IQ v7.0.0-7.0.0.1
BIG-IQ v6.0.0-6.1.0
BIG-IP v16.0.1.1
BIG-IP v15.1.2.1
BIG-IP v14.1.4
BIG-IP v13.1.3.6
BIG-IP v12.1.5.3
BIG-IQ v8.0.0
BIG-IQ v7.1.0.3
BIG-IQ v7.0.0.2
通过爆破目录发现所有的REST API都返回401未认证,结合本漏洞得出存在认证绕过的结论。发送到BIG-IP的HTTP请求会先经过HTTPD Apache进行分析过滤后发送到Jetty的REST服务器,由此可得有两个认证绕过的漏洞。更改HTTPD的Proxy配置可以在本机进行流量分析。
前面提到请求会先发送到Apache,而REST的API目录前缀都含有mgmt
, 于是使用grep
指令寻找该字符串并找到mod_pam_auth.so
模块。把文件放到IDA中进行反编译,出于不知名原因IDA不能成功将文件反编译为C代码,便通过总体程序流向分析。
通过关键词可以看出以上部分是查看是否请求中有X-F5-Auth-Token,有的话将直接发送到Jetty并进行后续认证。没有的话会对访问目录进行查看,最终会查找Authorization和其他HTTP的header。
通过函数名可以得知是对密码进行操作比较。根据以上逻辑可以得出如果HTTP请求中设置了X-F5-Auth-Token的header,不论值正确与否都将成功绕过Authorization的检测。并且经测试服务器返回结果确实如此。
没有Authorization header但有X-F5-Auth-Token时,401的回复是由Jetty发送。
错误的Authorization header和空的X-F5-Auth-Token也将由Jetty发送回复且甚至还可以绕过认证,此问题将在后续提到。
接下来测试没有X-F5-Auth-Token header的结果,同样是401但是是有Apache返回的结果。
下面来看Jetty部分的认证绕过。在服务器根目录里找到相关jar
文件,将所有文件通过JD进行反编译,后通过关键词查询找到f5.rest.jar
。在本文件中发现通过Jetty并不会对通过Apache的HTTP的Basic Authorization进行进一步的认证导致认证绕过。
在f5.rest.workers.authz.AuthzHelper.class中,此函数将Authorization的header解码后返回
public static BasicAuthComponents decodeBasicAuth(String encodedValue) {
BasicAuthComponents components = new BasicAuthComponents();
if (encodedValue == null) {
return components;
}
String decodedBasicAuth = new String(DatatypeConverter.parseBase64Binary(encodedValue));
int idx = decodedBasicAuth.indexOf(':');
if (idx > 0) {
components.userName = decodedBasicAuth.substring(0, idx);
if (idx + 1 < decodedBasicAuth.length())
{
components.password = decodedBasicAuth.substring(idx + 1);
}
}
return components;
}
此函数位于f5.rest.common.RestOperationIdentifier.class,通过basicAuth来设置identityData
private static boolean setIdentityFromBasicAuth(RestOperation request) {
String authHeader = request.getBasicAuthorization();
if (authHeader == null) {
return false;
}
AuthzHelper.BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);
request.setIdentityData(components.userName, null, null);
return true;
}
}
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(new String[] { WellKnownPorts.AUTHZ_USERS_WORKER_URI_PATH, segment }))))
{
userName = segment;
}
}
if (userName != null && RestReference.isNullOrEmpty(userReference)) {
userReference = new RestReference(UrlHelper.buildPublicUri(UrlHelper.buildUriPath(new String[] { 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;
}
通过
request.setIdentityData(components.userName, null, null);
之后的request有以下性质:
identityData.userName = 'admin';
identityData.userReference = 'http://localhost/mgmt/shared/authz/users/admin'
identityData.groupReference = null;
f5.rest.jar中有authn和authz两种class,根据关键词查询发现authn中有BIGIPAuthCookie及其他与BIGIP有关的Cookie, 而authz库中只有basic auth相关的方法函数。由此判断出若请求中有BIGIP相关Cookie则由authn认证,若有Authorization则由authz进行认证。
因为REST服务器默认Basic Authorization数据已经由Apache进行认证所以不需要重新验证账户密码,所以在给identityData赋值时直接是根据用户名的。
在f5.rest.workers.EvaluatePermissions.class中,通过此函数可以了解到第二个认证绕过是如何发生的。
private static void completeEvaluatePermission(final RestOperation request, AuthTokenItemState token, final CompletionHandler<Void> finalCompletion) {
final String path;
//由于token没有值, 所以是null,绕过第一个F5 token的认证
if (token != null) {
if (token.expirationMicros.longValue() < RestHelper.getNowMicrosUtc()) {
String error = "X-F5-Auth-Token has expired.";
setStatusUnauthorized(request);
finalCompletion.failed(new SecurityException(error), null);
return;
}
request.setXF5AuthTokenState(token);
}
//此处的request是前面返回的request物件,这个setBasicAuthFromIdentity仅将identity.userName重新进行编码,并不会查看密码。
request.setBasicAuthFromIdentity();
//由于uri不符合所以跳过以下两个比较
if (request.getUri().getPath().equals(EXTERNAL_LOGIN_WORKER) && request.getMethod().equals(RestOperation.RestMethod.POST)) {
finalCompletion.completed(null);
return;
}
if (request.getUri().getPath().equals(UrlHelper.buildUriPath(new String[] { EXTERNAL_LOGIN_WORKER, "available" })) && request.getMethod().equals(RestOperation.RestMethod.GET)) {
finalCompletion.completed(null);
return;
}
//此处的userRef是admin的ref,因为basic auth的用户名是admin
final RestReference userRef = request.getAuthUserReference();
if (RestReference.isNullOrEmpty(userRef)) {
String error = "Authorization failed: no user authentication header or token detected. Uri:" + request.getUri() + " Referrer:" + request.getReferer() + " Sender:" + request.getRemoteSender();
setStatusUnauthorized(request);
finalCompletion.failed(new SecurityException(error), null);
return;
}
//因为admin是DefaultAdminRef, 所以认证成功
if (AuthzHelper.isDefaultAdminRef(userRef)) {
finalCompletion.completed(null);
return;
}
//认证成功所以并不会执行以下所有代码
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("$expand") != null) {
String filterField = request.getParameter("$filter");
if (USERS_GROUP_FILTER_STRING.equals(filterField) || USERGROUPS_GROUP_FILTER_STRING.equals(filterField)) {
finalCompletion.completed(null);
return;
}
}
if (token != null && path.equals(UrlHelper.buildUriPath(new String[] { EXTERNAL_AUTH_TOKEN_WORKER_PATH, token.token }))) {
finalCompletion.completed(null);
return;
}
roleEval.evaluatePermission(request, path, verb, new CompletionHandler<Boolean>()
{
public void completed(Boolean result)
{
if (result.booleanValue()) {
finalCompletion.completed(null);
return;
}
String error = "Authorization failed: user=" + userRef.link + " resource=" + path + " verb=" + verb + " uri:" + request.getUri() + " referrer:" + request.getReferer() + " sender:" + request.getRemoteSender();
EvaluatePermissions.setStatusUnauthorized(request);
finalCompletion.failed(new SecurityException(error), null);
}
public void failed(Exception ex, Boolean result) {
request.setBody(null);
request.setStatusCode(500);
String error = "Internal server error while authorizing request";
finalCompletion.failed(new Exception(error), null);
}
});
}
而以上通过设置Authorization的header可以绕过Authn而进行Authz认证。
通过了以上认证绕过的请求已经拥有了管理员admin的权限,故此可以访问REST API。通过查询F5的官方文档得知可以通过POST请求到/mgmt/tm/util/bash
地址来执行指令,请求格式为JSON。
{
"command": "run",
"utilCmdArgs": "-c id"
}
通过比较修复前后的Jetty端代码可发现F5如何应对并修复的漏洞。
- private static boolean setIdentityFromBasicAuth(RestOperation request) {
+
+
+ private static boolean setIdentityFromBasicAuth(final RestOperation request, final Runnable runnable) {
String authHeader = request.getBasicAuthorization();
if (authHeader == null) {
return false;
}
- AuthzHelper.BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);
- request.setIdentityData(components.userName, null, null);
+ final AuthzHelper.BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);
+
+
+
+
+
+ String xForwardedHostHeaderValue = request.getAdditionalHeader("X-Forwarded-Host");
+
+
+
+ if (xForwardedHostHeaderValue == null) {
+ request.setIdentityData(components.userName, null, null);
+ if (runnable != null) {
+ runnable.run();
+ }
+ return true;
+ }
+
+
+
+ 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")) {
+
+ request.setIdentityData(components.userName, null, null);
+ if (runnable != null) {
+ runnable.run();
+ }
+ return true;
+ }
+
+
+ if (!PasswordUtil.isPasswordReset().booleanValue()) {
+ request.setIdentityData(components.userName, null, null);
+ if (runnable != null) {
+ runnable.run();
+ }
+ return true;
+ }
+
+ 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, null, 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), null)).setCompletion(authCompletion);
+
+
+ RestRequestSender.sendPost(subRequest);
+ } catch (URISyntaxException e) {
+ LOGGER.warningFmt("ERROR: URISyntaxEception %s", new Object[] { e.getMessage() });
+ }
return true;
}
}
而通过阅读以上代码可以发现首先Jetty会解码并查看比较Basic Auth header的账号密码,后面也会查看发送请求的IP是否为localhost,最后也对密码和账户状态进行一系列的查看。
出去本文提到的认证绕过导致的RCE,F5仍然还存在着SSRF导致的RCE漏洞,对于这个的漏洞分析可于https://attackerkb.com/topics/J6pWeg5saG/k03009991-icontrol-rest-unauthenticated-remote-command-execution-vulnerability-cve-2021-22986 找到。
https://support.f5.com/csp/article/K02566623
https://support.f5.com/csp/article/K43371345
https://attackerkb.com/topics/J6pWeg5saG/k03009991-icontrol-rest-unauthenticated-remote-command-execution-vulnerability-cve-2021-22986
3accords
2023/01/27 08:233deposits
2022/09/02 20:46匿名
2021/03/19 11:53