OFcms代码审计
前言
今天看到一篇审计Java的文章,于是在没有继续系统学习基础的情况下,又一次当其搬运工,打算熟悉熟悉java项目搭建流程,然后再跟着过一下Java代码审计流程,全文除搭建的坑,其余漏洞代码分析均CV大佬思路。。。
环境搭建
- 源码地址:https://gitee.com/oufu/ofcms
- 环境依赖:
a. 建议采用 idea 工具开发
b. mysql 5.6+
c. jdk 1.8
d. tomcat 8 5.通过war包直接放TOMCAT下面 到附件中下载
e. phpstudy
大致安装步骤:
- 首先就是直接用idea打开下载好的源码文件
- 然后将ofcms-master\ofcms-master\ofcms-admin\src\main\resources\dev\conf目录下的db-config.properties 改成db.properties 修改数据库连接
- 修改db.properties中的数据库密码为本地设置的mysql密码
- 配置好数据库后,先用IDEA中的数据连接插件Database Navigator测试数据库连通性(插件安装步骤见:https://blog.csdn.net/Liu_wen_wen/article/details/125958039)
- 安装成功后,在视图->工具窗口中选中DB Browser,配置本地数据库用户名密码
- 连接成功后返回数据表项
- 安装Maven依赖,这个其实在你打开项目的时候,idea就自动检测了,一般等他下载完就可以了
- 这里要是下载太慢可以配置阿里加速源
- 若是使用 IDEA 自带的 maven,从 IDEA 所在目录开始:IntelliJ IDEA\plugins\maven\lib\maven3\conf\settings.xml。若是自己下载的 maven 则从 IDEA 所在目录开始:maven的安装目录\conf\settings.xml。
- 将其中的 url 进行修改。(可用 ctrl + F 进行直接搜索)
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
-
安装tomcat,社区版不自带这个,我之前是装了smart tomcat,这里直接在设置->插件中下载就行,下载好,重启idea就行,就会在设置界面中多一个tomcat server
-
配置tomcat
-
选择好之后,进行部署到ofcms-admin
-
全部完成后,就可以构建项目了,项目构建成功后,访问http://localhost:8080/ofcms-admin/即可以安装
-
这里还有几个坑,要是在本地的phpstudy中使用mysql,直接导入ofcms-V1.1.3\doc\sql\ofcms-v1.1.3.sql文件,再重启即可
-
然后刷新页面即安装成功
-
PS:构建过程中若出现junit缺少jar包,在文件->项目结构->库中点击加号,然后弹出对话框,选择Java,找到idea的安装目录lib下,选择junit的jar包,选择junit.jar,然后重新构建即可
漏洞审计复现
任意文件读取
漏洞分析
这个文件的getTemplates函数,可以看到从前台获取dir、up_dir、res_path值,直接把dir拼接到pathfile,并未对其处理,直接获取pathfile目录下的所有目录dirs和文件files,但是获取的文件后缀只能是html、xml、css、js
这里先看一下java的文件拼接
示例代码如下:
import java.io.File;
public class Main {
public static void main(String[] args) {
File file3 = new File("D:\\workspace_idea1","JavaSenior");
System.out.println(file3);
}
}
找了一个在线平台(https://www.bejson.com/runcode/java/)
运行一下后,发现File类,会直接拼接路径
public File(String parent,String child) 以parent为父路径,child为子路径创建File对象。
![](https://img2024.cnblogs.com/blog/1973814/202410/1973814-20241012111334757-1973437600.png)
源码:
public void getTemplates() {
//当前目录
String dirName = getPara("dir","");
//上级目录
String upDirName = getPara("up_dir","/");
//类型区分
String resPath = getPara("res_path");
//文件目录
String dir = null;
if(!"/".equals(upDirName)){
dir = upDirName+dirName;
}else{
dir = dirName;
}
File pathFile = null;
if("res".equals(resPath)){
pathFile = new File(SystemUtile.getSiteTemplateResourcePath(),dir);
}else {
pathFile = new File(SystemUtile.getSiteTemplatePath(),dir);
}
File[] dirs = pathFile.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if(StringUtils.isBlank (dirName)){
upDirName = upDirName.substring(upDirName.indexOf("/"),upDirName.lastIndexOf("/"));
}
setAttr("up_dir_name",upDirName);
setAttr("up_dir","".equals(dir)?"/":dir);
setAttr("dir_name",dirName.equals("")?SystemUtile.getSiteTemplatePathName():dirName);
setAttr("dirs", dirs);
/*if (dirName != null) {
pathFile = new File(pathFile, dirName);
}*/
File[] files = pathFile.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return !file.isDirectory() && (file.getName().endsWith(".html") || file.getName().endsWith(".xml")
|| file.getName().endsWith(".css") || file.getName().endsWith(".js"));
}
});
setAttr("files", files);
String fileName = getPara("file_name", "index.html");
File editFile = null;
if (fileName != null && files != null && files.length > 0) {
for (File f : files) {
if (fileName.equals(f.getName())) {
editFile = f;
break;
}
}
if (editFile == null) {
editFile = files[0];
fileName = editFile.getName();
}
}
setAttr("file_name", fileName);
if (editFile != null) {
String fileContent = FileUtils.readString(editFile);
if (fileContent != null) {
fileContent = fileContent.replace("<", "<").replace(">", ">");
setAttr("file_content", fileContent);
setAttr("file_path", editFile);
}
}
if("res".equals(resPath)) {
render("/admin/cms/template/resource.html");
}else{
render("/admin/cms/template/index.html");
}
}
进而在从前台获取file_name参数,默认为index.html,再判断files是否为空,如果不为空,循环所有文件files和file_name进行对比,有则返回该文件,无则返回所有文件files的第一个文件,最终读取该文件内容
复现
任意文件写入
漏洞分析
文件位置:
ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java
在TemplateController.java文件里的save函数,从前台主要获取file_name、file_content两个参数,可以发现该函数的file_name是直接和pathfile目录拼接上的,所以是可以路径穿越的,导致文件可以写到任意位置下
public void save() {
String resPath = getPara("res_path");
File pathFile = null;
if("res".equals(resPath)){
pathFile = new File(SystemUtile.getSiteTemplateResourcePath());
}else {
pathFile = new File(SystemUtile.getSiteTemplatePath());
}
String dirName = getPara("dirs");
if (dirName != null) {
pathFile = new File(pathFile, dirName);
}
String fileName = getPara("file_name");
// 没有用getPara原因是,getPara因为安全问题会过滤某些html元素。
String fileContent = getRequest().getParameter("file_content");
fileContent = fileContent.replace("<", "<").replace(">", ">");
File file = new File(pathFile, fileName);
FileUtils.writeString(file, fileContent);
rendSuccessJson();
}
// getParameter获取POST/GET传递的参数值
复现
在admin目录下写一个a.xml,默认会写在根目录下:
这里在写入完文件后,会发现全局也搜不到文件位置,但是又显示处理成功,那么为了能验证效果,这里要配合一下上面的任意文件读取,这也就是为什么要创建一个.xml文件的原因
模板注入(危害程度不大,仅能影响自己系统)
漏洞分析
在后台的模板管理处,可以直接修改模板语句导致模板注入
直接在index.html中插入如下语句:
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("calc") }
然后访问首页
SQL注入
漏洞分析
文件位置
ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController.java
漏洞代码:
public void create() {
try {
String sql = getPara("sql");
Db.update(sql);
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999"), e.getMessage());
}
}
这里直接获取到sql参数后,直接执行update,没有经过任何过滤
跟了一下函数实现,发现也是直接使用的executeUpdate()
int update(Config config, Connection conn, String sql, Object... paras) throws SQLException {
PreparedStatement pst = conn.prepareStatement(sql);
config.dialect.fillStatement(pst, paras);
int result = pst.executeUpdate();
DbKit.close(pst);
return result;
}
复现
payload:sql=update of_cms_ad set ad_id=updatexml(1,concat(1,user()),1)
任意文件上传
漏洞分析
文件位置:ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/ComnController.java
这个文件下的ComnController.java、UeditorAction.java文件,其中的upload、editUploadImage、uploadImage、uploadFile、uploadVideo和uploadScrawl函数都是可以进行上传的,可以看到用的都是getFile函数:
- 漏洞代码(ComnController.java)
public void upload() {
try {
UploadFile file = this.getFile("file", "image");
file.getFile().createNewFile();
Map<String, Object> data = new HashMap<String, Object>();
data.put("filePath", "/upload/image/" + file.getFileName());
data.put("fileName", file.getFileName());
rendSuccessJson(data);
} catch (Exception e) {
rendFailedJson(ErrorCode.get("9999"));
}
先看一下upload函数,其中getFile函数先获取文件和上传路径,然后创建新文件,跟一下getFile函数
public UploadFile getFile(String parameterName, String uploadPath) {
this.getFiles(uploadPath);
return this.getFile(parameterName);
}
这里又用getFiles处理上传路径,然后返回文件名,继续跟一下getFiles函数
public List<UploadFile> getFiles(String uploadPath) {
if (!(this.request instanceof MultipartRequest)) {
this.request = new MultipartRequest(this.request, uploadPath);
}
这里if判断请求类型不是MultipartRequest后会用MultipartRequest继续处理请求和上传路径,那么再继续跟一下MultipartRequest
public MultipartRequest(HttpServletRequest request, String uploadPath) {
super(request);
this.wrapMultipartRequest(request, this.getFinalPath(uploadPath), maxPostSize, encoding);
}
这里先调用了父类的构造方法,然后又调用wrapMultipartRequest处理请求和文件上传路径,继续跟一下wrapMultipartRequest
private void wrapMultipartRequest(HttpServletRequest request, String uploadPath, int maxPostSize, String encoding) {
File dir = new File(uploadPath);
if (!dir.exists() && !dir.mkdirs()) {
throw new RuntimeException("Directory " + uploadPath + " not exists and can not create directory.");
} else {
this.uploadFiles = new ArrayList();
try {
this.multipartRequest = new com.oreilly.servlet.MultipartRequest(request, uploadPath, maxPostSize, encoding, fileRenamePolicy);
Enumeration files = this.multipartRequest.getFileNames();
while(files.hasMoreElements()) {
String name = (String)files.nextElement();
String filesystemName = this.multipartRequest.getFilesystemName(name);
if (filesystemName != null) {
String originalFileName = this.multipartRequest.getOriginalFileName(name);
String contentType = this.multipartRequest.getContentType(name);
UploadFile uploadFile = new UploadFile(name, uploadPath, filesystemName, originalFileName, contentType);
if (this.isSafeFile(uploadFile)) {
this.uploadFiles.add(uploadFile);
}
}
}
} catch (ExceededSizeException var12) {
throw new com.jfinal.upload.ExceededSizeException(var12);
} catch (IOException var13) {
throw new RuntimeException(var13);
}
}
}
这里可以看到详细的文件上传时文件处理了,先是根据给出的上传路径创建目录路径,会事先判断目录是否存在或者是否可以创建,全都满足后,即可进入下一步判断文件名是否为空,最后在进入isSafeFile函数进行文件类型判断,所以这里再跟一下isSafeFile函数
private boolean isSafeFile(UploadFile uploadFile) {
String fileName = uploadFile.getFileName().trim().toLowerCase();
if (!fileName.endsWith(".jsp") && !fileName.endsWith(".jspx")) {
return true;
} else {
uploadFile.getFile().delete();
return false;
}
}
这里先将文件名转为小写,然后判断是否为jsp和jspx后缀,若是则删除文件,所以这里应该是要绕过的,但是这里我们可以利用Windows或中间件文件上传特性来避免结尾为jsp或jspx
复现
漏洞路径http://localhost:8080/ofcms-admin/admin/comn/service/upload
随意找一下一个上传路径
然后抓包
改为image/jpg,并且文件名后多加一个点,即为shell.jsp.,即可以上传成功
到文件路径下查看
本来想用冰蝎连一下,发现连不了,最后在参考文章的最后看到了这样一句话,关于上传jsp文件为什么执行不了——是因为存在jfinal过滤器,所以没有办法传上去。
XXE漏洞
漏洞分析
文件位置:ofcms-admin\src\main\java\com\ofsoft\cms\admin\controller\ReprotAction.java
其中的expReport方法
public void expReport() {
HttpServletResponse response = getResponse();// 获取响应包
Map<String, Object> hm = getParamsMap(); // 将getParamsMap返回的result Map类型赋值给Map类型的数据集hm
String jrxmlFileName = (String) hm.get("j");// 传参j(string类型)赋值给jrxmlFileName
jrxmlFileName = "/WEB-INF/jrxml/" + jrxmlFileName + ".jrxml";//拼接路径+文件名+后缀给jrxmlFileName
File file = new File(PathKit.getWebRootPath() + jrxmlFileName);//创建file实例,路径,参数给出
String fileName = (String) hm.get("reportName");//从请求中获取reportName赋值给fileName
log.info("报表文件名[{}]", file.getPath());//返回信息报表文件名路径
OutputStream out = null;
try {
DataSource dataSource = (DataSource) SysBeans
.getBean("dataSourceProxy");
JasperPrint jprint = (JasperPrint) JasperFillManager.fillReport(
JasperCompileManager
.compileReport(new FileInputStream(file)), hm,
dataSource.getConnection());
JRXlsExporter exporter = new JRXlsExporter();
response.setHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode(fileName, "utf-8") + ".xls");
response.setContentType("application/xls");
response.setCharacterEncoding("UTF-8");
JasperReportsUtils.render(exporter, jprint,
response.getOutputStream());
response.setStatus(HttpServletResponse.SC_OK);
out=response.getOutputStream();
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
renderNull();
// renderJson();
}
getParamsMap() 有传参
后边将输入的值传递给 hm 和 jrxmlFileName
服务器接收用户输入的j参数后,拼接生成文件路径,这里没有进行过滤,可以穿越到其它目录,但是限制了文件后缀为jrxml。
定位 getParamsMap()方法 没有过滤
public Map<String, Object> getParamsMap() {
Map<String, String[]> params = getParaMap();//定义一个返回map类型数据集的params参数,将获取到的请求参数的键值赋给params
Map<String, Object> result = new ConcurrentHashMap<String, Object>();//定义一个map类型数据集的result参数, 实例化方法,允许一边更新一遍遍历
for (String value : params.keySet()) {
result.put(value, params.get(value)[0]);// for 循环,获取params的全部键和值写入result map类型数据集中
}
return result; //返回result map类型的数据集
}
接下来会调用JasperCompileManager.compileReport()方法,跟进该方法看看
try {
DataSource dataSource = (DataSource) SysBeans
.getBean("dataSourceProxy");
JasperPrint jprint = (JasperPrint) JasperFillManager.fillReport(
JasperCompileManager
.compileReport(new FileInputStream(file)), hm,
dataSource.getConnection());
JRXlsExporter exporter = new JRXlsExporter();
response.setHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode(fileName, "utf-8") + ".xls");
response.setContentType("application/xls");
response.setCharacterEncoding("UTF-8");
JasperReportsUtils.render(exporter, jprint,
response.getOutputStream());
response.setStatus(HttpServletResponse.SC_OK);
out=response.getOutputStream();
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
在compileReport方法中又调用了JRXmlLoader.load()方法,继续跟踪
复现
复现锤子啊,这个xxe貌似是旧版本的,新版本的compileReport函数已经被修复了。
小结
整体上说算是完整的搭建和跟了一遍整体代码,发现对很多java的基本函数了解程度还是不够,需要继续进一步学习,其中有部分漏洞算是自己重新分析了一下,其他的就是跟这个大佬的文章复现的。这个cms应该还是有很多洞的,等把炼石计划的java基础学完,再继续重新分析一波吧,尽请期待OFcms代码审计(二)!!!无限期拖更!!!