一次CSRF漏洞挖掘与审计

2025-02-18 10 0

前言

最近闲余时间一直在学习JAVA开发,最近也是跟着某位开发大佬的B站教程,开发了一套超市管理系统。在开发的过程中,我发现自己在代码编写和系统设计上可能存在CSRF漏洞。为了提升自己的开发能力和安全意识,我决定对这套系统的CSRF漏洞进行一次漏洞挖掘和代码审计,并将整个过程记录下来,发布到博客上,供大家参考和指正。

开发环境

idea2024 jdk1.8  tomcat+maven

存在漏洞描述

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的Web安全漏洞,攻击者可以利用该漏洞在用户不知情的情况下,以用户的身份执行未经授权的操作。在修改他人密码的场景中,CSRF漏洞的危害尤为严重,因为攻击者可以通过伪造请求,直接修改目标用户的密码,从而完全控制其账户。

漏洞原理

CSRF攻击的核心原理是利用了Web应用对用户身份验证的依赖。通常情况下,用户在登录某个网站后,浏览器会保存用户的会话信息(如Cookie)。当用户访问其他页面或发起请求时,浏览器会自动携带这些会话信息,服务器通过验证会话信息来判断用户的身份。

攻击者可以利用这一机制,构造一个恶意请求,诱使用户在已登录目标网站的情况下访问恶意页面。此时,浏览器会自动携带用户的会话信息,向目标网站发起请求,服务器会认为这是用户本人发起的合法请求,从而执行相应的操作(如修改密码)。

漏洞复现

一次CSRF漏洞挖掘与审计插图

漏洞存在于修改密码位置:

一次CSRF漏洞挖掘与审计插图1

这里虽然存在旧密码验证,但是由于系统构造仍然是存在CSRF漏洞,请君往下看。

这里显而易见点击保存后第一个包并非修改密码的请求包,而是一个验证数据的包,这里的oldpassword参数首先会验证旧密码是否正确:

一次CSRF漏洞挖掘与审计插图2

点击拦截响应包查看

一次CSRF漏洞挖掘与审计插图3

返回了result=true,那么确实了这个包的目的就是为了验证旧密码是否正确,放包接着往下走。

发现请求包如下:

一次CSRF漏洞挖掘与审计插图4

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

一次CSRF漏洞挖掘与审计插图5

换一个浏览器登陆其他账户进行测试。

一次CSRF漏洞挖掘与审计插图6

将poc保存未html文件。

一次CSRF漏洞挖掘与审计插图7

浏览器打开POC文件进行测试:

一次CSRF漏洞挖掘与审计插图8

点击提交后,发现确实跳转到了退出登陆后的界面。

一次CSRF漏洞挖掘与审计插图9

再去看看数据库。

一次CSRF漏洞挖掘与审计插图10

密码确实被改掉了。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关键字。

一次CSRF漏洞挖掘与审计插图11

发现当前端过来的method=savepwd时会调用updatePwd方法,那么就继续跟踪具体去看updatePwd方法

该方法代码如下:

一次CSRF漏洞挖掘与审计插图12

以下是我对代码逻辑的注释


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请求。

一次CSRF漏洞挖掘与审计插图13

现在是错误的旧密码,我拦截相应包,发现result的值未false。

一次CSRF漏洞挖掘与审计插图14

手动改为true,然后放行。结果符合意料仍然来到了修改密码功能的请求包这里。

一次CSRF漏洞挖掘与审计插图15

结语

说到这儿,其实漏洞的也是很直接了当的漏洞技术含量只能说一丢丢,但是不得不提一下我之前的一个“想当然”的误区,以前看到修改密码功能有旧密码验证的站点,我潜意识里会觉得:“CSRF应该不存在,因为数据包里有旧密码,其他用户的旧密码和你的不一样后端会校验的”结果这次实践啪啪打脸——即使有旧密码验证,如果没加CSRF Token或者其他防护机制,攻击者还是可能通过伪造请求搞事情。

总之,现在我觉额挖漏洞,关键就是多动手、多思考,细心一点总能有收获。当然,网络安全这条路没有尽头,咱们得一直保持学习的心态。

最后,送各位师傅一句话:**“漏洞虐我千百遍,我待漏洞如初恋。”** 别怕踩坑,每一次发现和修复漏洞,都是咱们技术路上的一次成长。一起加油吧!

共勉!挖漏洞的路上,咱们都是战友!


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

Web应用&企业产权&域名资产&网络空间&威胁情报
【CTF】Python Jail沙箱逃逸手法总结 PyJail All in One
风投巨头Insight Partners遭遇网络攻击,敏感数据或泄露
网络犯罪转向社交媒体,攻击量达历史新高
雅虎数据泄露事件:黑客涉嫌兜售60.2万个电子邮件账户
黑客如何利用提示词工程操纵AI代理?

发布评论