rebuild代码审计
6right 发表于 浙江 漏洞分析 862浏览 · 2024-01-29 02:57

注:以下漏洞测试在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

0 条评论
某人
表情
可输入 255