1、漏洞描述
漏洞描述:
Cross-Site Request Forgery(CSRF),跨站请求伪造攻击。
攻击者在用户浏览网页时,利用页面元素(例如img的src),强迫受害者的浏览器向Web应用程序发送一个改变用户信息的请求。
由于发生CSRF攻击后,攻击者是强迫用户向服务器发送请求,所以会造成用户信息被迫修改,更严重者引发蠕虫攻击。
CSRF攻击可以从站外和站内发起。从站内发起CSRF攻击,需要利用网站本身的业务,比如“自定义头像”功能,恶意用户指定自己的头像URL是一个修改用户信息的链接,当其他已登录用户浏览恶意用户头像时,会自动向这个链接发送修改信息请求。
从站外发送请求,则需要恶意用户在自己的服务器上,放一个自动提交修改个人信息的htm页面,并把页面地址发给受害者用户,受害者用户打开时,会发起一个请求。
如果恶意用户能够知道网站管理后台某项功能的URL,就可以直接攻击管理员,强迫管理员执行恶意用户定义的操作。
2、漏洞场景复现
漏洞场景一:
一个没有CSRF安全防御的代码如下:
HttpServletRequest request, HttpServletResponse response)
{
int userid=Integer.valueOf( request.getSession().getAttribute("userid").toString());
String email=request.getParameter("email");
String tel=request.getParameter("tel");
String realname=request.getParameter("realname");
Object[] params = new Object[4];
params[0] = email;
params[1] = tel;
params[2] = realname;
params[3] = userid;
final String sql = "update user set email=?,tel=?,realname=? where userid=?";
conn.execUpdate(sql,params);
}
代码中接收用户提交的参数“email,tel,realname”,之后修改了该用户的数据,一旦接收到一个用户发来的请求,就执行修改操作。提交表单代码:
<form action="http://localhost/servlet/modify" method="POST">
<input name="email">
<input name="tel">
<input name="realname">
<input name="userid">
<input type="submit">
</form>
当用户点提交时,就会触发修改操作。
本例子是一个站外发起CSRF攻击例子。
如果“代码示例”中的代码,是xxxx.com上的一个web应用,那么恶意用户为了攻击xxxx.com的登录用户,可以构造2个HTML页面。
1、页面a.htm中,iframe一下b.htm,把宽和高都设为0。
<iframe src="b.htm" width="0" height="0"></frame>
这是为了当攻击发生时,受害用户看不到提交成功结果页面。
2、页面b.htm中,有一个表单,和一段脚本,脚本的作用是,当页面加载时,自动提交这个表单。
<form id="modify" action="http://xxxx.com/servlet/modify" method="POST">
<input name="email">
<input name="tel">
<input name="realname">
<input name="userid">
<input type="submit">
</form>
<script>
document.getElementById("modify").submit();
</script>
3、攻击者只要把页面a.htm放在自己的web服务器上,并发送给登录用户即可。
4、用户打开a.htm后,会自动提交表单,发送给xxxx.com下的那个存在CSRF漏洞的web应用,所以用户的信息,就被迫修改了。
在整个攻击过程中,受害者用户仅仅看到了一个空白页面(可以伪造成其他无关页面),并且一直不知道自己的信息已经被修改了。
漏洞场景二:
一个没有CSRF安全防御的代码如下:
String info=request.getParameter("info");
String id=session.getAttribute("userid").toString();
if(info!=null && !info.equals("") && id!=null)
{
Statement stmt = con.createStatement();
stmt.executeUpdate("Update users set about='"+info+"' where id="+id);
out.print("<b class='fail'>info Changed</b>");
}
out.print("<br/><br/><a href='"+path+"/myprofile.jsp?id="+id+"'>Return to Profile Page >></a>");
代码中接收用户提交的参数“info”,之后修改了该用户的数据,一旦接收到一个用户发来的请求,就执行修改操作,通过get请求构造CSRF链接:http://localhost:8080/WebLab/vulnerability/csrf/change-info.jsp?info=test&change=Change
本例子是一个站内发起CSRF攻击例子
1、在网站公共区域处,例如发布帖子处插入img标签,src设置为
http://localhost:8080/WebLab/vulnerability/csrf/change-info.jsp?info=test&change=Change
<img src=http://localhost:8080/WebLab/vulnerability/csrf/change-info.jsp?info=test&change=Change />
插入之后当其他已登录用户浏览其页面,即可执行修改个人资料操作
3、漏洞修复建议
要防御CSRF攻击,应遵循以下过程:
1、在用户登陆时,设置一个TOKEN;
2、表单被提交后,在接收用户请求的Web应用中,判断表单中的TOKEN值是否和系统记录的TOKEN值一致,如果不一致或没有这个值,就判断为CSRF攻击,同时记录攻击日志。由于攻击者无法预测每一个用户登录时生成的那个随机TOKEN值,所以无法伪造这个参数。
具体实现代码如下:
前端表单修改成:
<form action="change-info.jsp" method="GET">
Description:
<input type="text" name="info" value=""/>
<input type="hidden" name="token" value="<%=session.getAttribute("csrftoken").toString()%>" />
<br/><br/>
<input type="submit" name="change" value="Change"/>
1、代码中<%=session.getAttribute(“csrftoken”).toString()%>将会生成一个隐藏域,用于生成验证token,它将会作为表单的其中一个参数一起提交。
2、当出现GET请求修改用户数据时,若在url中出现了token,当前页面就不允许出现用户定义的站外链接,否则攻击者可以引诱用户点击攻击者定义的链接,访问在自己的网站,从referer中,获取url中的token,造成token泄露。
后台Java防御代码参考实现如下:
第一步,新建CSRF令牌添加进用户每次登陆以及存储在httpsession里,这种令牌至少对每个用户会话应是唯一的,或者是对每个请求是唯一的。
//this code is in the Defaulter implementation of ESAPI
/**this user’s CSRF token. */
Private String csrfToken = resetCSRFToken();
Public StringresetCSRFToken() {
csrfToken = ESAPI.random().getRandomString(8, DefaultEncoder.CHAR_ALPHANUMBERICS);
//利用ESAPI生成随机TOKEN
Return csrfToken
}
第二步,令牌可以包含在URL中或作为一个URL参数记/隐藏字段。
//from HTTP Utilitiles interface
Final static String CSRF_TOKEN_NAME="token";
//this code is from the Default HTTP Utilities implementation in ESAPI
Public String addCSRFToken(Stringhref) {
User user=ESAPI.authenticator().getCurrentUser();
if(user.isAnonymous()){returnhref;}
//if there are already parameters append with&,otherwise append with?
String token=CSRF_TOKEN_NAME+"="+user.getCSRFToken();
return href.indexOf('?')!=-1?href+"&"+token:href+"?"+token;
}
...
public StringgetCSRFToken() {
User user=ESAPI.authenticator().getCurrentUser();
if(user==null) return null;return user.getCSRFToken();
}
第三步,在服务器端检查提交令牌与用户会话对象令牌是否匹配。
//this code is from the Defaul tHTTP Utilities implementation in
//ESAPI
Public void verifyCSRFToken(HttpServletRequest request) throws IntrusionException {
User user=ESAPI.authenticator().getCurrentUser();
//check if user authenticated with this request-noCSRFprotection required
if(request.getAttribute(user.getCSRFToken())!=null) {
return;
}
String token=request.getParameter(CSRF_TOKEN_NAME);
if(!user.getCSRFToken().equals(token)) {
//比较session中token与客户端参数中token是否一致
throw new IntrusionException("Authenticationfailed","Possibly forgeted HTTP request without proper CSRFtokendetected");
}
}
第四步,在注销和会话超时,删除用户对象会话和会话销毁。
//this code is in the DefaultUser implementation of ESAPI
Public void logout() {
ESAPI.httpUtilities().killCookie(ESAPI.currentResponse(),ESAPI.currentRequest(),HTTPUtilities.REMEMBER_TOKEN_COOKIE_NAME);
HttpSession session=ESAPI.currentRequest().getSession(false);
if(session!=null) {
removeSession(session);
session.invalidate();
}
ESAPI.httpUtilities().killCookie(ESAPI.currentRequest(),ESAPI.currentResponse(),"JSESSIONID");
loggedIn=false;
logger.info(Logger.SECURITY_SUCCESS,"Logout successful");
ESAPI.authenticator().setCurrentUser(User.ANONYMOUS);
}