文章总结: 本文对newbee-mall开源电商项目进行Java代码审计,分析了鉴权机制及历史越权修复。重点挖掘支付逻辑漏洞,包括负数商品导致的低价购及参数操纵导致的零元购。同时披露了后台任意文件上传与编辑器存储型XSS漏洞,并辟谣CVE-2024-48178实为CSRF登出风险。文末包含大量培训广告。 综合评分: 72 文章分类: 代码审计,WEB安全,漏洞分析,漏洞POC,软文广告
历史漏洞之水平越权
1.在2022年2月之前的版本中,在ltd.newbee.mall.service.impl.NewBeeMallUserServiceImpl下的updateUserInfo函数中,是这么写的:
@Overridepublic NewBeeMallUserVO updateUserInfo(MallUser mallUser, HttpSession httpSession) { NewBeeMallUserVO userTemp = (NewBeeMallUserVO) httpSession.getAttribute(Constants.MALL_USER_SESSION_KEY); MallUser userFromDB = mallUserMapper.selectByPrimaryKey(userTemp.getUserId()); if (userFromDB != null) { if (StringUtils.hasText(mallUser.getNickName())) { userFromDB.setNickName(NewBeeMallUtils.cleanString(mallUser.getNickName())); } if (StringUtils.hasText(mallUser.getAddress())) { userFromDB.setAddress(NewBeeMallUtils.cleanString(mallUser.getAddress())); } if (StringUtils.hasText(mallUser.getIntroduceSign())) { userFromDB.setIntroduceSign(NewBeeMallUtils.cleanString(mallUser.getIntroduceSign())); } if (mallUserMapper.updateByPrimaryKeySelective(userFromDB) > 0) { NewBeeMallUserVO newBeeMallUserVO = new NewBeeMallUserVO(); userFromDB = mallUserMapper.selectByPrimaryKey(mallUser.getUserId()); BeanUtil.copyProperties(userFromDB, newBeeMallUserVO); httpSession.setAttribute(Constants.MALL_USER_SESSION_KEY, newBeeMallUserVO); return newBeeMallUserVO; } } return null;}
问题就出现在17行的userFromDB = mallUserMapper.selectByPrimaryKey(mallUser.getUserId());这里根据传入的mallUser的Id进行修改,而mallUser的Id我们是可控的,可以跟代码看一下:
如图,我们可以通过传入mallUser这个类中的Id,来越权修改别人的信息。
2.那么作者是如何修复改漏洞的呢,很简单,就是直接把第17行删了。为什么可以这样呢?因为在前两行:
NewBeeMallUserVO userTemp%20=%20(NewBeeMallUserVO)%20httpSession.getAttribute(Constants.MALL_USER_SESSION_KEY);MallUser userFromDB%20=%20mallUserMapper.selectByPrimaryKey(userTemp.getUserId());
这里的userFromDB是根据登录用户的session来判断的,因此只能修改自己的信息。也就是说,其实作者之前写的那行代码不仅多余,而且还造成了越权漏洞。
支付漏洞
低价购
1.在\src\main\java\ltd\newbee\mall\controller\mall\ShoppingCartController.java中有如下代码:
@GetMapping("/shop-cart/settle")public String settlePage(HttpServletRequest request, %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 HttpSession httpSession)%20{ %20 %20int%20priceTotal%20= 0; %20 NewBeeMallUserVO user%20=%20(NewBeeMallUserVO)%20httpSession.getAttribute(Constants.MALL_USER_SESSION_KEY); %20 List<NewBeeMallShoppingCartItemVO>%20myShoppingCartItems%20=%20newBeeMallShoppingCartService.getMyShoppingCartItems(user.getUserId()); %20 if (CollectionUtils.isEmpty(myShoppingCartItems))%20{ %20 %20 %20 //无数据则不跳转至结算页 %20 %20 %20 return "/shop-cart"; %20 %20} else { %20 %20 %20 //总价 %20 %20 %20 for (NewBeeMallShoppingCartItemVO newBeeMallShoppingCartItemVO%20:%20myShoppingCartItems)%20{ %20 %20 %20 %20 %20 %20priceTotal%20+=%20newBeeMallShoppingCartItemVO.getGoodsCount()%20*%20newBeeMallShoppingCartItemVO.getSellingPrice(); %20 %20 %20 %20} %20 %20 %20 if (priceTotal%20< 1)%20{ %20 %20 %20 %20 %20 NewBeeMallException.fail("购物项价格异常"); %20 %20 %20 %20} %20 %20} %20 %20request.setAttribute("priceTotal",%20priceTotal); %20 %20request.setAttribute("myShoppingCartItems",%20myShoppingCartItems); %20 return "mall/order-settle";}
漏洞出现在如下代码中:
这里作者值判断了priceTotal,即总的价格大于1,并未判断是否每件价格都大于1,并且未规定购买数量不能为负数,因此我们可以购买一些负数的低价商品,再购买一件高价商品,实现低价购买,如图为加入购物车的数据包:
POST%20/shop-cart%20HTTP/1.1Host:%20127.0.0.1:28089Content-Length:%2033sec-ch-ua-platform:%20"Windows"X-Requested-With:%20XMLHttpRequestUser-Agent:%20Mozilla/5.0%20(Windows%20NT%2010.0;%20Win64;%20x64)%20AppleWebKit/537.36%20(KHTML,%20like%20Gecko)%20Chrome/143.0.0.0%20Safari/537.36Accept:%20*/*sec-ch-ua:%20"Google%20Chrome";v="143",%20"Chromium";v="143",%20"Not%20A(Brand";v="24"Content-Type:%20application/jsonsec-ch-ua-mobile:%20?0Origin:%20http://127.0.0.1:28089Sec-Fetch-Site:%20same-originSec-Fetch-Mode:%20corsSec-Fetch-Dest:%20emptyReferer:%20http://127.0.0.1:28089/goods/detail/10195Accept-Encoding:%20gzip,%20deflate,%20brAccept-Language:%20zh-CN,zh;q=0.9Cookie:%20JSESSIONID=5E4F79C3A28A36AADEDEB26CAB069CF4Connection:%20keep-alive{"goodsId":10195,"goodsCount":-4}
订单提交成功:
零元购
1.漏洞代码的路由在src\main\java\ltd\newbee\mall\controller\mall\OrderController.java中:
跟进一下paySuccess方法:
这里是说newBeeMallOrderMapper.updateByPrimaryKeySelective(newBeeMallOrder)的值大于0就可以判定为支付成功,但问题i就是,payType是我们传入的参数,是可控的呀,我们直接修改payType的值就可以让newBeeMallOrderMapper的值修改,导致if语句判断成功,实现0元购。
如图,成功支付。
后台任意文件上传
1.上传代码在src\main\java\ltd\newbee\mall\controller\common\UploadController.java中:
@Controller@RequestMapping("/admin")public class UploadController { %20 @Autowired %20 private StandardServletMultipartResolver%20standardServletMultipartResolver; %20 @PostMapping({"/upload/file"}) %20 @ResponseBody %20 public Result upload(HttpServletRequest%20httpServletRequest, @RequestParam("file") MultipartFile%20file) throws URISyntaxException,%20IOException%20{ %20 %20 %20 String fileName = file.getOriginalFilename(); %20 %20 %20 BufferedImage bufferedImage = ImageIO.read(file.getInputStream()); %20 %20 %20 if (bufferedImage%20== null)%20{ %20 %20 %20 %20 %20 return ResultGenerator.genFailResult("请上传图片类型的文件"); %20 %20 %20 %20} %20 %20 %20 String suffixName = fileName.substring(fileName.lastIndexOf(".")); %20 %20 %20 //生成文件名称通用方法 %20 %20 %20 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss"); %20 %20 %20 Random r = new Random(); %20 %20 %20 StringBuilder tempName = new StringBuilder(); %20 %20 %20 %20tempName.append(sdf.format(new Date())).append(r.nextInt(100)).append(suffixName); %20 %20 %20 String newFileName = tempName.toString(); %20 %20 %20 File fileDirectory = new File(Constants.FILE_UPLOAD_DIC); %20 %20 %20 //创建文件 %20 %20 %20 File destFile = new File(Constants.FILE_UPLOAD_DIC%20+%20newFileName); %20 %20 %20 try { %20 %20 %20 %20 %20 if (!fileDirectory.exists())%20{ %20 %20 %20 %20 %20 %20 %20 if (!fileDirectory.mkdir())%20{ %20 %20 %20 %20 %20 %20 %20 %20 %20 throw new IOException("文件夹创建失败,路径为:" +%20fileDirectory); %20 %20 %20 %20 %20 %20 %20 %20} %20 %20 %20 %20 %20 %20} %20 %20 %20 %20 %20 %20file.transferTo(destFile); %20 %20 %20 %20 %20 Result resultSuccess = ResultGenerator.genSuccessResult(); %20 %20 %20 %20 %20 %20resultSuccess.setData(NewBeeMallUtils.getHost(new URI(httpServletRequest.getRequestURL()%20+ ""))%20+ "/upload/" +%20newFileName); %20 %20 %20 %20 %20 return resultSuccess; %20 %20 %20 %20} catch (IOException%20e)%20{ %20 %20 %20 %20 %20 %20e.printStackTrace(); %20 %20 %20 %20 %20 return ResultGenerator.genFailResult("文件上传失败"); %20 %20 %20 %20} %20 %20} %20 @PostMapping({"/upload/files"}) %20 @ResponseBody %20 public Result uploadV2(HttpServletRequest%20httpServletRequest) throws URISyntaxException,%20IOException%20{ %20 %20 %20 %20List<MultipartFile>%20multipartFiles%20= new ArrayList<>(8); %20 %20 %20 if (standardServletMultipartResolver.isMultipart(httpServletRequest))%20{ %20 %20 %20 %20 %20 MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest)%20httpServletRequest; %20 %20 %20 %20 %20 %20Iterator<String>%20iter%20=%20multiRequest.getFileNames(); %20 %20 %20 %20 %20 int total = 0; %20 %20 %20 %20 %20 while (iter.hasNext())%20{ %20 %20 %20 %20 %20 %20 %20 if (total%20> 5)%20{ %20 %20 %20 %20 %20 %20 %20 %20 %20 return ResultGenerator.genFailResult("最多上传5张图片"); %20 %20 %20 %20 %20 %20 %20 %20} %20 %20 %20 %20 %20 %20 %20 %20total%20+= 1; %20 %20 %20 %20 %20 %20 %20 MultipartFile file = multiRequest.getFile(iter.next()); %20 %20 %20 %20 %20 %20 %20 BufferedImage bufferedImage = ImageIO.read(file.getInputStream()); %20 %20 %20 %20 %20 %20 %20 //%20只处理图片类型的文件 %20 %20 %20 %20 %20 %20 %20 if (bufferedImage%20!= null)%20{ %20 %20 %20 %20 %20 %20 %20 %20 %20 %20multipartFiles.add(file); %20 %20 %20 %20 %20 %20 %20 %20} %20 %20 %20 %20 %20 %20} %20 %20 %20 %20} %20 %20 %20 if (CollectionUtils.isEmpty(multipartFiles))%20{ %20 %20 %20 %20 %20 return ResultGenerator.genFailResult("请选择图片类型的文件上传"); %20 %20 %20 %20} %20 %20 %20 if (multipartFiles%20!= null &&%20multipartFiles.size()%20> 5)%20{ %20 %20 %20 %20 %20 return ResultGenerator.genFailResult("最多上传5张图片"); %20 %20 %20 %20} %20 %20 %20 %20List<String>%20fileNames%20= new ArrayList(multipartFiles.size()); %20 %20 %20 for (int i = 0;%20i%20<%20multipartFiles.size();%20i++)%20{ %20 %20 %20 %20 %20 String fileName = multipartFiles.get(i).getOriginalFilename(); %20 %20 %20 %20 %20 String suffixName = fileName.substring(fileName.lastIndexOf(".")); %20 %20 %20 %20 %20 //生成文件名称通用方法 %20 %20 %20 %20 %20 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss"); %20 %20 %20 %20 %20 Random r = new Random(); %20 %20 %20 %20 %20 StringBuilder tempName = new StringBuilder(); %20 %20 %20 %20 %20 %20tempName.append(sdf.format(new Date())).append(r.nextInt(100)).append(suffixName); %20 %20 %20 %20 %20 String newFileName = tempName.toString(); %20 %20 %20 %20 %20 File fileDirectory = new File(Constants.FILE_UPLOAD_DIC); %20 %20 %20 %20 %20 //创建文件 %20 %20 %20 %20 %20 File destFile = new File(Constants.FILE_UPLOAD_DIC%20+%20newFileName); %20 %20 %20 %20 %20 try { %20 %20 %20 %20 %20 %20 %20 if (!fileDirectory.exists())%20{ %20 %20 %20 %20 %20 %20 %20 %20 %20 if (!fileDirectory.mkdir())%20{ %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 throw new IOException("文件夹创建失败,路径为:" +%20fileDirectory); %20 %20 %20 %20 %20 %20 %20 %20 %20 %20} %20 %20 %20 %20 %20 %20 %20 %20} %20 %20 %20 %20 %20 %20 %20 %20multipartFiles.get(i).transferTo(destFile); %20 %20 %20 %20 %20 %20 %20 %20fileNames.add(NewBeeMallUtils.getHost(new URI(httpServletRequest.getRequestURL()%20+ ""))%20+ "/upload/" +%20newFileName); %20 %20 %20 %20 %20 %20} catch (IOException%20e)%20{ %20 %20 %20 %20 %20 %20 %20 %20e.printStackTrace(); %20 %20 %20 %20 %20 %20 %20 return ResultGenerator.genFailResult("文件上传失败"); %20 %20 %20 %20 %20 %20} %20 %20 %20 %20} %20 %20 %20 Result resultSuccess = ResultGenerator.genSuccessResult(); %20 %20 %20 %20resultSuccess.setData(fileNames); %20 %20 %20 return resultSuccess; %20 %20}}
可以看到,作者通过ImageIO.read来判断传入的是否为图片,但是作者没有考虑到,我们可以先传入图片,再在图片的尾部添加恶意代码,也就是所谓的“图片马”。
2.我们先找到一处文件上传点,如图:
数据包如下:(图片内容用xxx代替)
POST%20/admin/upload/file%20HTTP/1.1Host:%20localhost:28089Content-Length:%2013342Cache-Control:%20max-age=0sec-ch-ua: "Google%20Chrome";v="143", "Chromium";v="143", "Not%20A(Brand";v="24"sec-ch-ua-mobile:%20?0sec-ch-ua-platform: "Windows"Origin:%20http://localhost:28089Content-Type:%20multipart/form-data;%20boundary=----WebKitFormBoundaryRII4LixNAimwdMeHUpgrade-Insecure-Requests:%201User-Agent:%20Mozilla/5.0%20(Windows%20NT%2010.0;%20Win64;%20x64)%20AppleWebKit/537.36%20(KHTML,%20like%20Gecko)%20Chrome/143.0.0.0%20Safari/537.36Accept:%20text/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.7Sec-Fetch-Site:%20same-originSec-Fetch-Mode:%20navigateSec-Fetch-Dest:%20iframeReferer:%20http://localhost:28089/admin/goods/editAccept-Encoding:%20gzip,%20deflate,%20brAccept-Language:%20zh-CN,zh;q=0.9Cookie:%20JSESSIONID=0779CECA6C110A0E83FB037F5AB3B99AConnection:%20keep-alive------WebKitFormBoundaryRII4LixNAimwdMeHContent-Disposition:%20form-data;%20name="file";%20filename="test.jpg"Content-Type: image/jpegxxx------WebKitFormBoundaryRII4LixNAimwdMeH--
当用户在点入test5商品时,就会触发xss:
CVE-2024-48178
1.该cve的发现者将分析写在了github上:https://github.com/dabaizhizhu/123/issues/10 但是我分析过后,发现该cve并不存在,即不存在ssrf漏洞。
如图,按照cve发现者的意思,我们将img改为dnslog地址,并上架商品,我们登录普通用户进行访问。触发的dnslog如下:
很明显这里并不是服务端发起的请求,而是用户通过浏览器发出的请求,因此肯定不是ssrf漏洞。我们通过调试也能发现:
我们在src\main\java\ltd\newbee\mall\controller\mall\GoodsController.java打一个断点进行调试。
当调试到如图,也就是spring内部发出的一条请求后,触发了dnslog:
也就是说,spring发送给浏览器渲染,然后浏览器访问了这个url,触发了dnslog:
但是话又说回来,这个地方不存在ssrf,但是用户能请求到,是否也是有问题的呢?答案是肯定的。我们可以结合网站的csrf漏洞,进行结合利用。经过测试发现,代码中没有对csrf的防护,且http://localhost:28089/logout这个退出url是存在csrf漏洞的,因此我们可以将商品的url改成http://localhost:28089/logout:
此时,只要别人访问了我们的商品,就会自动退出。
总结
实践出真知,不能一昧的相信网上的文章(甚至连cve都有可能是错的),自己调试分析才是硬道理。
最后
有其它问题或者对文章内容有疑问的,可以加作者微信,加交流群的话备注”交流群”
公众号培训广告,有需要可联系(混口饭吃)
新一期课程介绍:菜狗安全《代码审计培训》2026年新征程
JAVA审计闭源专题课表
内容对比上一期会更细点,并且主要围绕企业闭源项目,如果说上一期是CVE随便刷,那么这一轮就是奔着CNVD,CNNVD去了
课程内容涵盖
1、基础篇
-
通用学习路线与指南
-
这个会作为我们新一期课程的第一讲,主要是为师傅们梳理学习的思路,建立一套“以业务为核心,架构为先导”的底层审计逻辑,从而具备审任何语言都能快速上手的通杀能力
-
漏洞的本质
-
审计审计,我们审计的主要是漏洞,那么对于漏洞的理解就显得尤为重要,从漏洞产生的本质出发,理解漏洞,吃透漏洞,会大大提升对于不同类型漏洞的审计关注点,大大提升审计效率
-
闭源项目常见架构审计差异与反编译
-
讲解常见闭源项目的不同架构差异,以及源码到手后的准备工作
-
不同架构项目路由传参以及鉴权装配模式
-
讲解常见闭源项目的不同架构路由配置以及接口构造与参数传递,以及框架特性导致的鉴权装配模式差异
-
JAVAweb开发
-
这一块内容是分割在每节漏洞审计的前置部分,不同于市面上的常规开发课程,通过实际业务分析漏洞产生原因,在实战案例前就学会如何挖漏洞!!!
2、鉴权分析
-
讲解java项目中常见的鉴权方法
-
不同鉴权方式的路由白名单配置
-
以及实战如何绕过鉴权
-
实战情况中存在鉴权绕过的缺陷逻辑
3、常见漏洞审计
-
SQL注入挖掘
-
文件操作类挖掘
-
组件安全审计
-
SSRF审计
-
XXE审计
-
RCE审计篇
-
越权类挖掘
-
……
4、审计番外内容
- ……
这个第一期上过挺多次了,主要还是根据学员的需求来加的
5、企业源码审计实战
选取国内某OA,某ERP,某管理系统,等企业级源码,甚至是我实战中遇到的企业泄露源码(脱敏处理后的),从反编译->项目架构分析->鉴权分析->漏洞审计的完整流程,并且每节都有0day挖掘案例,手把手教学,避免一看就会,一上手就废。
这个模块第一期上了17次直播,也是给学员听爽了,也是修改为常驻,慢慢上
秉承节节有0Day,节节挖0Day,从源码解压到漏洞审计完整流程,现场手把手教学审0day
学员内部福利
1、国内几个源码站会员:9k9k,刀客,小蚂蚁,优选源码,源码庄,狗凯等,如果有需要其他源码站会员可联系我,我看情况开通
2、内部代码审计文档库加配套对应源码(持续更新中)
每篇都是从项目架构分析->鉴权分析->漏洞审计,以思路为主,并非简单漏洞披露,源码类型广
3、一些源码资源
4、不定期抽奖和布置考核任务给予奖品,形成玩中学,学中玩的学习氛围(太多了随便放两张)
考核靶场
4、部分项目(代码审计,渗透测试,讲师,HVV等等)优先推送
5、更多内容筹备中。。。
往期精彩课程:
代码审计第一期(JAVA专题)菜狗安全《代码审计培训》双十一优惠开启:手把手0day挖掘教学
价格
哎呀,要不要涨价呢,我给出的回答是,短期内不考虑,目前课程原价依旧1299,并且从今天到第二轮开课1月30号之前1199,一次报名后面可以一直听不会有二次收费,如果是学生确实没啥💴就看B站公开课吧,入门绝对够,付费课有能力再考虑
暂时我也想不到其它问题了,如果有其他疑问可以在群里问,或者私信我
心动不如行动
有需要或者有问题可以加微信,备注“培训”
学员反馈
放几张意思下,还有一堆懒得找了
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:菜狗安全 学员投稿 学员投稿《【JAVA代审】newbee-mall开源电商项目代码审计》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论