漏洞概述
Apache Kafka Connect是Apache Kafka生态系统的核心组件,负责实现Kafka与其他外部系统(如数据库、云服务)之间的可靠数据集成。
近期,网宿安全演武实验室监测到攻击者在未授权的情况下可以通过特定方式读取Kafka Connect服务器上的任意文件,从而造成信息泄露。(网宿评分:高危、CVSS 3.1 评分:7.5)
目前该漏洞POC状态已在互联网公开,建议客户尽快做好自查及防护。
受影响版本
3.1.0 <= Apache Kafka <= 3.9.0
漏洞分析
该漏洞源于Kafka Client在SASL/OAUTHBEARER认证配置中对以下参数的校验缺陷:
sasl.oauthbearer.token.endpoint.url
sasl.oauthbearer.jwks.endpoint.url
这里先简单梳理Kafka SASL/OAUTHBEARER认证流程:
客户端 → 获取Token → 连接Kafka → Broker验证 → 授权访问
具体细节详见:https://docs.confluent.io/platform/current/security/authentication/sasl/oauthbearer/overview.html
那么上述问题参数参与了哪部分流程?先看sasl.oauthbearer.token.endpoint.url,它出现在客户端获取Token阶段,其次是sasl.oauthbearer.jwks.endpoint.url,它出现在Broker验证Token阶段。正因这里未严格校验它们所指向的URL,才产生漏洞,至于具体的漏洞文件位置,我们对比一下新旧版本就可以清晰地发现:
clients/src/main/java/org/apache/kafka/common/security/oauthbearer/internals/secured/AccessTokenRetrieverFactory.java

clients/src/main/java/org/apache/kafka/common/security/oauthbearer/internals/secured/VerificationKeyResolverFactory.java

以sasl.oauthbearer.token.endpoint.url为例,对其相应的漏洞文件进行断点分析,代码如下:
public static AccessTokenRetriever create(Map<String, ?> configs, String saslMechanism, Map<String, Object> jaasConfig) {
ConfigurationUtils cu = new ConfigurationUtils(configs, saslMechanism);
URL tokenEndpointUrl = cu.validateUrl("sasl.oauthbearer.token.endpoint.url");
if (tokenEndpointUrl.getProtocol().toLowerCase(Locale.ROOT).equals("file")) {
return new FileTokenRetriever(cu.validateFile("sasl.oauthbearer.token.endpoint.url"));
} else {
JaasOptionsUtils jou = new JaasOptionsUtils(jaasConfig);
String clientId = jou.validateString("clientId");
String clientSecret = jou.validateString("clientSecret");
String scope = jou.validateString("scope", false);
SSLSocketFactory sslSocketFactory = null;
if (jou.shouldCreateSSLSocketFactory(tokenEndpointUrl)) {
sslSocketFactory = jou.createSSLSocketFactory();
}
boolean urlencodeHeader = validateUrlencodeHeader(cu);
return new HttpAccessTokenRetriever(clientId, clientSecret, scope, sslSocketFactory, tokenEndpointUrl.toString(), cu.validateLong("sasl.login.retry.backoff.ms"), cu.validateLong("sasl.login.retry.backoff.max.ms"), cu.validateInteger("sasl.login.connect.timeout.ms", false), cu.validateInteger("sasl.login.read.timeout.ms", false), urlencodeHeader);
}
}
显然该方法动态选择了token的获取方式,那么攻击者想要窃取敏感文件,则需要使用file协议促使代码走本地文件读取token这一逻辑,跟进至
org.apache.kafka.common.security.oauthbearer.internals.secured.FileTokenRetriever
public class FileTokenRetriever implements AccessTokenRetriever {
private final Path accessTokenFile;
private String accessToken;
public FileTokenRetriever(Path accessTokenFile) {
this.accessTokenFile = accessTokenFile;
}
public void init() throws IOException {
this.accessToken = Utils.readFileAsString(this.accessTokenFile.toFile().getPath());
this.accessToken = this.accessToken.trim();
}
public String retrieve() throws IOException {
if (this.accessToken == null) {
throw new IllegalStateException("Access token is null; please call init() first");
} else {
return this.accessToken;
}
}
}
不难发现,这里只接收保存了accessTokenFile参数,没用任何的路径校验或安全防护。而下面的retrieve()方法,也只是检查accessToken是否初始化,就直接返回原token字符串了。
那么接下来就需要考虑,如何将敏感文件的内容带出来?回顾漏洞描述,触发点其实就在认证环节,查看核心登录模块:
org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule
其中就有提取token的方法:
private void identifyToken() throws LoginException {
OAuthBearerTokenCallback tokenCallback = new OAuthBearerTokenCallback();
try {
this.callbackHandler.handle(new Callback[]{tokenCallback});
} catch (UnsupportedCallbackException | IOException var3) {
log.error(var3.getMessage(), var3);
throw new LoginException("An internal error occurred while retrieving token from callback handler");
}
this.tokenRequiringCommit = tokenCallback.token();
if (tokenCallback.errorCode() != null) {
log.info("Login failed: {} : {} (URI={})", new Object[]{tokenCallback.errorCode(), tokenCallback.errorDescription(), tokenCallback.errorUri()});
throw new LoginException(tokenCallback.errorDescription());
}
}
跟进查看处理逻辑:
org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler#handleTokenCallback
private void handleTokenCallback(OAuthBearerTokenCallback callback) throws IOException {
this.checkInitialized();
String accessToken = this.accessTokenRetriever.retrieve();
try {
OAuthBearerToken token = this.accessTokenValidator.validate(accessToken);
callback.token(token);
} catch (ValidateException var4) {
log.warn(var4.getMessage(), var4);
callback.error("invalid_token", var4.getMessage(), (String)null);
}
}
只要token内容不满足jwt结构,则会被展示在报错信息中。验证token有效性的逻辑详见:
org.apache.kafka.common.security.oauthbearer.internals.secured.LoginAccessTokenValidator#validate
public OAuthBearerToken validate(String accessToken) throws ValidateException {
SerializedJwt serializedJwt = new SerializedJwt(accessToken);
...
}
测试一下,成功读取win.ini

漏洞复现

修复方案
目前官方已有可更新版本,建议受影响用户升级至最新版本:
https://github.com/apache/kafka/releases/tag/3.9.1
产品支持
网宿全站防护-WAF模块已支持对该漏洞利用攻击的防护,并持续挖掘分析其他变种攻击方式和各类组件漏洞,第一时间上线防护规则,缩短防护"空窗期"。