注:以下漏洞测试在rebuild历史版本,且都已提交CNNVD并通过
鉴权分析
com.rebuild.web.RebuildWebInterceptor#preHandle
if (!Application.isReady()) {
final boolean isError = requestUri.endsWith("/error") || requestUri.contains("/error/");
// 已安装
if (checkInstalled()) {
log.error("Server Unavailable : " + requestEntry);
if (isError) {
return true;
} else {
sendRedirect(response, "/error/server-status", null);
return false;
}
}
// 未安装
else if (!(requestUri.contains("/setup/") || requestUri.contains("/commons/theme/") || isError)) {
sendRedirect(response, "/setup/install", null);
return false;
} else {
return true;
}
}
首先会有一个安装文件
- 安装文件位置 ~/.rebuild/.rebuild
com.rebuild.core.support.setup.InstallState#checkInstalled进行检查
default boolean checkInstalled() {
return Application.devMode() || getInstallFile().exists();
}
前面的选项通过配置检查是否dev环境,默认不是,后面检查.rebuild文件是否存在,所以如果存在任意文件删除,删除掉.rebuild文件就会存在jdbc attack
在接下去看没有授权信息时的处理
} else if (!isIgnoreAuth(requestUri)) {
// 独立验证逻辑
if (requestUri.contains("/filex/")) return true;
log.warn("Unauthorized access {}", RebuildWebConfigurer.getRequestUrls(request));
if (isHtmlRequest(requestUri, request)) {
sendRedirect(response, "/user/login", requestEntry.getRequestUriWithQuery());
} else {
response.sendError(HttpStatus.UNAUTHORIZED.value());
}
return false;
} else {
skipCheckSafeUse = true;
}
看com.rebuild.web.RebuildWebInterceptor#isIgnoreAuth
private boolean isIgnoreAuth(String requestUri) {
if (requestUri.contains("/user/") && !requestUri.contains("/user/admin")) {
return true;
}
requestUri = requestUri.replaceFirst(AppUtils.getContextPath(), "");
return requestUri.length() < 3
|| requestUri.endsWith("/error") || requestUri.contains("/error/")
|| requestUri.endsWith("/logout")
|| requestUri.startsWith("/f/")
|| requestUri.startsWith("/s/")
|| requestUri.startsWith("/gw/")
|| requestUri.startsWith("/setup/")
|| requestUri.startsWith("/language/")
|| requestUri.startsWith("/filex/access/")
|| requestUri.startsWith("/filex/download/")
|| requestUri.startsWith("/filex/img/")
|| requestUri.startsWith("/commons/announcements")
|| requestUri.startsWith("/commons/url-safe")
|| requestUri.startsWith("/commons/barcode/render")
|| requestUri.startsWith("/commons/theme/")
|| requestUri.startsWith("/account/user-avatar/")
|| requestUri.startsWith("/rbmob/env");
}
当返回为ture时不需要授权 也就是
- 1:满足isIgnoreAuth函数return中任意条件可以跳过授权
- 2:url中存在/user/但不存在/user/admin
- 3:url中包含/filex/
注:且controller函数体中不能存在getRequestUser
根据以上代码获取到未授权接口再进行测试,发现以下未授权漏洞
未授权敏感信息泄露
代码分析
漏洞点:com.rebuild.web.commons.FileDownloader#readRaw
@GetMapping(value = "read-raw")
public void readRaw(HttpServletRequest request, HttpServletResponse response) throws IOException {
String filePath = getParameterNotNull(request, "url");
boolean fullUrl = CommonsUtils.isExternalUrl(filePath);
final String charset = getParameter(request, "charset", AppUtils.UTF8);
final int cut = getIntParameter(request, "cut"); // MB
String content;
if (QiniuCloud.instance().available()) {
FileInfo fi = QiniuCloud.instance().stat(filePath);
if (fi == null) {
content = "ERROR:FILE_NOT_EXISTS";
} else if (cut > 0 && fi.fsize / 1024 / 1024 > cut) {
content = "ERROR:FILE_TOO_LARGE";
} else {
String privateUrl = fullUrl ? filePath : QiniuCloud.instance().makeUrl(filePath);
content = OkHttpUtils.get(privateUrl, null, charset);
}
} else {
if (fullUrl) {
String e = filePath.split("\\?e=")[1];
RbAssert.is(checkEsign(e), "Unauthorized access");
filePath = filePath.split("/filex/access/")[1].split("\\?")[0];
}
// Local storage
filePath = checkFilePath(filePath);
File file = RebuildConfiguration.getFileOfData(filePath);
if (!file.exists()) {
content = "ERROR:FILE_NOT_EXISTS";
} else if (cut > 0 && FileUtils.sizeOf(file) / 1024 / 1024 > cut) {
content = "ERROR:FILE_TOO_LARGE";
} else {
content = FileUtils.readFileToString(file, charset);
}
}
ServletUtils.write(response, content);
}
存在限制条件
限制了目录穿越和一些目录:com.rebuild.web.commons.FileDownloader#checkFilePath
但是能够直接读取.rebuild中的环境变量
其中数据库密码由aes加密,当开发者未指定配置aeskey时
com.rebuild.utils.AES#getPassKey使用默认key:REBUILD2018
可以用com.rebuild.utils.AES#main进行解密
得到解密结果
也就是未授权限制性文件读取->未授权的敏感信息获取
未授权sql注入
代码分析
漏洞点:com.rebuild.web.commons.FileShareController#delShareFile
@PostMapping("/filex/del-make-share")
public RespBody delShareFile(@IdParam ID shortId) {
Application.getCommonsService().delete(shortId, false);
return RespBody.ok();
}
存在限制条件
将payload分为三部分
1:实体id3位,需要为int类型且真实存在才不会走到异常(这里可以使用爆破001-999)即可探测
2:-符号1位
3:字符串16位,因为限制了整个payload为20位,所以可以操作的字符串只有16位
其中2,3部分的限制逻辑在:cn.devezhao.persist4j.engine.ID#isId
public static boolean isId(Object id) {
if (id instanceof ID) {
return true;
} else if (id != null && !StringUtils.isEmpty(id.toString()) && id.toString().length() == idLength) {
return id.toString().charAt(3) == '-';
} else {
return false;
}
}
idLength默认为20
1部分的限制逻辑有两处
第一处在:cn.devezhao.persist4j.engine.ID#valueOf
public static ID valueOf(String id) {
if (!isId(id)) {
throw new IllegalArgumentException("Invaild id character: " + id);
} else {
return new ID(id);
}
}
限制了id类型
第二处在:com.rebuild.core.service.CommonsService#tryIfHasPrivileges
private void tryIfHasPrivileges(Object idOrRecord) throws PrivilegesException {
Entity entity;
if (idOrRecord instanceof ID) {
entity = MetadataHelper.getEntity(((ID) idOrRecord).getEntityCode());
} else if (idOrRecord instanceof Record) {
entity = ((Record) idOrRecord).getEntity();
} else {
throw new RebuildException("Invalid argument [idOrRecord] : " + idOrRecord);
}
// 验证主实体
if (entity.getMainEntity() != null) {
entity = entity.getMainEntity();
}
if (MetadataHelper.hasPrivilegesField(entity)) {
throw new PrivilegesException("Privileges/Business entity cannot use this class (methods) : " + entity.getName());
}
}
getEntityCode就是获取前三位实体id
然后步入com.rebuild.core.metadata.MetadataHelper#getEntity(int)
public static Entity getEntity(int entityCode) throws MissingMetaExcetion {
try {
return getMetadataFactory().getEntity(entityCode);
} catch (MissingMetaExcetion ex) {
throw new MissingMetaExcetion(Language.L("实体 [%s] 已经不存在,请检查配置", entityCode));
}
}
如果实体不存在就会走入异常
在运行过程中其实在存在很多实体,所以使用低位实体id都是可以成功
根据以上代码分析结果,虽然存在长度限制,但是因为是delete操作,依旧可以用恶意payload导致拒绝服务
- id=001-aaaaaaaa'or+1=1%23