程式碼審計--13--CSRF漏洞
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);
}