Skip to main content

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

  1. Sự khác biệt giữa username enumeration và brute force là gì? Tại sao cả hai đều nguy hiểm?
  2. Mô tả cách bypass MFA nếu ứng dụng không có session state machine.
  3. Tại sao MD5 không phải là cách hash password an toàn? Nên dùng gì thay thế?
  4. Làm thế nào để implement rate limiting cho login endpoint mà không ảnh hưởng đến UX?
  5. Host Header Injection trong password reset hoạt động như thế nào?