Fork me on GitHub

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("<", "&lt;").replace(">", "&gt;");
            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("&lt;", "<").replace("&gt;", ">");
    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代码审计(二)!!!无限期拖更!!!

posted @ 2024-10-12 11:18  Konmu  阅读(41)  评论(0编辑  收藏  举报