Try   HackMD

Rootme: JWT Attacks

JWT - Introduction

Link tại đây.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Bài cho 1 form login.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Sau đó login với guest

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Kiểm tra cookie:
jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.OnuZnYMdetcg7AWGV6WURn8CFSfas6AQej4V9M13nsk
Sau đó decode JWT này tại jwt.io. Kết quả:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Xuất hiện Invalid Signature tất nhiên signature không đúng. Thuật toán đang sử dụng ở đây là HS256, ta thử chuyển nó sang none và xóa phần Signature của nó đồng thời sửa guest thành admin. Có thể làm thủ công hoặc dùng extension JSON Web Tokens trên Burp Suite:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

JWT mới: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIn0.
Kết quả:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Như vậy là ứng dụng sử dụng key để verify JWT nhưng cũng bỏ qua nó khi không dùng thuật toán ("alg": "none")

Flag: S1gn4tuR3_v3r1f1c4t10N_1S_1MP0Rt4n7

JWT - Weak secret

Link tại đây

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Vào bài thì tại /hello trả về thông báo: Let's play a small game, I bet you cannot access to my super secret admin section. Make a GET request to /token and use the token you'll get to try to access /admin with a POST request.
Như vậy:

  • /token chứa token (sử dụng method GET)
  • /admin nơi verify token (sử dụng method POST)

Vào /token:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Token nhận được:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlIjoiZ3Vlc3QifQ.4kBPNf7Y6BrtP-Y3A-vQXPY9jAh_d0E6L4IUjL65CvmEjgdTZyr2ag-TM-glH6EYKGgO3dBYbhblaPQsbeClcw
Decode tại jwt.io :

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Nó cho ta biết thuật toán sử dụng là HS512 (đây là mã hóa đối xứng dùng mộtsecret key để mã hóa cũng và giải mã ) có payload "role": "guest" và mục tiêu là đổi guest sang admin.

Thử lấy token đó verify ở /admin:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Thử đối thuật toán sang none như bài trên nhưng tất nhiên là không được. Nhìn lại tiêu đề là Weak secret nghĩ ngay đến việc brute-force. Ta sử dụng hashcat cùng với wordlist là rockyou.txt:

hashcat -a 0 -m 16500 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlIjoiZ3Vlc3QifQ.4kBPNf7Y6BrtP-Y3A-vQXPY9jAh_d0E6L4IUjL65CvmEjgdTZyr2ag-TM-glH6EYKGgO3dBYbhblaPQsbeClcw /usr/share/wordlists/rockyou.txt

Kết quả:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

secret keylol. Bây giờ verify lại jwt:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

JWT mới:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJyb2xlIjoiYWRtaW4ifQ.y9GHxQbH70x_S8F_VPAjra_S-nQ9MsRnuvwWFGoIyKXKk8xCcMpYljN190KcV1qV6qLFTNrvg4Gwyv29OCjAWA

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Flag: PleaseUseAStrongSecretNextTime

JWT - Revoked token

Link tại đây

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Bài cho source:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import Flask, request, jsonify
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, decode_token
import datetime
#from apscheduler.schedulers.background import BackgroundScheduler
import threading
import jwt
from config import *

# Setup flask
app = Flask(__name__)

app.config['JWT_SECRET_KEY'] = SECRET
jwtmanager = JWTManager(app)
blacklist = set()
lock = threading.Lock()

# Free memory from expired tokens, as they are no longer useful
def delete_expired_tokens():
    with lock:
        to_remove = set()
        global blacklist
        for access_token in blacklist:
            try:
                jwt.decode(access_token, app.config['JWT_SECRET_KEY'],algorithm='HS256')
            except:
                to_remove.add(access_token)
        
        blacklist = blacklist.difference(to_remove)

@app.route("/web-serveur/ch63/")
def index():
    return "POST : /web-serveur/ch63/login <br>\nGET : /web-serveur/ch63/admin"

# Standard login endpoint
@app.route('/web-serveur/ch63/login', methods=['POST'])
def login():
    try:
        username = request.json.get('username', None)
        password = request.json.get('password', None)
    except:
        return jsonify({"msg":"""Bad request. Submit your login / pass as {"username":"admin","password":"admin"}"""}), 400

    if username != 'admin' or password != 'admin':
        return jsonify({"msg": "Bad username or password"}), 401

    access_token = create_access_token(identity=username,expires_delta=datetime.timedelta(minutes=3))
    ret = {
        'access_token': access_token,
    }
    
    with lock:
        blacklist.add(access_token)

    return jsonify(ret), 200

# Standard admin endpoint
@app.route('/web-serveur/ch63/admin', methods=['GET'])
@jwt_required
def protected():
    access_token = request.headers.get("Authorization").split()[1]
    with lock:
        if access_token in blacklist:
            return jsonify({"msg":"Token is revoked"})
        else:
            return jsonify({'Congratzzzz!!!_flag:': FLAG})


if __name__ == '__main__':
    scheduler = BackgroundScheduler()
    job = scheduler.add_job(delete_expired_tokens, 'interval', seconds=10)
    scheduler.start()
    app.run(debug=False, host='0.0.0.0', port=5000)

Có 2 endpoints:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Oke bây giờ vào /login: Nó sẽ yêu cầu json chứa usernamepassword đều có giá trị là admin sẽ được token:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2ODU5MzU0NzcsIm5iZiI6MTY4NTkzNTQ3NywianRpIjoiZGIwZmQwYjAtZjM1Zi00MjEzLWI5MTEtZWRmZmNhODdlN2Q2IiwiZXhwIjoxNjg1OTM1NjU3LCJpZGVudGl0eSI6ImFkbWluIiwiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0.aqfOmz1hmOdxdz3C5Fg3OksrfQ0ZnYUedP-s9g_CzVg

Thử dùng nó trong /admin qua header Authorization: Bearer <token>:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Token is revoked. Đúng vậy vì sau khi tạo thì token sẽ được add vào backlist:

access_token = create_access_token(identity=username,expires_delta=datetime.timedelta(minutes=3))
ret = {
    'access_token': access_token,
}

with lock:
    blacklist.add(access_token)

Và nếu token dùng trong header Authorization không trong blacklist thì mới hiện flag:

access_token = request.headers.get("Authorization").split()[1]
with lock:
    if access_token in blacklist:
        return jsonify({"msg":"Token is revoked"})
    else:
        return jsonify({'Congratzzzz!!!_flag:': FLAG})

Token vừa tạo xong đã bị cho vào blacklist thì sao bypass được ???
Thử decode nó:

Bản chất việc revoke token giống như việc xóa bỏ nó ngay sau khi xác thực thành công. Code trong bài này sẽ so sánh xem có token nào y hệt trong blacklist không, vậy có thể biến đổi token để nó khác ban đầu mà nội dung không thay đổi? Nhìn lại cấu trúc JWT:

Đó là cả 3 phần đều được encode bằng Base64, điều quan tâm ở đây là Base64 sẽ padding thêm dấu = nếu thiếu ký tự và nó cũng sẽ bỏ nó nếu đủ. Vậy chỉ cần thêm dấu = vào token thì đc token mới không nằm trong blacklist nhưng nội dung vẫn vậy. Ta sẽ thêm dấu = vào cuối (signature) vì nếu thêm vào header hay payload sẽ thay đổi signature.

Kết quả:

Flag: Do_n0t_r3v0ke_3nc0d3dTokenz_Mam3ne-Us3_th3_JTI_f1eld

JWT - Unsecure File Signature

Link tại đây

Xem source thì thấy có endpoint /admin và có jwt trả về từ server trong cookie và nó dùng để verify vào endpoit đó:

Decode JWT:

Đầu tiên mình thử làm theo như mấy bài trên nhưng tất nhiên đều không được. Xem lại đầu bài có nhắc đến (K)ind (I)dentification (D)ance dễ dàng biết được bài này liên quan đến kid.
kid (Key ID): Cung cấp id mà server có thể sử dụng để xác định secret key trong trường hợp có nhiều secret key để lựa chọn. Nó nằm trong phần header của JWT. Trong nhiều trường hợp nó không chỉ là id mà còn là một đường dẫn đến 1 file chứa secret key và server sẽ lấy nội dung của file đó để verify. Cụ thể ở đây khi bạn thay đổi giá trị kid thành abc:

Như vậy là server sẽ lấy nội dung trong file abc của thư mục keys làm khóa và ở đây file abc không được tìm thấy. Vậy nếu chúng ta biết được nội dung của 1 file rồi lấy nó làm khóa (secret key) thì sẽ verify thành công. Tuy nhiên điều đó có thể khó khả thi vì làm được khi file nào đó bị leak ra.
Đơn giản nhất là lợi dụng /dev/null trên linux, đây là file rỗng và kid trỏ đến đây thì sẽ key lấy được là null. Liên tưởng đến lỗ hỗng LFI để kid trỏ đến /dev/null bằng payload ../../../../và sign nó bằng giá trị nullAA== (dạng base64). Tuy nhiên khi kid có giá trị là ../../../../../../../dev/null thì kết quả là keys/dev/null

Có vẻ chưa ../ đã bị replace. Ta bypass cái này bằng ....//....//....//....//....//....//....//dev/null

Lấy JWT mới set lại cookie, kết quả:

Flag: RM{Uns3cUr3_f1l3_H4ndl1nG!!}

JWT - Public key

Link tại đây

Như phần Statement thì có 3 endpoints:

  • /key lấy public key (dùng method GET)
  • /auth tạo token (dùng method POST)
  • /admin verify để lấy flag (dùng method POST)

Đầu tiền vào /key để lấy public key, format lại được:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3iPoCJJxcoYjU2CE4doM
vEC0kyxzh1wRGxDT0QV4Xr+vdscPZQ3RZFDtA8255BoqTWN5BFOH0LWwOrIlkVB+
eQFZ4JWoEweZLRsWYl6BNQm5p+TL9w9nMsNd+p3ha6I9bepLdzJgGm1zv8Ef9MyQ
uyZ8Aoaw16XzgevFqeegrlpHLO3odTnqOZ+K8xlXPYEvcoOBEr1LE9XpKxAg5puN
alokg+NeRMZBIYkEFODjDkZYsgCo97sfb/Y5hB7m6k4aTvxzpWSSQiBW90UJFLBi
BUn7fgwX6BuXt78l3S/eB9ByMscFZdtVckEzt5LP1dkjNcWS8CHfM1Eibq8Ca2eq
nQIDAQAB
  -----END PUBLIC KEY-----

Phần /auth yêu cầu: You have to provide 'username=YOURNAME' in your POST request nên mình username=chuong (không nhận giá trị admin) và nhận được token:

Sau đó dùng token này vào /admin qua header Authorization: Bearer <token>:

Decode nó xem thử:

JWT sử dụng thuật toán RS256. Đây là thuật toán asymmetric (bất đối xứng), trong đó private key được sử dụng để sign JWT và public key được sử dụng để verify signature của JWT.

Vậy hướng khai thác ở đây là gì? Sau một lúc tìm hiểu về phần này thì đó là thay đổi thuật toán. HS256 chỉ có 1 secret key để sign cũng như verify trong khi RS256 như mình đã nói sử dụng 2 key như trên. Vậy nếu chuyển từ RS256 sang HS256 thì sao? Thì HS256 sẽ dùng public key để verify (thứ mà bài đã cho).
Việc bây giờ là từ public key đó tạo signature cho JWT.
Mình sẽ dùng jwt_tool.py để tạo bằng:

python3 jwt_tool.py <token> -S hs256 -k public.pem

public.key là file chứa public key mà bài cho. Và nhớ sửa giá trị username thành admin.

Kết quả:

Flag: HardcodeYourAlgoBro

JWT - Header Injection

Link tại đây

Oke bắt đầu vào:

Có chỗ login bằng JWT:

Tuy nhiên không có chỗ tạo token nên mình lấy cái placeholder để verify xem sao:

Có vé phải login bằng userNeo, decode thấy phần payload chứa message có vẻ không liên quan nên nên mình bỏ đi cho gọn, và tất nhiên thì verify vẫn thất bại rồi:

Như tiêu đề của bài là Header Injection. Mình đã tìm hiểu sơ qua về phần này và sẽ tổng quát lại nó như sau:
Như ta biết thì phần header của JWT có 1 số tham số hay gặp như alg, typ, ngoài ra còn 1 số nữa dễ bị khai thác:

  • kid: mình đã nói ở bài JWT - Unsecure File Signature
  • jwk (JSON Web Key): Cung cấp một đối tượng JSON được đại diện cho khóa.
  • jku (JSON Web Key Set URL): Cung cấp một URL mà từ đó các server có thể tìm nạp một bộ khóa chứa khóa chính xác.

Ở bài này do mình đã làm rồi và xác định là khai thác dựa vào jwk nên mình sẽ nói về nó luôn.
Ví dụ về jwk:

"jwk": {
    "kty": "RSA",
    "e": "AQAB",
    "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
    "n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}

Nó đại diện cho public key của RSA, kid này cũng định nghĩa như trên, n, e là tham số liên quan về RSA (liên quan đến việc tính toán của thuật toán này).
Tham số jkw trong bài có thể kiểm soát từ người dùng mà server lại không xấc thực nó => bị tấn công. Ý tưởng ở đây là tạo private key sau đó nhúng jwk đó vào header.
Đầu tiên mình sẽ tạo RSA key bằng extension JWT Editor Keys của Burp Suite: chọn New RSA Key -> Generate (không cần quan tâm giá trị bit vì nó sẽ được điều chỉnh lại).

Tiếp theo dùng extension JSON Web Token để nhúng nó vào:
Attack -> Embedded JWK -> chon khóa rồi Ok.

JWT tạo được:
eyJraWQiOiJiZDFlMDZmYy03YTBhLTQ5MWUtYjU1MC02YWYxMTQ4MTRjMmIiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImtpZCI6ImJkMWUwNmZjLTdhMGEtNDkxZS1iNTUwLTZhZjExNDgxNGMyYiIsIm4iOiJsd2lUNWhUOVhPOHptaVRoRW5BX3A0aWtqblUxZkJyV2tYVWNTRVRwT19DNWZEeHl1dmVIV1A1T1NpdG4tbjNXME9SSUFWMHlTeW9ZeVhEZnFSMFloZGFiZk16WHR2cnB1b1BucXlPRGpMUHBvR1lJSFV4MGhHekdNajlPbFFMQl9pNTF6ZENGWF9JNXF5bEhPUndiNXhKcUNMcUhCUU5JakFPUjRPM1czSF8wdGZhaGhtT29fblVLRFFob3NMblFNc2N1bGZYUDZvVmFfOHZpWVZKNDAwSTdVemxqYzRTSElzb0lUUnZFR0FjTVRaRFZXd3o2NmZ2QTlJZ3FLa3V1YnVVV293OU5yTE9wTDdxM1RaYU12UzJ3NWI3TnQwMTlOaDA3bnRXcDE4RjlQRG9NT0dWVVdXVTJ0UXlMTVRXX0dQMnp1aDRyMXFFSVNVMXhGWGU5MFEifX0.eyJ1c2VyIjoiTmVvIiwiaWF0IjoxNjc2NjM3ODMyfQ.M0n_hBVOnzi-j751yiqzUmhVCPLxhrVZX4lK6Tfyc34Nm_-3gTYeKOwOtecb8CbYgjz6nWnbVQIIKleeSsVNcIlfYH8e1n2wJ6JAyNvqCCHHA5W7dzWX9hnZ0tiRSLNurNAutp-ghYjTfQouTZc4MyLAwx00YqsQtiCKQQqBXXOXq8A5-vhdPWp7czxy8LT1C5GqM7dWBsyeFZMHa5a-l4wVKmHvKZunG0PWxr1p6xPsZ5DTgPWyexPITjreiGJoEKCXmr5beUAnzu2Q29YA6s_XLNoUUSp6B6O5W_iGPPez8cSLZhmnK-knOqa7Wof6kK3Q33e6d4CBtgnyEtV4_w
Giờ verify nó, kết quả:

Flag: RM{N3v3r_All0w_UnTrusTed_K3ys}

JWT - Unsecure Key Handling

Link tại đây

Xem qua dễ dàng tìm được endpoint /admin. Và có được JWT, và tất nhiên không phải admin rồi:

Decode JWT:

Nhận thấy có "jku": "jwk.json" nghĩ ngay đến việc inject vào jku này rồi. Nhưng sau một hồi fuzz thì mình không tìm được gì trên trang web, có vẻ jwk.json được lưu vào đâu đó không biết được. Tuy nhiên khi thay đổi jku thì:

Như vậy address phải bắt đầu là http://localhost Mình nghĩ này đến các ký thuật SSRF nhưng bằng nhiều cách vẫn không được. 1 tháng sau thì mình gặp được bài này :3
Như vậy là tạo 1 site bắt đầu với localhost (của mình sẽ là http://localhostchuong.000webhostapp.com) bằng 000webhost.com và có jwk.json chứa thông tin key tạo qua burp(tham khảo bài trên):

Key tạo bằng burp:

Sẽ được:

Và giờ chỉ việc sign bằng key có sẵn:


Kết quả:

Flag: RM{4lw4y5_ch3ck_th3_JKU_C0rr3ctly}

Tổng kết

Mình có vẽ 1 mindmap nhỏ về JWT Attacks mà mình tìm hiểu được ^^

Một số nguồn tham khảo thêm:

Một số tools: