Web安全技術大作業-限制同一個賬戶一段時間內登錄次數及繞過腳本

0x00 介紹

本文介紹的是《Web安全技術》的大作業在線服務限制繞過中,限制同一個賬戶一段時間內登錄次數及繞過腳本的設計與實現。

本模塊包含的功能如下:

  1. 限制賬戶在一段時間內登錄失敗的次數:如果在該段時間內登錄失敗的次數超過某個閾值,則禁止登錄。這裏有幾個需要理清楚的:如何定義一段時間,如何確保賬戶被封禁的狀態的更新,一段時間的起始時間的更新等
  2. 繞過慢速撞庫的方法,對於同一個賬戶,可以在短時間內只嘗試1次,或者少於網站上限的次數,然後不停的嘗試不同的賬戶,直到嘗試完賬戶字典中所有的賬戶,再進入下一輪撞庫。

0x01 限制賬戶在一段時間內登錄失敗的次數

1.1 思路

  1. 數據庫新增login_error表,記錄賬戶登錄失敗的信息:

    • user: auth_user表的外鍵
    • first_err_time:記錄第一次登錄失敗的時間
    • err_login_times:記錄一段時間內登錄失敗的次數
    • block_state:記錄賬戶是否被封禁,True表示被封禁,False表示賬戶正常
  2. 思路:

    • 從first_err_time開始的10min內爲需要判斷當前賬戶登陸失敗次數的時間段。

      ​ |___________________________|

    ​ first_err_time                       first_err_time+10min

    • 在這段時間內,如果失敗次數err_login_times超過3次,則block_state置爲True

1.2 實現

  1. 代碼如下:

def check_account_state(request):
    """
    檢查賬戶是否在封禁狀態
    Args:
        request: request對象
    Returns:
        state: 1表示該賬戶狀態正常,0表示該賬戶還在被封禁狀態,-1表示用戶名或密碼錯誤
    """
    state = 1
    username = request.POST.get('username', "")
    password = request.POST.get('password', "")
    auth_res = auth.authenticate(username=username, password=password)
    default_first_err_time = datetime.datetime(2019, 10, 24, 12, 30, 11).replace(tzinfo=pytz.timezone('Asia/Shanghai'))
    default_first_err_time = timestamp_to_1970(default_first_err_time)
    curr_time = datetime.datetime.now().replace(tzinfo=pytz.timezone('Asia/Shanghai'))
    curr_time = timestamp_to_1970(curr_time)

    # auth_user表中查找該用戶
    user_obj = User.objects.filter(username=username).first()
    if not user_obj:  # 該用戶不存在
        state = -1
        return state  # 直接返回True,交給Form進一步判斷

    # 據user id查詢login_err表
    login_err = LoginError.objects.filter(user=user_obj).first()
    if (not login_err) and (not auth_res):  # login_err表中沒有該賬戶的記錄並且登錄失敗
        err_login_times = 1
        block_state = False
        login_err_obj = LoginError(user=user_obj, first_err_time=curr_time, err_login_times=err_login_times, block_state=block_state)
        login_err_obj.save()
    elif login_err:  # login_err表中存在該賬戶的記錄
        first_err_time = login_err.first_err_time
        err_login_times = login_err.err_login_times
        block_state = login_err.block_state

        time_interval = curr_time - first_err_time

        if time_interval > ACCOUNT_LIMIT_TIME:  # 超過10min
            if not auth_res:  # 用戶名密碼錯誤,重置login_err對象,並將錯誤次數置爲1
                login_err = set_login_err(login_err, curr_time, 1, False)
                login_err.save()
            else:  # 用戶名密碼正確,登錄成功
                login_err = set_login_err(login_err, default_first_err_time, 0, False)
                login_err.save()
        elif (time_interval <= ACCOUNT_LIMIT_TIME) and block_state:  # 10min內,並且賬戶已被封禁
            state = 0
            return state
        elif (time_interval <= ACCOUNT_LIMIT_TIME) and (not block_state):  # 10min內,並且賬戶未被封禁
            if not auth_res:  # 用戶名和密碼錯誤
                err_login_times = err_login_times + 1
                login_err = set_login_err(login_err, first_err_time, err_login_times, False)
                if err_login_times == MAX_ERR_LOGIN_TIMES:
                    login_err = set_login_err(login_err, first_err_time, 0, True)  # 這裏的時間必須是數據庫中取出來的第一次登錄失敗的時間
                login_err.save()
            else:  # 用戶名和密碼正確
                login_err = set_login_err(login_err, default_first_err_time, 0, False)
                login_err.save()
    elif auth_res:
        return state

    return state


def set_login_err(login_err, first_err_time, err_login_times, block_state):
    """
    更新 login_err 對象
    """
    login_err.first_err_time = first_err_time
    login_err.err_login_times = err_login_times
    login_err.block_state = block_state

    return login_err


def timestamp_to_1970(timestamp):
    """
    當前時間距離1970的秒數
    """
    return time.mktime(timestamp.timetuple())

  1. datetime獲取的時間存在時區不一致的問題:在debug時候打印出來的時間與系統的時間一致,但是存儲到數據庫以後是差了8h的時間,即使我在代碼中設置時區的替換也沒法解決,settings.py中設置了時區也不行。最後我只想得到兩種方法解決:
    • 數據庫中存儲時間戳字符串
    >>> from datetime import datetime
    >>> a = str(datetime.now())
    >>> a
    '2019-10-26 15:57:51.582710'
    >>> b = datetime.strptime(a, '%Y-%m-%d %H:%M:%S')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "D:\ProgramFiles\Anaconda3\lib\_strptime.py", line 577, in _strptime_datetime
        tt, fraction, gmtoff_fraction = _strptime(data_string, format)
      File "D:\ProgramFiles\Anaconda3\lib\_strptime.py", line 362, in _strptime
        data_string[found.end():])
    ValueError: unconverted data remains: .582710
    >>> b = datetime.strptime(a.split('.')[0], '%Y-%m-%d %H:%M:%S')
    >>> b
    datetime.datetime(2019, 10, 26, 15, 57, 51)
    
    • 數據庫中存儲的是當前時間距離1970年的秒數(FloatField()),這樣避免了時區處理的問題,通過代碼中的timestamp_to_1970函數進行轉換。

1.3 改進

可以根據賬戶登錄失敗的頻率、或是該賬戶使用頻率來動態設置封賬號的時間

0x02 繞過

2.1 思路

爲了儘量不觸發目標網站對單個賬戶一段時間內的登錄失敗次數的限制,利用慢速撞庫的方法。

針對單個賬戶,短時間內只進行一次撞庫嘗試,在一輪裏遍歷所有的賬戶;然後進行中場休息,再繼續下一輪嘗試。如果在過程中已經有撞庫成功的賬戶,則跳過該賬戶。

2.2 實現

  1. 詳細的代碼在iansmith123開源的bruteforce Repository中的文件:block_account_bypass.pyba_bypass_bruteforce.py
  2. 主要的代碼:
def get_dict(dict_user, dict_pass):
    """
    生成字典隊列
    :return:
    """
    with open("dict/{}".format(dict_user)) as f:
        username = [line.strip() for line in f.readlines()]

    with open('dict/{}'.format(dict_pass)) as f:
        passwords = [line.strip() for line in f.readlines()]

    count = 0
    for u in username:
        # 每一輪都換下一個密碼
        p = passwords[curr_round % len(passwords)]
        count += 1
        pair = (u, p)
        dict_queue.put(pair)
    print("字典生成完成,長度 {}".format(count))

2.3 改進

  • 測試得到目標網站的賬戶封禁時間time_interval,該段時間內的封禁次數n
  • 在繞過腳本中,對於同一個賬戶可以直接嘗試前n-1次,記錄第一次錯誤時間first_time,然後程序sleep,直到first_time + time_interval

0x03 Tools and Tricks

  1. 進入sqlite3命令行:
sqlite3 db_file_path
  1. pycharm連接sqlite3:

    • 側邊欄的Database中,點擊+新建一個sqlite數據庫
    • 記得點擊窗口下方的安裝相關的驅動
    • 然後在File 裏選擇數據庫文件即可
  2. django自帶的auth_user表的引用:from django.contrib.auth.models import User

  3. 刪除數據庫的表中所有數據:delete from user_loginerror;

發佈了80 篇原創文章 · 獲贊 106 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章