0x00 写在开头
国庆假期开了个新副本,学习了下java的语法和java中owasp top 10漏洞代码产生,本文审计项目是《java代码审计实战》实战的一个项目拿来练练手,初窥代码审计照葫芦画瓢跟着思路分析一下,刚上手java代码审计,文章内容有描述不严谨的地方师傅们多多指教。
0x01 环境搭建
ofcms是一个java 版CMS系统、基于java技术研发的内容管理系统、功能:栏目模板自定义、内容模型自定义、多个站点管理、在线模板页面编辑等功能、代码完全开源,技术选型:jfinal DB+Record mysql freemarker Encache spring 等 layui zTree bootstrap。
源代码下载:
https://gitee.com/oufu/ofcms/tree/V1.1.2/
下载之后idea打开目录,idea会自动解析所需要的包,还需要下载一个tomcat环境包。
tomcat下载
https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.68/bin/apache-tomcat-9.0.68-windows-x64.zip
文件下载好之后配置运行环境,在idea里面配置好tomcat运行配置。
部署配置
配置好之后再修改数据库配置文件,ctrl + shift + f全局搜索jdbc.username关键词可以找到数据库配置文件。
修改成数据库的对应配置,数据库我这里用的vm vctl起的一个环境,vctl类似docker但是比docker难用,也可以用phpstudy起方便一些。
vctl system start // 启动vctl
vctl pull mysql:5.7 // 拉取镜像
// 运行服务 注意这里要以管理员用户运行,因为这里我做了目录持久化挂载
vctl run -p 3306:3306 --name mysql $PWD/conf:/etc/mysql -v $PWD/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
// 进入容器操作
vctl exec -it mysql /bin/bash
// mysql 语句执行 确保外面可以连接
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;
数据库创建
create database ofcms charset utf8;
use ofcms;
数据库文件导入,数据库文件在doc/sql/ofcms-v1.2.2.sql下,可以通过命令行导入也可以直接用navicat导入。
运行环境
登陆到后台确定环境运行正常
0x02 漏洞审计
后台大概看了下功能模块
-
代码生成
-
模板编辑
-
通知管理
-
留言管理
文件读取漏洞
漏洞位置:后台=>模板设置=>模板文件
根据网站路由关键词全局搜索所在代码行getTemplates
定位到代码在TemplateController文件28行
双击阅读代码,看代码的传参和功能实现逻辑,下面两个关键词方法是需要关注的点,getPare方法获取请求中的内容赋值传递给对应的方法。
-
传参 getPara方法
-
赋值 setattr 方法
搜索getPara关键词,获取到以下参数
String dirName = getPara("dir","");
//上级目录
String upDirName = getPara("up_dir","/");
//类型区分
String resPath = getPara("res_path");
String fileName = getPara("file_name", "index.html");
根据网站后台抓包确定在模板目录传参使用了dir、up_dir
成功定位到传递的参数内容,跟进dirName使用位置
if(!"/".equals(upDirName)){
dir = upDirName+dirName;
}else{
dir = dirName;
}
这里使用了一个路径拼接,首先做了一个判断如果upDirName也就是前端传入参数up_dir不等于"/"那么执行下面的路径拼接,如果没有传入up_dir参数,那么默认值"/"就会走到41行。
跟进dir传递处理后传递给了pathFile
这里做了一个过滤,在文件列表显示时候只获取.html .xml .js文件后缀并返回到files文件数组。
74行setAttr files数组为files参数,77行做了一个文件名判断不为空这里75行这里默认值是index.html,files数组不为空还有files数组长度不为空,进入78行for循环,如果fileName等于f.getName那么就确定了需要编辑的文件赋值给editFile,91行做了一个editFile是否为空,然后读取文件setAttr返回给前端页面,大概代码逻辑图
图-文件读取漏洞代码逻辑-1
通过上面的代码流程逻辑的分析,dir、file_name没有做任何的过滤处理传递到后端传递到92行FileUtils.readString方法读取文件。
尝试构造路径穿越读取上级路径和文件,但是这里因为限制了读取文件的类型,所以利用的点很有限,但是读取路径信息是没有问题的。
通过burp构造数据包成功获取到tomcat路径信息
传递file_name参数获取到对应文件内容,但是在读取白名单以外的文件后缀就没办法了,图-文件读取漏洞代码逻辑-1 79行代码起了作用。
获取文件内容失败
文件写入漏洞
模板编辑在保存的时候有写入文件的操作,跟进对应路由admin/cms/template/save.json
完整代码如下
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参数,刚才在浏览器抓包的时候也可以看到通过POST请求传递了以下内容
file_path=D:apache-tomcat-9.0.68webappsofcms_admin_warWEB-INFpagedefaultindex.html&dirs=/&res_path=&file_name=index.html&file_content=<#assign+column_name='/'/>
内容我截取了部分内容,通过分析代码file_path、dirs、file_name、file_content都没有经过过滤,可以通过路径穿越的方式向上级指定目录写任意文件,
两种利用方式
利用方式1
通过dirs控制写的路径
利用方式1
通过file_name控制写的路径
可以写入jsp到对应的静态文件路径达到getshell目的
模板注入漏洞
漏洞位置:后台=>模板设置=>模板文件
编辑模板文件,在index.html头部加上以下内容
<
命令执行成功
任意文件上传
文件上传功能都是使用的com.jfinal.upload.UploadFile类
漏洞位置:
漏洞位置1 后台=>运营管理=>友情链接=>编辑上传图片
漏洞位置2 后台=>内容管理=>文章管理=>编辑上传图片
任意文件上传的位置有多处,jsp可以上传但是存在一个问题,只有目录包含了static/的路径才可以解析,没办法控制上传文件的路径。
漏洞位置1
ComnController.java文件101行upload、editUploadImage方法
public void upload() {
try {
UploadFile file = this.getFile("file", "image");
file.getFile().createNewFile();
Map<String, Object> data = new HashMap<String, Object>();
data.put("filePath", "/upload/image/" + file.getFileName());
data.put("fileName", file.getFileName());
rendSuccessJson(data);
} catch (Exception e) {
rendFailedJson(ErrorCode.get("9999"));
}
}
public void editUploadImage() {
try {
UploadFile file = this.getFile("file", "image");
file.getFile().createNewFile();
Map<String, Object> msg = new HashMap<String, Object>();
Map<String, Object> data = new HashMap<String, Object>();
data.put("src", SystemUtile.getParam("http_image_url")
+ "/upload/image/" + file.getFileName());
data.put("title", file.getFileName());
msg.put("data", data);
msg.put("code", 0);
msg.put("msg", "处理成功");
rendJson(msg);
} catch (Exception e) {
rendFailedJson(ErrorCode.get("9999"));
}
}
jfinal-3.2自带的一个文件后缀过滤
用windows特性绕过上传文件成功
漏洞位置2
UeditorAction.java文件uploadImage、uploadFile、uploadVideo、uploadScrawl方法都可以上传jsp文件,同样目录不可控无法解析。
示例
sql注入
漏洞位置:后台=>系统设置=>代码生成=>增加表
随便输入了一个字符点击新增,出现sql报错信息
根据路由system/generate定位到对应代码
ctrl + b根据传参跟进到48行代码
跟进到Db.java文件252行
继续跟进一步一步跟进到Db.pro.java文件282行,代码如下
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;
}
update传参sql到conn.prepareStatement(sql),创建了一个pst PreparedStatement对象,pst.executeUpdate()执行执行了传入的sql语句,但是这里sql语句是update语句才会生效,构造一个update报错语句测试。
成功报错获取到数据库名,使用sqlmap构造数据包,使用python .sqlmap.py -r 23.txt --batch,ad_id=*标记注入位置。
POST /ofcms_admin_war/admin/system/generate/create.json?sqlid= HTTP/1.1
Host: localhost:8080
Content-Length: 62
sec-ch-ua: "Chromium";v="105", "Not)A;Brand";v="8"
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/105.0.5195.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8080/ofcms_admin_war/admin/f.html?p=system/generate/add.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=9D780D7FDD8ED8688D718D3A37146EF9; Webstorm-ab1afb59=f884fe2b-ed88-4d51-8544-41e503afa445; JSESSIONID=4E086CA062A4826D64252774316AAE49; XSRF-TOKEN=4802db00-a6e2-46fa-87ac-5e29f6f65d5b; remember-me=YWRtaW46MTY2NzM5MzczMjA3NTozYzJmODhlYWE1YmI3ODBkZmVjNjI1N2EzZWM3YjhkNQ
Connection: close
sql=update of_cms_ad set ad_id=*
成功利用sqlmap获取数据库名、表名
0x03 总结
第一次审计跟着文章还有书照葫芦画瓢,对框架审计有个大概的了解,正向开发能力还是不够,遇到不知道的函数方法只有跟着百度和debug代码输出内容还有作用
0x04 参考文章
《网络安全 java代码审计实战》书籍实战篇
酒仙桥六号部队文章
https://www.secpulse.com/archives/185233.html
java 基础知识
https://blog.csdn.net/weixin_42504649/article/details/114615038
Java SSTI freemarker
https://www.cnblogs.com/CoLo/p/16706091.html
ofcms审计扩展
https://sec-in.com/article/281
jfinal
https://blog.csdn.net/nfgch/article/details/106733234
往期回顾
·END·
原文始发于微信公众号(泛星安全团队):初窥JAVA代码审计
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论