前言
最近闲余时间一直在学习JAVA开发,最近也是跟着某位开发大佬的B站教程,开发了一套超市管理系统。在开发的过程中,我发现自己在代码编写和系统设计上可能存在CSRF漏洞。为了提升自己的开发能力和安全意识,我决定对这套系统的CSRF漏洞进行一次漏洞挖掘和代码审计,并将整个过程记录下来,发布到博客上,供大家参考和指正。
开发环境
idea2024 jdk1.8 tomcat+maven
存在漏洞描述
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的Web安全漏洞,攻击者可以利用该漏洞在用户不知情的情况下,以用户的身份执行未经授权的操作。在修改他人密码的场景中,CSRF漏洞的危害尤为严重,因为攻击者可以通过伪造请求,直接修改目标用户的密码,从而完全控制其账户。
漏洞原理
CSRF攻击的核心原理是利用了Web应用对用户身份验证的依赖。通常情况下,用户在登录某个网站后,浏览器会保存用户的会话信息(如Cookie)。当用户访问其他页面或发起请求时,浏览器会自动携带这些会话信息,服务器通过验证会话信息来判断用户的身份。
攻击者可以利用这一机制,构造一个恶意请求,诱使用户在已登录目标网站的情况下访问恶意页面。此时,浏览器会自动携带用户的会话信息,向目标网站发起请求,服务器会认为这是用户本人发起的合法请求,从而执行相应的操作(如修改密码)。
漏洞复现
漏洞存在于修改密码位置:
这里虽然存在旧密码验证,但是由于系统构造仍然是存在CSRF漏洞,请君往下看。
这里显而易见点击保存后第一个包并非修改密码的请求包,而是一个验证数据的包,这里的oldpassword参数首先会验证旧密码是否正确:
点击拦截响应包查看
返回了result=true,那么确实了这个包的目的就是为了验证旧密码是否正确,放包接着往下走。
发现请求包如下:
POST /smbms/jsp/user.do HTTP/1.1 Host: 192.168.32.142:8080 Content-Length: 85 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://192.168.32.142:8080 Content-Type: application/x-www-form-urlencoded User-Agent: User-Agentï¼Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 NetType/WIFI MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090a1b) XWEB/11275 Flue Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://192.168.32.142:8080/smbms/jsp/pwdmodify.jsp Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=386FCEF330712CC65F793089E7377DC0 Connection: close method=savepwd&oldpassword=123456789&newpassword=12345678%2B&rnewpassword=12345678%2B
尝试将该数据包用burp自带的工具生成一个CSRFPoc
换一个浏览器登陆其他账户进行测试。
将poc保存未html文件。
浏览器打开POC文件进行测试:
点击提交后,发现确实跳转到了退出登陆后的界面。
再去看看数据库。
密码确实被改掉了。OK到了这里漏洞已经复现成功。
代码分析审计
这里请求路径说user.do,查看web.xml,如下所示:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"><!--将web版本4.0修改为3.1,注意上一行约束文件也要修改 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>org.example.servlet.user.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login.do</url-pattern> </servlet-mapping> <!--字符编码过滤器--> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.example.filter.CharacterEncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--欢迎页--> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <!-- 默认session过期时间:真实业务需求 <session-config> <session-timeout>10</session-timeout> </session-config> --> <!--登录过滤器--> <filter> <filter-name>SysFilter</filter-name> <filter-class>org.example.filter.SysFilter</filter-class> </filter> <filter-mapping> <filter-name>SysFilter</filter-name> <url-pattern>/jsp/*</url-pattern> </filter-mapping> <!--注销功能--> <servlet> <servlet-name>LoginOutServlet</servlet-name> <servlet-class>org.example.servlet.user.LoginOutServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginOutServlet</servlet-name> <url-pattern>/jsp/logout.do</url-pattern> </servlet-mapping> <!--用户修改密码--> <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> <!--session过期时间--> <session-config> <session-timeout>30</session-timeout><!--设置session三十分钟过期--> </session-config> <!--jsp中的js乱码解决方案,好用!--> <jsp-config> <jsp-property-group> <display-name>HtmlConfiguration</display-name> <url-pattern>*.html</url-pattern> <page-encoding>UTF-8</page-encoding> </jsp-property-group> <jsp-property-group> <display-name>JspConfiguration</display-name> <url-pattern>*.jsp</url-pattern> <page-encoding>UTF-8</page-encoding> </jsp-property-group> <jsp-property-group> <display-name>JsConfiguration</display-name> <url-pattern>*.js</url-pattern> <page-encoding>UTF-8</page-encoding> </jsp-property-group> </jsp-config> </web-app>
那么就根据servlet-class去查看相关的具体代码逻辑。
全局搜索savepwd关键字。
发现当前端过来的method=savepwd时会调用updatePwd方法,那么就继续跟踪具体去看updatePwd方法
该方法代码如下:
以下是我对代码逻辑的注释
public void updatePwd(HttpServletRequest req, HttpServletResponse resp){
//从session中获取用户id
Object attribute = req.getSession().getAttribute(Constansts.USER_SESSION);//创建一个对象获取session
String newpassword = req.getParameter("newpassword");//获取前端传递过来的newpassword的值
boolean flag = false;
if(attribute !=null && !StringUtils.isNullOrEmpty(newpassword)){//验证新密码是否为空
UserService userService = new UserServiceImpl();
flag = userService.updatePwd(((User)attribute).getId(),newpassword);//调用Service层userService.updatePwd修改密码
if(flag){
req.setAttribute("message","修改密码成功!请重新登录!");
//密码修改成功,移除当前session
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);
}
}
这里的问题已经暴漏,回过头再来看请求该方法的数据包。
POST /smbms/jsp/user.do HTTP/1.1 Host: 192.168.32.142:8080 Content-Length: 85 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://192.168.32.142:8080 Content-Type: application/x-www-form-urlencoded User-Agent: User-Agentï¼Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 NetType/WIFI MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090a1b) XWEB/11275 Flue Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://192.168.32.142:8080/smbms/jsp/pwdmodify.jsp Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: JSESSIONID=386FCEF330712CC65F793089E7377DC0 Connection: close method=savepwd&oldpassword=123456789&newpassword=12345678%2B&rnewpassword=12345678%2B
这里虽然传递了oldpassword(旧密码),但是在这个修改密码函数中根本没有用到旧密码,更没有做校验。也没有token因此存在利用难度很低的CSRF漏洞。
还记得刚才修改密码功能的校验方法嘛?这里我也分析以下第一个请求包也就是验证旧密码功能是否牢靠。
先回来看这个修改密码界面的pwdmodify.jsp的实现
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@include file="/jsp/common/head.jsp"%> <div class="right"> <div class="location"> <strong>你现在所在的位置是:</strong> <span>密码修改页面</span> </div> <div class="providerAdd"> <form name="userForm" method="post" action="${pageContext.request.contextPath }/jsp/user.do"> <input type="hidden" name="method" value="savepwd"> <!--div的class 为error是验证错误,ok是验证成功--> <div class="info">${message}</div> <div class=""> <label for="oldPassword">旧密码:</label> <input type="password" name="oldpassword" value=""> <font color="red"></font> </div> <div> <label for="newPassword">新密码:</label> <input type="password" name="newpassword" value=""> <font color="red"></font> </div> <div> <label for="reNewPassword">确认新密码:</label> <input type="password" name="rnewpassword" value=""> <font color="red"></font> </div> <div class="providerAddBtn"> <!--<a href="https://www.freebuf.com/articles/web/421762.html#">保存</a>--> <input type="button" name="save" value="保存" class="input-button"> </div> </form> </div> </div> </section> <%@include file="/jsp/common/foot.jsp" %> <script type="text/javascript" src="https://www.freebuf.com/articles/web/${pageContext.request.contextPath }/js/pwdmodify.js" charset="UTF-8"></script>
我这里是先发送一个ajax请求到pwdmodify方法来校验的。校验代码如下:
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"){//当前用户session过期,请重新登录 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); } });
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){//首先看用户是否登录也就是session是否为空
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();
//调用fastjson将内容转换为json格式
writer.write(JSONArray.toJSONString(ressltMap));
writer.flush();
writer.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
验证旧密码功能逻辑也还好,但是存在一个问题,他只是比对了旧密码是否正确,然后如果正确就把result=true返回给ajax进行处理,但是Ajax是一个前端的,因此这里也存在被篡改的风险。也就是旧密码验证这个功能完全形同虚设。
来试验一下:
抓包提前开着,旧密码随便输入一个错误的。只要旧密码框失去焦距就会发送Ajax请求。
现在是错误的旧密码,我拦截相应包,发现result的值未false。
手动改为true,然后放行。结果符合意料仍然来到了修改密码功能的请求包这里。
结语
说到这儿,其实漏洞的也是很直接了当的漏洞技术含量只能说一丢丢,但是不得不提一下我之前的一个“想当然”的误区,以前看到修改密码功能有旧密码验证的站点,我潜意识里会觉得:“CSRF应该不存在,因为数据包里有旧密码,其他用户的旧密码和你的不一样后端会校验的”结果这次实践啪啪打脸——即使有旧密码验证,如果没加CSRF Token或者其他防护机制,攻击者还是可能通过伪造请求搞事情。
总之,现在我觉额挖漏洞,关键就是多动手、多思考,细心一点总能有收获。当然,网络安全这条路没有尽头,咱们得一直保持学习的心态。
最后,送各位师傅一句话:**“漏洞虐我千百遍,我待漏洞如初恋。”** 别怕踩坑,每一次发现和修复漏洞,都是咱们技术路上的一次成长。一起加油吧!
共勉!挖漏洞的路上,咱们都是战友!
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)