一次CSRF漏洞挖掘与审计
字数 1427 2025-08-22 12:23:36
CSRF漏洞挖掘与审计教学文档
一、漏洞概述
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的Web安全漏洞,攻击者可以利用该漏洞在用户不知情的情况下,以用户的身份执行未经授权的操作。
漏洞危害
- 在修改他人密码的场景中尤为严重
- 攻击者可以完全控制目标用户账户
- 可能导致数据泄露、权限提升等严重后果
二、漏洞原理
核心机制
- Web应用依赖用户身份验证(通常通过会话信息如Cookie)
- 用户登录后,浏览器会保存会话信息
- 用户访问其他页面时,浏览器自动携带这些会话信息
- 服务器通过验证会话信息判断用户身份
攻击流程
- 攻击者构造恶意请求
- 诱使用户在已登录目标网站的情况下访问恶意页面
- 浏览器自动携带用户的会话信息向目标网站发起请求
- 服务器误认为是用户本人发起的合法请求
- 执行未经授权的操作(如修改密码)
三、漏洞复现
环境准备
- 开发环境:idea2024、jdk1.8、tomcat+maven
- 目标系统:超市管理系统
- 测试工具:Burp Suite
复现步骤
-
定位漏洞点:修改密码功能
-
分析请求流程:
- 第一个请求包:验证旧密码(
oldpassword参数) - 第二个请求包:实际修改密码请求
- 第一个请求包:验证旧密码(
-
关键请求包分析:
POST /smbms/jsp/user.do HTTP/1.1
Host: 192.168.32.142:8080
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=386FCEF330712CC65F793089E7377DC0
method=savepwd&oldpassword=123456789&newpassword=12345678%2B&rnewpassword=12345678%2B
- 使用Burp Suite生成CSRF PoC
- 在另一个浏览器中登录其他账户测试PoC
- 验证密码是否被修改
四、代码审计分析
1. Web.xml配置分析
<servlet>
<servlet-name>UserServlet</servlet-name>
<servlet-class>org.example.servlet.user.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UserServlet</servlet-name>
<url-pattern>/jsp/user.do</url-pattern>
</servlet-mapping>
2. 关键代码分析
修改密码功能(updatePwd方法)
public void updatePwd(HttpServletRequest req, HttpServletResponse resp){
// 从session中获取用户id
Object attribute = req.getSession().getAttribute(Constansts.USER_SESSION);
String newpassword = req.getParameter("newpassword");
boolean flag = false;
if(attribute !=null && !StringUtils.isNullOrEmpty(newpassword)){
UserService userService = new UserServiceImpl();
flag = userService.updatePwd(((User)attribute).getId(),newpassword);
if(flag){
req.setAttribute("message","修改密码成功!请重新登录!");
req.getSession().removeAttribute(Constansts.USER_SESSION);
}else {
req.setAttribute("message","修改密码失败!");
}
}else{
req.setAttribute("message","新密码有问题!");
}
try {
req.getRequestDispatcher("pwdmodify.jsp").forward(req, resp);
} catch (ServletException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
漏洞点:
- 虽然前端传递了
oldpassword参数,但后端代码中完全没有使用和校验 - 没有CSRF Token等防护机制
- 仅依赖session验证用户身份
旧密码验证功能(pwdModify方法)
public void pwdModify(HttpServletRequest req, HttpServletResponse resp){
Object attribute = req.getSession().getAttribute(Constansts.USER_SESSION);
String oldpassword = req.getParameter("oldpassword");
Map<String,String> ressltMap = new HashMap<String,String>();
if(attribute == null){
ressltMap.put("result","sessionor");
} else if (StringUtils.isNullOrEmpty(oldpassword)) {
ressltMap.put("result","error");
}else {
String userPassword = ((User)attribute).getUserPassword();
if(userPassword.equals(oldpassword)){
ressltMap.put("result","true");
}else{
ressltMap.put("result","false");
}
}
try {
resp.setContentType("application/json");
PrintWriter writer = resp.getWriter();
writer.write(JSONArray.toJSONString(ressltMap));
writer.flush();
writer.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
漏洞点:
- 验证结果仅通过前端AJAX处理
- 攻击者可拦截响应包修改
result值为true绕过验证 - 后端验证与修改密码操作分离,没有关联性
3. 前端实现分析(pwdmodify.jsp)
<form id="userForm" name="userForm" method="post" action="${pageContext.request.contextPath }/jsp/user.do">
<input type="hidden" name="method" value="savepwd">
<div class="">
<label for="oldPassword">旧密码:</label>
<input type="password" name="oldpassword" id="oldpassword" value="">
</div>
<div>
<label for="newPassword">新密码:</label>
<input type="password" name="newpassword" id="newpassword" value="">
</div>
<div>
<label for="reNewPassword">确认新密码:</label>
<input type="password" name="rnewpassword" id="rnewpassword" value="">
</div>
<div class="providerAddBtn">
<input type="button" name="save" id="save" value="保存" class="input-button">
</div>
</form>
AJAX验证代码:
oldpassword.on("blur",function(){
$.ajax({
type:"GET",
url:path+"/jsp/user.do",
data:{method:"pwdmodify",oldpassword:oldpassword.val()},
dataType:"json",
success:function(data){
if(data.result == "true"){
validateTip(oldpassword.next(),{"color":"green"},imgYes,true);
}else if(data.result == "false"){
validateTip(oldpassword.next(),{"color":"red"},imgNo + " 原密码输入不正确",false);
}else if(data.result == "sessionerror"){
validateTip(oldpassword.next(),{"color":"red"},imgNo + " 当前用户session过期,请重新登录",false);
}else if(data.result == "error"){
validateTip(oldpassword.next(),{"color":"red"},imgNo + " 请输入旧密码",false);
}
},
error:function(data){
validateTip(oldpassword.next(),{"color":"red"},imgNo + " 请求错误",false)
}
});
});
五、漏洞利用分析
1. 直接利用方式
- 构造恶意HTML表单,诱导用户点击
- 绕过旧密码验证:
- 直接发送修改密码请求(
method=savepwd) - 拦截验证响应,修改
result为true
- 直接发送修改密码请求(
2. 利用难点
- 需要用户已登录系统
- 需要诱使用户访问恶意页面
3. 利用效果
- 成功修改用户密码
- 用户被强制退出登录(session被清除)
六、修复建议
1. 添加CSRF Token
- 在表单中添加随机生成的Token
- 服务器端验证Token有效性
// 生成Token
String csrfToken = UUID.randomUUID().toString();
session.setAttribute("csrfToken", csrfToken);
// 在表单中添加
<input type="hidden" name="csrfToken" value="${csrfToken}">
// 服务器端验证
if(!request.getParameter("csrfToken").equals(session.getAttribute("csrfToken"))){
// 验证失败处理
}
2. 加强旧密码验证
- 将旧密码验证与修改操作合并
- 在修改密码方法中验证旧密码
public void updatePwd(HttpServletRequest req, HttpServletResponse resp){
Object attribute = req.getSession().getAttribute(Constansts.USER_SESSION);
String oldpassword = req.getParameter("oldpassword");
String newpassword = req.getParameter("newpassword");
if(attribute == null){
// 处理未登录情况
return;
}
User user = (User)attribute;
if(!user.getUserPassword().equals(oldpassword)){
// 旧密码不匹配
return;
}
// 继续修改密码逻辑
}
3. 使用SameSite Cookie属性
<session-config>
<cookie-config>
<http-only>true</http-only>
<secure>true</secure>
<same-site>strict</same-site>
</cookie-config>
<session-timeout>30</session-timeout>
</session-config>
4. 验证HTTP Referer头
String referer = request.getHeader("Referer");
if(referer == null || !referer.startsWith("https://yourdomain.com")){
// 非法请求
return;
}
5. 关键操作使用POST请求
- 避免使用GET请求进行状态修改操作
七、经验总结
-
常见误区:认为有旧密码验证就能防御CSRF
- 实际:如果验证与操作分离,仍可能被绕过
-
审计要点:
- 检查关键操作是否有CSRF防护机制
- 验证前端验证是否可被绕过
- 检查操作是否与验证相关联
-
开发建议:
- 关键操作必须添加CSRF Token
- 前后端验证要紧密结合
- 遵循"不信任用户输入"原则
-
测试方法:
- 使用Burp Suite生成CSRF PoC测试
- 尝试绕过前端验证
- 检查是否依赖单一验证机制
安全箴言:漏洞虐我千百遍,我待漏洞如初恋。保持学习心态,持续提升安全意识和技能。